2 * Asterisk -- An open source telephony toolkit.
4 * Copyright (C) 2004, Constantine Filin and Christos Ricudis
6 * Christos Ricudis <ricudis@itc.auth.gr>
7 * Constantine Filin <cf@intermedia.net>
9 * See http://www.asterisk.org for more information about
10 * the Asterisk project. Please do not directly contact
11 * any of the maintainers of this project for assistance;
12 * the project provides a web site, mailing lists and IRC
13 * channels for your use.
15 * This program is free software, distributed under the terms of
16 * the GNU General Public License Version 2. See the LICENSE file
17 * at the top of the source tree.
22 * \brief MYSQL dialplan application
23 * \ingroup applications
27 <depend>mysqlclient</depend>
28 <defaultenabled>no</defaultenabled>
33 #include <mysql/mysql.h>
35 #include "asterisk/file.h"
36 #include "asterisk/logger.h"
37 #include "asterisk/channel.h"
38 #include "asterisk/pbx.h"
39 #include "asterisk/module.h"
40 #include "asterisk/linkedlists.h"
41 #include "asterisk/chanvars.h"
42 #include "asterisk/lock.h"
43 #include "asterisk/options.h"
44 #include "asterisk/app.h"
45 #include "asterisk/config.h"
49 enum { NULLSTRING, NULLVALUE, EMPTYSTRING } nullvalue = NULLSTRING;
51 static const char app[] = "MYSQL";
53 static const char synopsis[] = "Do several mySQLy things";
55 static const char descrip[] =
56 "MYSQL(): Do several mySQLy things\n"
58 " MYSQL(Set timeout <num>)\n"
59 " Set the connection timeout, in seconds.\n"
60 " MYSQL(Connect connid dhhost dbuser dbpass dbname)\n"
61 " Connects to a database. Arguments contain standard MySQL parameters\n"
62 " passed to function mysql_real_connect. Connection identifer returned\n"
64 " MYSQL(Query resultid ${connid} query-string)\n"
65 " Executes standard MySQL query contained in query-string using established\n"
66 " connection identified by ${connid}. Result of query is stored in ${resultid}.\n"
67 " MYSQL(Nextresult resultid ${connid}\n"
68 " If last query returned more than one result set, it stores the next\n"
69 " result set in ${resultid}. It's useful with stored procedures\n"
70 " MYSQL(Fetch fetchid ${resultid} var1 var2 ... varN)\n"
71 " Fetches a single row from a result set contained in ${result_identifier}.\n"
72 " Assigns returned fields to ${var1} ... ${varn}. ${fetchid} is set TRUE\n"
73 " if additional rows exist in result set.\n"
74 " MYSQL(Clear ${resultid})\n"
75 " Frees memory and datastructures associated with result set.\n"
76 " MYSQL(Disconnect ${connid})\n"
77 " Disconnects from named connection to MySQL.\n"
78 " On exit, always returns 0. Sets MYSQL_STATUS to 0 on success and -1 on error.\n";
83 exten => s,2,MYSQL(Connect connid localhost asterisk mypass credit)
84 exten => s,3,MYSQL(Query resultid ${connid} SELECT username,credit FROM credit WHERE callerid=${CALLERIDNUM})
85 exten => s,4,MYSQL(Fetch fetchid ${resultid} datavar1 datavar2)
86 exten => s,5,GotoIf(${fetchid}?6:8)
87 exten => s,6,Festival("User ${datavar1} currently has credit balance of ${datavar2} dollars.")
88 exten => s,7,Goto(s,4)
89 exten => s,8,MYSQL(Clear ${resultid})
90 exten => s,9,MYSQL(Disconnect ${connid})
93 AST_MUTEX_DEFINE_STATIC(_mysql_mutex);
95 #define MYSQL_CONFIG "app_mysql.conf"
96 #define MYSQL_CONFIG_OLD "mysql.conf"
97 #define AST_MYSQL_ID_DUMMY 0
98 #define AST_MYSQL_ID_CONNID 1
99 #define AST_MYSQL_ID_RESID 2
100 #define AST_MYSQL_ID_FETCHID 3
102 static int autoclear = 0;
104 static void mysql_ds_destroy(void *data);
105 static void mysql_ds_fixup(void *data, struct ast_channel *oldchan, struct ast_channel *newchan);
107 static struct ast_datastore_info mysql_ds_info = {
108 .type = "APP_ADDON_SQL_MYSQL",
109 .destroy = mysql_ds_destroy,
110 .chan_fixup = mysql_ds_fixup,
113 struct ast_MYSQL_id {
114 struct ast_channel *owner;
115 int identifier_type; /* 0=dummy, 1=connid, 2=resultid */
118 AST_LIST_ENTRY(ast_MYSQL_id) entries;
121 AST_LIST_HEAD(MYSQLidshead,ast_MYSQL_id) _mysql_ids_head;
123 static void mysql_ds_destroy(void *data)
125 /* Destroy any IDs owned by the channel */
126 struct ast_MYSQL_id *i;
127 if (AST_LIST_LOCK(&_mysql_ids_head)) {
128 ast_log(LOG_WARNING, "Unable to lock identifiers list\n");
130 AST_LIST_TRAVERSE_SAFE_BEGIN(&_mysql_ids_head, i, entries) {
131 if (i->owner == data) {
132 AST_LIST_REMOVE_CURRENT(entries);
133 if (i->identifier_type == AST_MYSQL_ID_CONNID) {
134 /* Drop connection */
135 mysql_close(i->data);
136 } else if (i->identifier_type == AST_MYSQL_ID_RESID) {
138 mysql_free_result(i->data);
143 AST_LIST_TRAVERSE_SAFE_END
144 AST_LIST_UNLOCK(&_mysql_ids_head);
148 static void mysql_ds_fixup(void *data, struct ast_channel *oldchan, struct ast_channel *newchan)
150 /* Destroy any IDs owned by the channel */
151 struct ast_MYSQL_id *i;
152 if (AST_LIST_LOCK(&_mysql_ids_head)) {
153 ast_log(LOG_WARNING, "Unable to lock identifiers list\n");
155 AST_LIST_TRAVERSE_SAFE_BEGIN(&_mysql_ids_head, i, entries) {
156 if (i->owner == data) {
157 AST_LIST_REMOVE_CURRENT(entries);
158 if (i->identifier_type == AST_MYSQL_ID_CONNID) {
159 /* Drop connection */
160 mysql_close(i->data);
161 } else if (i->identifier_type == AST_MYSQL_ID_RESID) {
163 mysql_free_result(i->data);
168 AST_LIST_TRAVERSE_SAFE_END
169 AST_LIST_UNLOCK(&_mysql_ids_head);
174 static void *find_identifier(int identifier, int identifier_type)
176 struct MYSQLidshead *headp = &_mysql_ids_head;
177 struct ast_MYSQL_id *i;
181 if (AST_LIST_LOCK(headp)) {
182 ast_log(LOG_WARNING, "Unable to lock identifiers list\n");
184 AST_LIST_TRAVERSE(headp, i, entries) {
185 if ((i->identifier == identifier) && (i->identifier_type == identifier_type)) {
192 ast_log(LOG_WARNING, "Identifier %d, identifier_type %d not found in identifier list\n", identifier, identifier_type);
194 AST_LIST_UNLOCK(headp);
200 static int add_identifier(struct ast_channel *chan, int identifier_type, void *data)
202 struct ast_MYSQL_id *i = NULL, *j = NULL;
203 struct MYSQLidshead *headp = &_mysql_ids_head;
204 int maxidentifier = 0;
206 if (AST_LIST_LOCK(headp)) {
207 ast_log(LOG_WARNING, "Unable to lock identifiers list\n");
210 i = malloc(sizeof(*i));
211 AST_LIST_TRAVERSE(headp, j, entries) {
212 if (j->identifier > maxidentifier) {
213 maxidentifier = j->identifier;
216 i->identifier = maxidentifier + 1;
217 i->identifier_type = identifier_type;
220 AST_LIST_INSERT_HEAD(headp, i, entries);
221 AST_LIST_UNLOCK(headp);
223 return i->identifier;
226 static int del_identifier(int identifier, int identifier_type)
228 struct ast_MYSQL_id *i;
229 struct MYSQLidshead *headp = &_mysql_ids_head;
232 if (AST_LIST_LOCK(headp)) {
233 ast_log(LOG_WARNING, "Unable to lock identifiers list\n");
235 AST_LIST_TRAVERSE(headp, i, entries) {
236 if ((i->identifier == identifier) &&
237 (i->identifier_type == identifier_type)) {
238 AST_LIST_REMOVE(headp, i, entries);
244 AST_LIST_UNLOCK(headp);
248 ast_log(LOG_WARNING, "Could not find identifier %d, identifier_type %d in list to delete\n", identifier, identifier_type);
255 static int set_asterisk_int(struct ast_channel *chan, char *varname, int id)
259 snprintf(s, sizeof(s), "%d", id);
260 ast_debug(5, "MYSQL: setting var '%s' to value '%s'\n", varname, s);
261 pbx_builtin_setvar_helper(chan, varname, s);
266 static int add_identifier_and_set_asterisk_int(struct ast_channel *chan, char *varname, int identifier_type, void *data)
268 return set_asterisk_int(chan, varname, add_identifier(chan, identifier_type, data));
271 static int safe_scan_int(char **data, char *delim, int def)
275 char *s = strsep(data, delim);
277 res = strtol(s, &end, 10);
279 res = def; /* not an integer */
284 static int aMYSQL_set(struct ast_channel *chan, char *data)
287 AST_DECLARE_APP_ARGS(args,
289 AST_APP_ARG(variable);
293 AST_NONSTANDARD_APP_ARGS(args, data, ' ');
295 if (args.argc == 3) {
296 var = alloca(6 + strlen(args.variable) + 1);
297 sprintf(var, "MYSQL_%s", args.variable);
299 /* Make the parameter case-insensitive */
300 for (tmp = var + 6; *tmp; tmp++)
301 *tmp = toupper(*tmp);
303 pbx_builtin_setvar_helper(chan, var, args.value);
308 /* MYSQL operations */
309 static int aMYSQL_connect(struct ast_channel *chan, char *data)
311 AST_DECLARE_APP_ARGS(args,
312 AST_APP_ARG(connect);
321 const char *ctimeout;
323 AST_NONSTANDARD_APP_ARGS(args, data, ' ');
325 if (args.argc != 6) {
326 ast_log(LOG_WARNING, "MYSQL_connect is missing some arguments\n");
330 if (!(mysql = mysql_init(NULL))) {
331 ast_log(LOG_WARNING, "mysql_init returned NULL\n");
335 ctimeout = pbx_builtin_getvar_helper(chan, "MYSQL_TIMEOUT");
336 if (ctimeout && sscanf(ctimeout, "%30d", &timeout) == 1) {
337 mysql_options(mysql, MYSQL_OPT_CONNECT_TIMEOUT, (void *)&timeout);
340 if (! mysql_real_connect(mysql, args.dbhost, args.dbuser, args.dbpass, args.dbname, 0, NULL,
341 #ifdef CLIENT_MULTI_STATEMENTS
342 CLIENT_MULTI_STATEMENTS | CLIENT_MULTI_RESULTS
343 #elif defined(CLIENT_MULTI_QUERIES)
349 ast_log(LOG_WARNING, "mysql_real_connect(mysql,%s,%s,dbpass,%s,...) failed(%d): %s\n",
350 args.dbhost, args.dbuser, args.dbname, mysql_errno(mysql), mysql_error(mysql));
354 add_identifier_and_set_asterisk_int(chan, args.connid, AST_MYSQL_ID_CONNID, mysql);
358 static int aMYSQL_query(struct ast_channel *chan, char *data)
360 AST_DECLARE_APP_ARGS(args,
362 AST_APP_ARG(resultid);
371 AST_NONSTANDARD_APP_ARGS(args, data, ' ');
373 if (args.argc != 4 || (connid = atoi(args.connid)) == 0) {
374 ast_log(LOG_WARNING, "missing some arguments\n");
378 if (!(mysql = find_identifier(connid, AST_MYSQL_ID_CONNID))) {
379 ast_log(LOG_WARNING, "Invalid connection identifier %s passed in aMYSQL_query\n", args.connid);
383 if ((mysql_query_res = mysql_query(mysql, args.sql)) != 0) {
384 ast_log(LOG_WARNING, "aMYSQL_query: mysql_query failed. Error: %s\n", mysql_error(mysql));
388 if ((mysqlres = mysql_store_result(mysql))) {
389 add_identifier_and_set_asterisk_int(chan, args.resultid, AST_MYSQL_ID_RESID, mysqlres);
391 } else if (!mysql_field_count(mysql)) {
394 ast_log(LOG_WARNING, "mysql_store_result() failed on query %s\n", args.sql);
399 static int aMYSQL_nextresult(struct ast_channel *chan, char *data)
403 AST_DECLARE_APP_ARGS(args,
404 AST_APP_ARG(nextresult);
405 AST_APP_ARG(resultid);
410 AST_NONSTANDARD_APP_ARGS(args, data, ' ');
411 sscanf(args.connid, "%30d", &connid);
413 if (args.argc != 3 || connid <= 0) {
414 ast_log(LOG_WARNING, "missing some arguments\n");
418 if (!(mysql = find_identifier(connid, AST_MYSQL_ID_CONNID))) {
419 ast_log(LOG_WARNING, "Invalid connection identifier %d passed in aMYSQL_query\n", connid);
423 if (mysql_more_results(mysql)) {
424 mysql_next_result(mysql);
425 if ((mysqlres = mysql_store_result(mysql))) {
426 add_identifier_and_set_asterisk_int(chan, args.resultid, AST_MYSQL_ID_RESID, mysqlres);
428 } else if (!mysql_field_count(mysql)) {
431 ast_log(LOG_WARNING, "mysql_store_result() failed on storing next_result\n");
433 ast_log(LOG_WARNING, "mysql_more_results() result set has no more results\n");
439 static int aMYSQL_fetch(struct ast_channel *chan, char *data)
443 AST_DECLARE_APP_ARGS(args,
445 AST_APP_ARG(resultvar);
446 AST_APP_ARG(fetchid);
450 int resultid = -1, numFields, j;
452 parse = ast_strdupa(data);
453 AST_NONSTANDARD_APP_ARGS(args, parse, ' ');
454 sscanf(args.fetchid, "%30d", &resultid);
456 if (args.resultvar && (resultid >= 0) ) {
457 if ((mysqlres = find_identifier(resultid, AST_MYSQL_ID_RESID)) != NULL) {
458 /* Grab the next row */
459 if ((mysqlrow = mysql_fetch_row(mysqlres)) != NULL) {
460 numFields = mysql_num_fields(mysqlres);
461 for (j = 0; j < numFields; j++) {
462 s5 = strsep(&args.vars, " ");
464 ast_log(LOG_WARNING, "ast_MYSQL_fetch: More fields (%d) than variables (%d)\n", numFields, j);
468 pbx_builtin_setvar_helper(chan, s5, mysqlrow[j] ? mysqlrow[j] :
469 nullvalue == NULLSTRING ? "NULL" :
470 nullvalue == EMPTYSTRING ? "" :
473 ast_debug(5, "ast_MYSQL_fetch: numFields=%d\n", numFields);
474 set_asterisk_int(chan, args.resultvar, 1); /* try more rows */
476 ast_debug(5, "ast_MYSQL_fetch : EOF\n");
477 set_asterisk_int(chan, args.resultvar, 0); /* no more rows */
481 set_asterisk_int(chan, args.resultvar, 0);
482 ast_log(LOG_WARNING, "aMYSQL_fetch: Invalid result identifier %d passed\n", resultid);
485 ast_log(LOG_WARNING, "aMYSQL_fetch: missing some arguments\n");
491 static int aMYSQL_clear(struct ast_channel *chan, char *data)
496 strsep(&data, " "); /* eat the first token, we already know it :P */
497 id = safe_scan_int(&data, " \n", -1);
498 if ((mysqlres = find_identifier(id, AST_MYSQL_ID_RESID)) == NULL) {
499 ast_log(LOG_WARNING, "Invalid result identifier %d passed in aMYSQL_clear\n", id);
501 mysql_free_result(mysqlres);
502 del_identifier(id, AST_MYSQL_ID_RESID);
508 static int aMYSQL_disconnect(struct ast_channel *chan, char *data)
512 strsep(&data, " "); /* eat the first token, we already know it :P */
514 id = safe_scan_int(&data, " \n", -1);
515 if ((mysql = find_identifier(id, AST_MYSQL_ID_CONNID)) == NULL) {
516 ast_log(LOG_WARNING, "Invalid connection identifier %d passed in aMYSQL_disconnect\n", id);
519 del_identifier(id, AST_MYSQL_ID_CONNID);
525 static int MYSQL_exec(struct ast_channel *chan, const char *data)
530 ast_debug(5, "MYSQL: data=%s\n", data);
533 ast_log(LOG_WARNING, "MYSQL requires an argument (see manual)\n");
540 struct ast_datastore *mysql_store = ast_channel_datastore_find(chan, &mysql_ds_info, NULL);
542 if (!(mysql_store = ast_datastore_alloc(&mysql_ds_info, NULL))) {
543 ast_log(LOG_WARNING, "Unable to allocate new datastore.\n");
545 mysql_store->data = chan;
546 ast_channel_datastore_add(chan, mysql_store);
550 ast_mutex_lock(&_mysql_mutex);
552 if (strncasecmp("connect", data, strlen("connect")) == 0) {
553 result = aMYSQL_connect(chan, ast_strdupa(data));
554 } else if (strncasecmp("query", data, strlen("query")) == 0) {
555 result = aMYSQL_query(chan, ast_strdupa(data));
556 } else if (strncasecmp("nextresult", data, strlen("nextresult")) == 0) {
557 result = aMYSQL_nextresult(chan, ast_strdupa(data));
558 } else if (strncasecmp("fetch", data, strlen("fetch")) == 0) {
559 result = aMYSQL_fetch(chan, ast_strdupa(data));
560 } else if (strncasecmp("clear", data, strlen("clear")) == 0) {
561 result = aMYSQL_clear(chan, ast_strdupa(data));
562 } else if (strncasecmp("disconnect", data, strlen("disconnect")) == 0) {
563 result = aMYSQL_disconnect(chan, ast_strdupa(data));
564 } else if (strncasecmp("set", data, 3) == 0) {
565 result = aMYSQL_set(chan, ast_strdupa(data));
567 ast_log(LOG_WARNING, "Unknown argument to MYSQL application : %s\n", data);
571 ast_mutex_unlock(&_mysql_mutex);
573 snprintf(sresult, sizeof(sresult), "%d", result);
574 pbx_builtin_setvar_helper(chan, "MYSQL_STATUS", sresult);
578 static int unload_module(void)
580 return ast_unregister_application(app);
583 static int load_module(void)
585 struct MYSQLidshead *headp = &_mysql_ids_head;
586 struct ast_flags config_flags = { 0 };
587 struct ast_config *cfg = ast_config_load(MYSQL_CONFIG, config_flags);
591 /* Backwards compatibility ftw */
592 cfg = ast_config_load(MYSQL_CONFIG_OLD, config_flags);
596 if ((temp = ast_variable_retrieve(cfg, "general", "nullvalue"))) {
597 if (!strcasecmp(temp, "nullstring")) {
598 nullvalue = NULLSTRING;
599 } else if (!strcasecmp(temp, "emptystring")) {
600 nullvalue = EMPTYSTRING;
601 } else if (!strcasecmp(temp, "null")) {
602 nullvalue = NULLVALUE;
604 ast_log(LOG_WARNING, "Illegal value for 'nullvalue': '%s' (must be 'nullstring', 'null', or 'emptystring')\n", temp);
607 if ((temp = ast_variable_retrieve(cfg, "general", "autoclear")) && ast_true(temp)) {
610 ast_config_destroy(cfg);
613 AST_LIST_HEAD_INIT(headp);
614 return ast_register_application(app, MYSQL_exec, synopsis, descrip);
617 AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "Simple Mysql Interface");