/* * Asterisk -- An open source telephony toolkit. * * Copyright (c) 2005, 2006 Tilghman Lesher * * Tilghman Lesher * * 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 ODBC lookups * * \author Tilghman Lesher * * \ingroup functions */ /*** MODULEINFO unixodbc ***/ #include "asterisk.h" ASTERISK_FILE_VERSION(__FILE__, "$Revision$") #include #include #include #include #include #include #include "asterisk/module.h" #include "asterisk/file.h" #include "asterisk/logger.h" #include "asterisk/options.h" #include "asterisk/channel.h" #include "asterisk/pbx.h" #include "asterisk/module.h" #include "asterisk/config.h" #include "asterisk/res_odbc.h" #include "asterisk/app.h" static char *config = "func_odbc.conf"; enum { OPT_ESCAPECOMMAS = (1 << 0), } odbc_option_flags; struct acf_odbc_query { AST_LIST_ENTRY(acf_odbc_query) list; char readhandle[5][30]; char writehandle[5][30]; char sql_read[2048]; char sql_write[2048]; unsigned int flags; struct ast_custom_function *acf; }; AST_LIST_HEAD_STATIC(queries, acf_odbc_query); static SQLHSTMT generic_prepare(struct odbc_obj *obj, void *data) { int res; char *sql = data; SQLHSTMT stmt; res = SQLAllocHandle (SQL_HANDLE_STMT, obj->con, &stmt); if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) { ast_log(LOG_WARNING, "SQL Alloc Handle failed!\n"); return NULL; } res = SQLPrepare(stmt, (unsigned char *)sql, SQL_NTS); if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) { ast_log(LOG_WARNING, "SQL Prepare failed![%s]\n", sql); SQLFreeHandle (SQL_HANDLE_STMT, stmt); return NULL; } return stmt; } /* * Master control routine */ static int acf_odbc_write(struct ast_channel *chan, const char *cmd, char *s, const char *value) { struct odbc_obj *obj = NULL; struct acf_odbc_query *query; char *t, buf[2048]="", varname[15]; int i, dsn; AST_DECLARE_APP_ARGS(values, AST_APP_ARG(field)[100]; ); AST_DECLARE_APP_ARGS(args, AST_APP_ARG(field)[100]; ); SQLHSTMT stmt; SQLLEN rows=0; AST_LIST_LOCK(&queries); AST_LIST_TRAVERSE(&queries, query, list) { if (!strcmp(query->acf->name, cmd)) { break; } } if (!query) { ast_log(LOG_ERROR, "No such function '%s'\n", cmd); AST_LIST_UNLOCK(&queries); return -1; } /* Parse our arguments */ t = value ? ast_strdupa(value) : ""; if (!s || !t) { ast_log(LOG_ERROR, "Out of memory\n"); AST_LIST_UNLOCK(&queries); return -1; } AST_STANDARD_APP_ARGS(args, s); for (i = 0; i < args.argc; i++) { snprintf(varname, sizeof(varname), "ARG%d", i + 1); pbx_builtin_pushvar_helper(chan, varname, args.field[i]); } /* Parse values, just like arguments */ /* Can't use the pipe, because app Set removes them */ AST_NONSTANDARD_APP_ARGS(values, t, ','); for (i = 0; i < values.argc; i++) { snprintf(varname, sizeof(varname), "VAL%d", i + 1); pbx_builtin_pushvar_helper(chan, varname, values.field[i]); } /* Additionally set the value as a whole (but push an empty string if value is NULL) */ pbx_builtin_pushvar_helper(chan, "VALUE", value ? value : ""); pbx_substitute_variables_helper(chan, query->sql_write, buf, sizeof(buf) - 1); /* Restore prior values */ for (i = 0; i < args.argc; i++) { snprintf(varname, sizeof(varname), "ARG%d", i + 1); pbx_builtin_setvar_helper(chan, varname, NULL); } for (i = 0; i < values.argc; i++) { snprintf(varname, sizeof(varname), "VAL%d", i + 1); pbx_builtin_setvar_helper(chan, varname, NULL); } pbx_builtin_setvar_helper(chan, "VALUE", NULL); AST_LIST_UNLOCK(&queries); for (dsn = 0; dsn < 5; dsn++) { if (!ast_strlen_zero(query->writehandle[dsn])) { obj = ast_odbc_request_obj(query->writehandle[dsn], 0); if (obj) stmt = ast_odbc_prepare_and_execute(obj, generic_prepare, buf); } if (stmt) break; } if (stmt) { /* Rows affected */ SQLRowCount(stmt, &rows); } /* Output the affected rows, for all cases. In the event of failure, we * flag this as -1 rows. Note that this is different from 0 affected rows * which would be the case if we succeeded in our query, but the values did * not change. */ snprintf(varname, sizeof(varname), "%d", (int)rows); pbx_builtin_setvar_helper(chan, "ODBCROWS", varname); if (stmt) SQLFreeHandle(SQL_HANDLE_STMT, stmt); if (obj) ast_odbc_release_obj(obj); return 0; } static int acf_odbc_read(struct ast_channel *chan, const char *cmd, char *s, char *buf, size_t len) { struct odbc_obj *obj = NULL; struct acf_odbc_query *query; char sql[2048] = "", varname[15], colnames[2048] = ""; int res, x, buflen = 0, escapecommas, dsn; AST_DECLARE_APP_ARGS(args, AST_APP_ARG(field)[100]; ); SQLHSTMT stmt; SQLSMALLINT colcount=0; SQLLEN indicator; SQLSMALLINT collength; AST_LIST_LOCK(&queries); AST_LIST_TRAVERSE(&queries, query, list) { if (!strcmp(query->acf->name, cmd)) { break; } } if (!query) { ast_log(LOG_ERROR, "No such function '%s'\n", cmd); AST_LIST_UNLOCK(&queries); return -1; } AST_STANDARD_APP_ARGS(args, s); for (x = 0; x < args.argc; x++) { snprintf(varname, sizeof(varname), "ARG%d", x + 1); pbx_builtin_pushvar_helper(chan, varname, args.field[x]); } pbx_substitute_variables_helper(chan, query->sql_read, sql, sizeof(sql) - 1); /* Restore prior values */ for (x = 0; x < args.argc; x++) { snprintf(varname, sizeof(varname), "ARG%d", x + 1); pbx_builtin_setvar_helper(chan, varname, NULL); } /* Save this flag, so we can release the lock */ escapecommas = ast_test_flag(query, OPT_ESCAPECOMMAS); AST_LIST_UNLOCK(&queries); for (dsn = 0; dsn < 5; dsn++) { if (!ast_strlen_zero(query->writehandle[dsn])) { obj = ast_odbc_request_obj(query->writehandle[dsn], 0); if (obj) stmt = ast_odbc_prepare_and_execute(obj, generic_prepare, sql); } if (stmt) break; } if (!stmt) { ast_log(LOG_ERROR, "Unable to execute query [%s]\n", sql); if (obj) ast_odbc_release_obj(obj); return -1; } res = SQLNumResultCols(stmt, &colcount); if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) { ast_log(LOG_WARNING, "SQL Column Count error!\n[%s]\n\n", sql); SQLFreeHandle (SQL_HANDLE_STMT, stmt); ast_odbc_release_obj(obj); return -1; } *buf = '\0'; res = SQLFetch(stmt); if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) { int res1 = -1; if (res == SQL_NO_DATA) { if (option_verbose > 3) { ast_verbose(VERBOSE_PREFIX_4 "Found no rows [%s]\n", sql); } res1 = 0; } else if (option_verbose > 3) { ast_log(LOG_WARNING, "Error %d in FETCH [%s]\n", res, sql); } SQLFreeHandle(SQL_HANDLE_STMT, stmt); ast_odbc_release_obj(obj); return res1; } for (x = 0; x < colcount; x++) { int i, namelen; char coldata[256], colname[256]; res = SQLDescribeCol(stmt, x + 1, (unsigned char *)colname, sizeof(colname), &collength, NULL, NULL, NULL, NULL); if (((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) || collength == 0) { snprintf(colname, sizeof(colname), "field%d", x); } if (!ast_strlen_zero(colnames)) strncat(colnames, ",", sizeof(colnames) - 1); namelen = strlen(colnames); /* Copy data, encoding '\' and ',' for the argument parser */ for (i = 0; i < sizeof(colname); i++) { if (escapecommas && (colname[i] == '\\' || colname[i] == ',')) { colnames[namelen++] = '\\'; } colnames[namelen++] = colname[i]; if (namelen >= sizeof(colnames) - 2) { colnames[namelen >= sizeof(colnames) ? sizeof(colnames) - 1 : namelen] = '\0'; break; } if (colname[i] == '\0') break; } buflen = strlen(buf); res = SQLGetData(stmt, x + 1, SQL_CHAR, coldata, sizeof(coldata), &indicator); if (indicator == SQL_NULL_DATA) { coldata[0] = '\0'; res = SQL_SUCCESS; } if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) { ast_log(LOG_WARNING, "SQL Get Data error!\n[%s]\n\n", sql); SQLFreeHandle(SQL_HANDLE_STMT, stmt); ast_odbc_release_obj(obj); return -1; } /* Copy data, encoding '\' and ',' for the argument parser */ for (i = 0; i < sizeof(coldata); i++) { if (escapecommas && (coldata[i] == '\\' || coldata[i] == ',')) { buf[buflen++] = '\\'; } buf[buflen++] = coldata[i]; if (buflen >= len - 2) break; if (coldata[i] == '\0') break; } buf[buflen - 1] = ','; buf[buflen] = '\0'; } /* Trim trailing comma */ buf[buflen - 1] = '\0'; pbx_builtin_setvar_helper(chan, "~ODBCFIELDS~", colnames); SQLFreeHandle(SQL_HANDLE_STMT, stmt); ast_odbc_release_obj(obj); return 0; } static int acf_escape(struct ast_channel *chan, const char *cmd, char *data, char *buf, size_t len) { char *out = buf; for (; *data && out - buf < len; data++) { if (*data == '\'') { *out = '\''; out++; } *out++ = *data; } *out = '\0'; return 0; } static struct ast_custom_function escape_function = { .name = "SQL_ESC", .synopsis = "Escapes single ticks for use in SQL statements", .syntax = "SQL_ESC()", .desc = "Used in SQL templates to escape data which may contain single ticks (') which\n" "are otherwise used to delimit data. For example:\n" "SELECT foo FROM bar WHERE baz='${SQL_ESC(${ARG1})}'\n", .read = acf_escape, .write = NULL, }; static int init_acf_query(struct ast_config *cfg, char *catg, struct acf_odbc_query **query) { const char *tmp; int i; if (!cfg || !catg) { return EINVAL; } *query = ast_calloc(1, sizeof(struct acf_odbc_query)); if (! (*query)) return ENOMEM; if (((tmp = ast_variable_retrieve(cfg, catg, "writehandle"))) || ((tmp = ast_variable_retrieve(cfg, catg, "dsn")))) { char *tmp2 = ast_strdupa(tmp); AST_DECLARE_APP_ARGS(write, AST_APP_ARG(dsn)[5]; ); AST_NONSTANDARD_APP_ARGS(write, tmp2, ','); for (i = 0; i < 5; i++) { if (!ast_strlen_zero(write.dsn[i])) ast_copy_string((*query)->writehandle[i], write.dsn[i], sizeof((*query)->writehandle[i])); } } if ((tmp = ast_variable_retrieve(cfg, catg, "readhandle"))) { char *tmp2 = ast_strdupa(tmp); AST_DECLARE_APP_ARGS(read, AST_APP_ARG(dsn)[5]; ); AST_NONSTANDARD_APP_ARGS(read, tmp2, ','); for (i = 0; i < 5; i++) { if (!ast_strlen_zero(read.dsn[i])) ast_copy_string((*query)->readhandle[i], read.dsn[i], sizeof((*query)->readhandle[i])); } } else { /* If no separate readhandle, then use the writehandle for reading */ for (i = 0; i < 5; i++) { if (!ast_strlen_zero((*query)->writehandle[i])) ast_copy_string((*query)->readhandle[i], (*query)->writehandle[i], sizeof((*query)->readhandle[i])); } } if ((tmp = ast_variable_retrieve(cfg, catg, "read"))) { ast_log(LOG_WARNING, "Parameter 'read' is deprecated for category %s. Please use 'readsql' instead.\n", catg); ast_copy_string((*query)->sql_read, tmp, sizeof((*query)->sql_read)); } else if ((tmp = ast_variable_retrieve(cfg, catg, "readsql"))) ast_copy_string((*query)->sql_read, tmp, sizeof((*query)->sql_read)); if (!ast_strlen_zero((*query)->sql_read) && ast_strlen_zero((*query)->readhandle[0])) { free(*query); *query = NULL; ast_log(LOG_ERROR, "There is SQL, but no ODBC class to be used for reading: %s\n", catg); return EINVAL; } if ((tmp = ast_variable_retrieve(cfg, catg, "write"))) { ast_log(LOG_WARNING, "Parameter 'write' is deprecated for category %s. Please use 'writesql' instead.\n", catg); ast_copy_string((*query)->sql_write, tmp, sizeof((*query)->sql_write)); } else if ((tmp = ast_variable_retrieve(cfg, catg, "writesql"))) ast_copy_string((*query)->sql_write, tmp, sizeof((*query)->sql_write)); if (!ast_strlen_zero((*query)->sql_write) && ast_strlen_zero((*query)->writehandle[0])) { free(*query); *query = NULL; ast_log(LOG_ERROR, "There is SQL, but no ODBC class to be used for writing: %s\n", catg); return EINVAL; } /* Allow escaping of embedded commas in fields to be turned off */ ast_set_flag((*query), OPT_ESCAPECOMMAS); if ((tmp = ast_variable_retrieve(cfg, catg, "escapecommas"))) { if (ast_false(tmp)) ast_clear_flag((*query), OPT_ESCAPECOMMAS); } (*query)->acf = ast_calloc(1, sizeof(struct ast_custom_function)); if (! (*query)->acf) { free(*query); *query = NULL; return ENOMEM; } if ((tmp = ast_variable_retrieve(cfg, catg, "prefix")) && !ast_strlen_zero(tmp)) { asprintf((char **)&((*query)->acf->name), "%s_%s", tmp, catg); } else { asprintf((char **)&((*query)->acf->name), "ODBC_%s", catg); } if (!((*query)->acf->name)) { free((*query)->acf); free(*query); *query = NULL; return ENOMEM; } asprintf((char **)&((*query)->acf->syntax), "%s([...[,]])", (*query)->acf->name); if (!((*query)->acf->syntax)) { free((char *)(*query)->acf->name); free((*query)->acf); free(*query); *query = NULL; return ENOMEM; } (*query)->acf->synopsis = "Runs the referenced query with the specified arguments"; if (!ast_strlen_zero((*query)->sql_read) && !ast_strlen_zero((*query)->sql_write)) { asprintf((char **)&((*query)->acf->desc), "Runs the following query, as defined in func_odbc.conf, performing\n" "substitution of the arguments into the query as specified by ${ARG1},\n" "${ARG2}, ... ${ARGn}. When setting the function, the values are provided\n" "either in whole as ${VALUE} or parsed as ${VAL1}, ${VAL2}, ... ${VALn}.\n" "\nRead:\n%s\n\nWrite:\n%s\n", (*query)->sql_read, (*query)->sql_write); } else if (!ast_strlen_zero((*query)->sql_read)) { asprintf((char **)&((*query)->acf->desc), "Runs the following query, as defined in func_odbc.conf, performing\n" "substitution of the arguments into the query as specified by ${ARG1},\n" "${ARG2}, ... ${ARGn}. This function may only be read, not set.\n\nSQL:\n%s\n", (*query)->sql_read); } else if (!ast_strlen_zero((*query)->sql_write)) { asprintf((char **)&((*query)->acf->desc), "Runs the following query, as defined in func_odbc.conf, performing\n" "substitution of the arguments into the query as specified by ${ARG1},\n" "${ARG2}, ... ${ARGn}. The values are provided either in whole as\n" "${VALUE} or parsed as ${VAL1}, ${VAL2}, ... ${VALn}.\n" "This function may only be set.\nSQL:\n%s\n", (*query)->sql_write); } else { free((char *)(*query)->acf->syntax); free((char *)(*query)->acf->name); free((*query)->acf); free(*query); ast_log(LOG_WARNING, "Section %s was found, but there was no SQL to execute. Ignoring.\n", catg); return EINVAL; } if (! ((*query)->acf->desc)) { free((char *)(*query)->acf->syntax); free((char *)(*query)->acf->name); free((*query)->acf); free(*query); *query = NULL; return ENOMEM; } if (ast_strlen_zero((*query)->sql_read)) { (*query)->acf->read = NULL; } else { (*query)->acf->read = acf_odbc_read; } if (ast_strlen_zero((*query)->sql_write)) { (*query)->acf->write = NULL; } else { (*query)->acf->write = acf_odbc_write; } return 0; } static int free_acf_query(struct acf_odbc_query *query) { if (query) { if (query->acf) { if (query->acf->name) free((char *)query->acf->name); if (query->acf->syntax) free((char *)query->acf->syntax); if (query->acf->desc) free((char *)query->acf->desc); free(query->acf); } free(query); } return 0; } static int load_module(void) { int res = 0; struct ast_config *cfg; char *catg; AST_LIST_LOCK(&queries); cfg = ast_config_load(config); if (!cfg) { ast_log(LOG_NOTICE, "Unable to load config for func_odbc: %s\n", config); AST_LIST_UNLOCK(&queries); return AST_MODULE_LOAD_DECLINE; } for (catg = ast_category_browse(cfg, NULL); catg; catg = ast_category_browse(cfg, catg)) { struct acf_odbc_query *query = NULL; int err; if ((err = init_acf_query(cfg, catg, &query))) { if (err == ENOMEM) ast_log(LOG_ERROR, "Out of memory\n"); else if (err == EINVAL) ast_log(LOG_ERROR, "Invalid parameters for category %s\n", catg); else ast_log(LOG_ERROR, "%s (%d)\n", strerror(err), err); } else { AST_LIST_INSERT_HEAD(&queries, query, list); ast_custom_function_register(query->acf); } } ast_config_destroy(cfg); res |= ast_custom_function_register(&escape_function); AST_LIST_UNLOCK(&queries); return res; } static int unload_module(void) { struct acf_odbc_query *query; int res = 0; AST_LIST_LOCK(&queries); while (!AST_LIST_EMPTY(&queries)) { query = AST_LIST_REMOVE_HEAD(&queries, list); ast_custom_function_unregister(query->acf); free_acf_query(query); } res |= ast_custom_function_unregister(&escape_function); /* Allow any threads waiting for this lock to pass (avoids a race) */ AST_LIST_UNLOCK(&queries); usleep(1); AST_LIST_LOCK(&queries); AST_LIST_UNLOCK(&queries); return 0; } static int reload(void) { int res = 0; struct ast_config *cfg; struct acf_odbc_query *oldquery; char *catg; AST_LIST_LOCK(&queries); while (!AST_LIST_EMPTY(&queries)) { oldquery = AST_LIST_REMOVE_HEAD(&queries, list); ast_custom_function_unregister(oldquery->acf); free_acf_query(oldquery); } cfg = ast_config_load(config); if (!cfg) { ast_log(LOG_WARNING, "Unable to load config for func_odbc: %s\n", config); goto reload_out; } for (catg = ast_category_browse(cfg, NULL); catg; catg = ast_category_browse(cfg, catg)) { struct acf_odbc_query *query = NULL; if (init_acf_query(cfg, catg, &query)) { ast_log(LOG_ERROR, "Cannot initialize query %s\n", catg); } else { AST_LIST_INSERT_HEAD(&queries, query, list); ast_custom_function_register(query->acf); } } ast_config_destroy(cfg); reload_out: AST_LIST_UNLOCK(&queries); return res; } /* XXX need to revise usecount - set if query_lock is set */ AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "ODBC lookups", .load = load_module, .unload = unload_module, .reload = reload, );