/* * Asterisk -- An open source telephony toolkit. * * Copyright (C) 2004, Constantine Filin and Christos Ricudis * * Christos Ricudis * Constantine Filin * * See http://www.asterisk.org for more information about * the Asterisk project. Please do not directly contact * any of the maintainers of this project for assistance; * the project provides a web site, mailing lists and IRC * channels for your use. * * This program is free software, distributed under the terms of * the GNU General Public License Version 2. See the LICENSE file * at the top of the source tree. */ /*! * \file * \brief MYSQL dialplan application * \ingroup applications */ /*** MODULEINFO mysqlclient no ***/ #include "asterisk.h" #include #include "asterisk/file.h" #include "asterisk/logger.h" #include "asterisk/channel.h" #include "asterisk/pbx.h" #include "asterisk/module.h" #include "asterisk/linkedlists.h" #include "asterisk/chanvars.h" #include "asterisk/lock.h" #include "asterisk/options.h" #include "asterisk/app.h" #include "asterisk/config.h" #define EXTRA_LOG 0 enum { NULLSTRING, NULLVALUE, EMPTYSTRING } nullvalue = NULLSTRING; static const char app[] = "MYSQL"; static const char synopsis[] = "Do several mySQLy things"; static const char descrip[] = "MYSQL(): Do several mySQLy things\n" "Syntax:\n" " MYSQL(Set timeout )\n" " Set the connection timeout, in seconds.\n" " MYSQL(Connect connid dhhost dbuser dbpass dbname)\n" " Connects to a database. Arguments contain standard MySQL parameters\n" " passed to function mysql_real_connect. Connection identifer returned\n" " in ${connid}\n" " MYSQL(Query resultid ${connid} query-string)\n" " Executes standard MySQL query contained in query-string using established\n" " connection identified by ${connid}. Result of query is stored in ${resultid}.\n" " MYSQL(Nextresult resultid ${connid}\n" " If last query returned more than one result set, it stores the next\n" " result set in ${resultid}. It's useful with stored procedures\n" " MYSQL(Fetch fetchid ${resultid} var1 var2 ... varN)\n" " Fetches a single row from a result set contained in ${result_identifier}.\n" " Assigns returned fields to ${var1} ... ${varn}. ${fetchid} is set TRUE\n" " if additional rows exist in result set.\n" " MYSQL(Clear ${resultid})\n" " Frees memory and datastructures associated with result set.\n" " MYSQL(Disconnect ${connid})\n" " Disconnects from named connection to MySQL.\n" " On exit, always returns 0. Sets MYSQL_STATUS to 0 on success and -1 on error.\n"; /* EXAMPLES OF USE : exten => s,2,MYSQL(Connect connid localhost asterisk mypass credit) exten => s,3,MYSQL(Query resultid ${connid} SELECT username,credit FROM credit WHERE callerid=${CALLERIDNUM}) exten => s,4,MYSQL(Fetch fetchid ${resultid} datavar1 datavar2) exten => s,5,GotoIf(${fetchid}?6:8) exten => s,6,Festival("User ${datavar1} currently has credit balance of ${datavar2} dollars.") exten => s,7,Goto(s,4) exten => s,8,MYSQL(Clear ${resultid}) exten => s,9,MYSQL(Disconnect ${connid}) */ AST_MUTEX_DEFINE_STATIC(_mysql_mutex); #define MYSQL_CONFIG "app_mysql.conf" #define MYSQL_CONFIG_OLD "mysql.conf" #define AST_MYSQL_ID_DUMMY 0 #define AST_MYSQL_ID_CONNID 1 #define AST_MYSQL_ID_RESID 2 #define AST_MYSQL_ID_FETCHID 3 static int autoclear = 0; static void mysql_ds_destroy(void *data); static void mysql_ds_fixup(void *data, struct ast_channel *oldchan, struct ast_channel *newchan); static struct ast_datastore_info mysql_ds_info = { .type = "APP_ADDON_SQL_MYSQL", .destroy = mysql_ds_destroy, .chan_fixup = mysql_ds_fixup, }; struct ast_MYSQL_id { struct ast_channel *owner; int identifier_type; /* 0=dummy, 1=connid, 2=resultid */ int identifier; void *data; AST_LIST_ENTRY(ast_MYSQL_id) entries; } *ast_MYSQL_id; AST_LIST_HEAD(MYSQLidshead,ast_MYSQL_id) _mysql_ids_head; static void mysql_ds_destroy(void *data) { /* Destroy any IDs owned by the channel */ struct ast_MYSQL_id *i; if (AST_LIST_LOCK(&_mysql_ids_head)) { ast_log(LOG_WARNING, "Unable to lock identifiers list\n"); } else { AST_LIST_TRAVERSE_SAFE_BEGIN(&_mysql_ids_head, i, entries) { if (i->owner == data) { AST_LIST_REMOVE_CURRENT(entries); if (i->identifier_type == AST_MYSQL_ID_CONNID) { /* Drop connection */ mysql_close(i->data); } else if (i->identifier_type == AST_MYSQL_ID_RESID) { /* Drop result */ mysql_free_result(i->data); } ast_free(i); } } AST_LIST_TRAVERSE_SAFE_END AST_LIST_UNLOCK(&_mysql_ids_head); } } static void mysql_ds_fixup(void *data, struct ast_channel *oldchan, struct ast_channel *newchan) { /* Destroy any IDs owned by the channel */ struct ast_MYSQL_id *i; if (AST_LIST_LOCK(&_mysql_ids_head)) { ast_log(LOG_WARNING, "Unable to lock identifiers list\n"); } else { AST_LIST_TRAVERSE_SAFE_BEGIN(&_mysql_ids_head, i, entries) { if (i->owner == data) { AST_LIST_REMOVE_CURRENT(entries); if (i->identifier_type == AST_MYSQL_ID_CONNID) { /* Drop connection */ mysql_close(i->data); } else if (i->identifier_type == AST_MYSQL_ID_RESID) { /* Drop result */ mysql_free_result(i->data); } ast_free(i); } } AST_LIST_TRAVERSE_SAFE_END AST_LIST_UNLOCK(&_mysql_ids_head); } } /* helpful procs */ static void *find_identifier(int identifier, int identifier_type) { struct MYSQLidshead *headp = &_mysql_ids_head; struct ast_MYSQL_id *i; void *res=NULL; int found=0; if (AST_LIST_LOCK(headp)) { ast_log(LOG_WARNING, "Unable to lock identifiers list\n"); } else { AST_LIST_TRAVERSE(headp, i, entries) { if ((i->identifier == identifier) && (i->identifier_type == identifier_type)) { found = 1; res = i->data; break; } } if (!found) { ast_log(LOG_WARNING, "Identifier %d, identifier_type %d not found in identifier list\n", identifier, identifier_type); } AST_LIST_UNLOCK(headp); } return res; } static int add_identifier(struct ast_channel *chan, int identifier_type, void *data) { struct ast_MYSQL_id *i = NULL, *j = NULL; struct MYSQLidshead *headp = &_mysql_ids_head; int maxidentifier = 0; if (AST_LIST_LOCK(headp)) { ast_log(LOG_WARNING, "Unable to lock identifiers list\n"); return -1; } else { i = malloc(sizeof(*i)); AST_LIST_TRAVERSE(headp, j, entries) { if (j->identifier > maxidentifier) { maxidentifier = j->identifier; } } i->identifier = maxidentifier + 1; i->identifier_type = identifier_type; i->data = data; i->owner = chan; AST_LIST_INSERT_HEAD(headp, i, entries); AST_LIST_UNLOCK(headp); } return i->identifier; } static int del_identifier(int identifier, int identifier_type) { struct ast_MYSQL_id *i; struct MYSQLidshead *headp = &_mysql_ids_head; int found = 0; if (AST_LIST_LOCK(headp)) { ast_log(LOG_WARNING, "Unable to lock identifiers list\n"); } else { AST_LIST_TRAVERSE(headp, i, entries) { if ((i->identifier == identifier) && (i->identifier_type == identifier_type)) { AST_LIST_REMOVE(headp, i, entries); free(i); found = 1; break; } } AST_LIST_UNLOCK(headp); } if (found == 0) { ast_log(LOG_WARNING, "Could not find identifier %d, identifier_type %d in list to delete\n", identifier, identifier_type); return -1; } else { return 0; } } static int set_asterisk_int(struct ast_channel *chan, char *varname, int id) { if (id >= 0) { char s[12] = ""; snprintf(s, sizeof(s), "%d", id); ast_debug(5, "MYSQL: setting var '%s' to value '%s'\n", varname, s); pbx_builtin_setvar_helper(chan, varname, s); } return id; } static int add_identifier_and_set_asterisk_int(struct ast_channel *chan, char *varname, int identifier_type, void *data) { return set_asterisk_int(chan, varname, add_identifier(chan, identifier_type, data)); } static int safe_scan_int(char **data, char *delim, int def) { char *end; int res = def; char *s = strsep(data, delim); if (s) { res = strtol(s, &end, 10); if (*end) res = def; /* not an integer */ } return res; } static int aMYSQL_set(struct ast_channel *chan, char *data) { char *var, *tmp; AST_DECLARE_APP_ARGS(args, AST_APP_ARG(set); AST_APP_ARG(variable); AST_APP_ARG(value); ); AST_NONSTANDARD_APP_ARGS(args, data, ' '); if (args.argc == 3) { var = alloca(6 + strlen(args.variable) + 1); sprintf(var, "MYSQL_%s", args.variable); /* Make the parameter case-insensitive */ for (tmp = var + 6; *tmp; tmp++) *tmp = toupper(*tmp); pbx_builtin_setvar_helper(chan, var, args.value); } return 0; } /* MYSQL operations */ static int aMYSQL_connect(struct ast_channel *chan, char *data) { AST_DECLARE_APP_ARGS(args, AST_APP_ARG(connect); AST_APP_ARG(connid); AST_APP_ARG(dbhost); AST_APP_ARG(dbuser); AST_APP_ARG(dbpass); AST_APP_ARG(dbname); ); MYSQL *mysql; int timeout; const char *ctimeout; AST_NONSTANDARD_APP_ARGS(args, data, ' '); if (args.argc != 6) { ast_log(LOG_WARNING, "MYSQL_connect is missing some arguments\n"); return -1; } if (!(mysql = mysql_init(NULL))) { ast_log(LOG_WARNING, "mysql_init returned NULL\n"); return -1; } ctimeout = pbx_builtin_getvar_helper(chan, "MYSQL_TIMEOUT"); if (ctimeout && sscanf(ctimeout, "%30d", &timeout) == 1) { mysql_options(mysql, MYSQL_OPT_CONNECT_TIMEOUT, (void *)&timeout); } if (! mysql_real_connect(mysql, args.dbhost, args.dbuser, args.dbpass, args.dbname, 0, NULL, #ifdef CLIENT_MULTI_STATEMENTS CLIENT_MULTI_STATEMENTS | CLIENT_MULTI_RESULTS #elif defined(CLIENT_MULTI_QUERIES) CLIENT_MULTI_QUERIES #else 0 #endif )) { ast_log(LOG_WARNING, "mysql_real_connect(mysql,%s,%s,dbpass,%s,...) failed(%d): %s\n", args.dbhost, args.dbuser, args.dbname, mysql_errno(mysql), mysql_error(mysql)); return -1; } add_identifier_and_set_asterisk_int(chan, args.connid, AST_MYSQL_ID_CONNID, mysql); return 0; } static int aMYSQL_query(struct ast_channel *chan, char *data) { AST_DECLARE_APP_ARGS(args, AST_APP_ARG(query); AST_APP_ARG(resultid); AST_APP_ARG(connid); AST_APP_ARG(sql); ); MYSQL *mysql; MYSQL_RES *mysqlres; int connid; int mysql_query_res; AST_NONSTANDARD_APP_ARGS(args, data, ' '); if (args.argc != 4 || (connid = atoi(args.connid)) == 0) { ast_log(LOG_WARNING, "missing some arguments\n"); return -1; } if (!(mysql = find_identifier(connid, AST_MYSQL_ID_CONNID))) { ast_log(LOG_WARNING, "Invalid connection identifier %s passed in aMYSQL_query\n", args.connid); return -1; } if ((mysql_query_res = mysql_query(mysql, args.sql)) != 0) { ast_log(LOG_WARNING, "aMYSQL_query: mysql_query failed. Error: %s\n", mysql_error(mysql)); return -1; } if ((mysqlres = mysql_store_result(mysql))) { add_identifier_and_set_asterisk_int(chan, args.resultid, AST_MYSQL_ID_RESID, mysqlres); return 0; } else if (!mysql_field_count(mysql)) { return 0; } else ast_log(LOG_WARNING, "mysql_store_result() failed on query %s\n", args.sql); return -1; } static int aMYSQL_nextresult(struct ast_channel *chan, char *data) { MYSQL *mysql; MYSQL_RES *mysqlres; AST_DECLARE_APP_ARGS(args, AST_APP_ARG(nextresult); AST_APP_ARG(resultid); AST_APP_ARG(connid); ); int connid = -1; AST_NONSTANDARD_APP_ARGS(args, data, ' '); sscanf(args.connid, "%30d", &connid); if (args.argc != 3 || connid <= 0) { ast_log(LOG_WARNING, "missing some arguments\n"); return -1; } if (!(mysql = find_identifier(connid, AST_MYSQL_ID_CONNID))) { ast_log(LOG_WARNING, "Invalid connection identifier %d passed in aMYSQL_query\n", connid); return -1; } if (mysql_more_results(mysql)) { mysql_next_result(mysql); if ((mysqlres = mysql_store_result(mysql))) { add_identifier_and_set_asterisk_int(chan, args.resultid, AST_MYSQL_ID_RESID, mysqlres); return 0; } else if (!mysql_field_count(mysql)) { return 0; } else ast_log(LOG_WARNING, "mysql_store_result() failed on storing next_result\n"); } else ast_log(LOG_WARNING, "mysql_more_results() result set has no more results\n"); return 0; } static int aMYSQL_fetch(struct ast_channel *chan, char *data) { MYSQL_RES *mysqlres; MYSQL_ROW mysqlrow; AST_DECLARE_APP_ARGS(args, AST_APP_ARG(fetch); AST_APP_ARG(resultvar); AST_APP_ARG(fetchid); AST_APP_ARG(vars); ); char *s5, *parse; int resultid = -1, numFields, j; parse = ast_strdupa(data); AST_NONSTANDARD_APP_ARGS(args, parse, ' '); sscanf(args.fetchid, "%30d", &resultid); if (args.resultvar && (resultid >= 0) ) { if ((mysqlres = find_identifier(resultid, AST_MYSQL_ID_RESID)) != NULL) { /* Grab the next row */ if ((mysqlrow = mysql_fetch_row(mysqlres)) != NULL) { numFields = mysql_num_fields(mysqlres); for (j = 0; j < numFields; j++) { s5 = strsep(&args.vars, " "); if (s5 == NULL) { ast_log(LOG_WARNING, "ast_MYSQL_fetch: More fields (%d) than variables (%d)\n", numFields, j); break; } pbx_builtin_setvar_helper(chan, s5, mysqlrow[j] ? mysqlrow[j] : nullvalue == NULLSTRING ? "NULL" : nullvalue == EMPTYSTRING ? "" : NULL); } ast_debug(5, "ast_MYSQL_fetch: numFields=%d\n", numFields); set_asterisk_int(chan, args.resultvar, 1); /* try more rows */ } else { ast_debug(5, "ast_MYSQL_fetch : EOF\n"); set_asterisk_int(chan, args.resultvar, 0); /* no more rows */ } return 0; } else { set_asterisk_int(chan, args.resultvar, 0); ast_log(LOG_WARNING, "aMYSQL_fetch: Invalid result identifier %d passed\n", resultid); } } else { ast_log(LOG_WARNING, "aMYSQL_fetch: missing some arguments\n"); } return -1; } static int aMYSQL_clear(struct ast_channel *chan, char *data) { MYSQL_RES *mysqlres; int id; strsep(&data, " "); /* eat the first token, we already know it :P */ id = safe_scan_int(&data, " \n", -1); if ((mysqlres = find_identifier(id, AST_MYSQL_ID_RESID)) == NULL) { ast_log(LOG_WARNING, "Invalid result identifier %d passed in aMYSQL_clear\n", id); } else { mysql_free_result(mysqlres); del_identifier(id, AST_MYSQL_ID_RESID); } return 0; } static int aMYSQL_disconnect(struct ast_channel *chan, char *data) { MYSQL *mysql; int id; strsep(&data, " "); /* eat the first token, we already know it :P */ id = safe_scan_int(&data, " \n", -1); if ((mysql = find_identifier(id, AST_MYSQL_ID_CONNID)) == NULL) { ast_log(LOG_WARNING, "Invalid connection identifier %d passed in aMYSQL_disconnect\n", id); } else { mysql_close(mysql); del_identifier(id, AST_MYSQL_ID_CONNID); } return 0; } static int MYSQL_exec(struct ast_channel *chan, const char *data) { int result; char sresult[10]; ast_debug(5, "MYSQL: data=%s\n", data); if (!data) { ast_log(LOG_WARNING, "MYSQL requires an argument (see manual)\n"); return -1; } result = 0; if (autoclear) { struct ast_datastore *mysql_store = ast_channel_datastore_find(chan, &mysql_ds_info, NULL); if (!mysql_store) { if (!(mysql_store = ast_datastore_alloc(&mysql_ds_info, NULL))) { ast_log(LOG_WARNING, "Unable to allocate new datastore.\n"); } else { mysql_store->data = chan; ast_channel_datastore_add(chan, mysql_store); } } } ast_mutex_lock(&_mysql_mutex); if (strncasecmp("connect", data, strlen("connect")) == 0) { result = aMYSQL_connect(chan, ast_strdupa(data)); } else if (strncasecmp("query", data, strlen("query")) == 0) { result = aMYSQL_query(chan, ast_strdupa(data)); } else if (strncasecmp("nextresult", data, strlen("nextresult")) == 0) { result = aMYSQL_nextresult(chan, ast_strdupa(data)); } else if (strncasecmp("fetch", data, strlen("fetch")) == 0) { result = aMYSQL_fetch(chan, ast_strdupa(data)); } else if (strncasecmp("clear", data, strlen("clear")) == 0) { result = aMYSQL_clear(chan, ast_strdupa(data)); } else if (strncasecmp("disconnect", data, strlen("disconnect")) == 0) { result = aMYSQL_disconnect(chan, ast_strdupa(data)); } else if (strncasecmp("set", data, 3) == 0) { result = aMYSQL_set(chan, ast_strdupa(data)); } else { ast_log(LOG_WARNING, "Unknown argument to MYSQL application : %s\n", data); result = -1; } ast_mutex_unlock(&_mysql_mutex); snprintf(sresult, sizeof(sresult), "%d", result); pbx_builtin_setvar_helper(chan, "MYSQL_STATUS", sresult); return 0; } static int unload_module(void) { return ast_unregister_application(app); } static int load_module(void) { struct MYSQLidshead *headp = &_mysql_ids_head; struct ast_flags config_flags = { 0 }; struct ast_config *cfg = ast_config_load(MYSQL_CONFIG, config_flags); const char *temp; if (!cfg) { /* Backwards compatibility ftw */ cfg = ast_config_load(MYSQL_CONFIG_OLD, config_flags); } if (cfg) { if ((temp = ast_variable_retrieve(cfg, "general", "nullvalue"))) { if (!strcasecmp(temp, "nullstring")) { nullvalue = NULLSTRING; } else if (!strcasecmp(temp, "emptystring")) { nullvalue = EMPTYSTRING; } else if (!strcasecmp(temp, "null")) { nullvalue = NULLVALUE; } else { ast_log(LOG_WARNING, "Illegal value for 'nullvalue': '%s' (must be 'nullstring', 'null', or 'emptystring')\n", temp); } } if ((temp = ast_variable_retrieve(cfg, "general", "autoclear")) && ast_true(temp)) { autoclear = 1; } ast_config_destroy(cfg); } AST_LIST_HEAD_INIT(headp); return ast_register_application(app, MYSQL_exec, synopsis, descrip); } AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "Simple Mysql Interface");