Files
asterisk/res/cdrel_custom/config.c
T
phoneben 7116b76c06 cdrel_custom: fix SQLite compatibility for versions < 3.20.0
cdrel_custom: fix SQLite compatibility for versions < 3.20.0

Replace sqlite3_prepare_v3 + SQLITE_PREPARE_PERSISTENT with a version-guarded fallback to sqlite3_prepare_v2 for older SQLite builds.

Resolves: #1885
2026-06-04 13:04:19 +00:00

1474 lines
43 KiB
C

/*
* Asterisk -- An open source telephony toolkit.
*
* Copyright (C) 2026, Sangoma Technologies Corporation
*
* George Joseph <gjoseph@sangoma.com>
*
* 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
* \author George Joseph <gjoseph@sangoma.com>
*
* \brief Common config file handling for res_cdrel_custom.
*
* This file is a 'bit' complex. The reasoning is that the functions
* do as much work as possible at module load time to reduce the workload
* at run time.
*
*/
#include "cdrel.h"
#include "asterisk/config.h"
#include "asterisk/module.h"
#include "asterisk/paths.h"
/*
* The DSV files get placed in specific subdirectories
* while the SQL databases get placed directly in /var/log/asterisk.
*/
static const char *dirname_map[cdrel_backend_type_end][cdrel_record_type_end] = {
[cdrel_backend_text] = {
[cdrel_record_cdr] = "cdr-custom",
[cdrel_record_cel] = "cel-custom",
},
[cdrel_backend_db] = {
[cdrel_record_cdr] = NULL,
[cdrel_record_cel] = NULL,
}
};
struct field_parse_result {
char *result;
int functions;
int csv_quote;
int cdr;
int is_literal;
int unknown_functions;
int parse_failed;
};
/*
* To maximize the possibility that we can put a legacy config through the
* much faster advanced process, we need to ensure that we can handle
* everything in the legacy config.
*/
static const char *allowed_functions[] = {
[cdrel_record_cdr] = "CSV_QUOTE CDR CALLERID CHANNEL",
[cdrel_record_cel] = "CSV_QUOTE CALLERID CHANNEL eventtype eventtime eventenum userdeftype eventextra BRIDGEPEER",
};
static const char *special_vars[] = {
[cdrel_record_cdr] = "",
[cdrel_record_cel] = "eventtype eventtime eventenum userdeftype eventextra BRIDGEPEER",
};
/*!
* \internal
* \brief Parse a raw legacy field template.
*
* \example
*
* ${CSV_QUOTE(${eventtype})}
* ${CSV_QUOTE(${CALLERID(name)})}
* ${CSV_QUOTE(${CDR(src)})}
* ${CDR(uservar)}
* "some literal"
* ${CSV_QUOTE("some literal")}
*
* \param record_type CDR or CEL
* \param input_field_template The trimmed raw field template
* \return
*/
static struct field_parse_result parse_field(enum cdrel_record_type record_type, char *input_field_template)
{
char *tmp_field = NULL;
struct field_parse_result result = { 0, };
/*
* If the template starts with a double-quote, it's automatically
* a literal.
*/
if (input_field_template[0] == '"') {
result.result = ast_strdup(ast_strip_quoted(ast_strdupa(input_field_template), "\"", "\""));
result.csv_quote = 1;
result.is_literal = 1;
return result;
}
/*
* If it starts with a single quote, it's probably a legacy SQL template
* so we need to force quote it on output.
*/
tmp_field = ast_strip(ast_strdupa(input_field_template));
if (tmp_field[0] == '\'') {
result.csv_quote = 1;
}
/*
* I really hate the fact that ast_strip really trims whitespace
* and ast_strip_quoted will strip anything in pairs.
* Anyway, get rid of any remaining enclosing quotes.
*/
tmp_field = ast_strip(ast_strip_quoted(tmp_field, "\"'", "\"'"));
/*
* If the template now starts with a '$' it's either a dialplan function
* call or one of the special CEL field names.
*
* Examples: ${CSV_QUOTE(${CALLERID(name)})}
* ${eventtime}
* We're going to iterate over function removal until there's just
* a plain text string left.
*
*/
while (tmp_field[0] == '$') {
char *ptr = NULL;
/*
* A function name longer that 64 characters is highly unlikely but
* we'll check later.
*/
char func_name[65];
/*
* Skip over the '$'
* {CSV_QUOTE(${CALLERID(name)})}
* {eventtime}
*/
tmp_field++;
/*
* Remove any enclosing brace-like characters
* CSV_QUOTE(${CALLERID(name)})
* eventtime
*/
tmp_field = ast_strip(ast_strip_quoted(tmp_field, "[{(", "]})"));
/*
* Check what's left to see if it matches a special variable.
* If it does (like "eventtime" in the example), we're done.
*/
if (strstr(special_vars[record_type], tmp_field) != NULL) {
result.functions++;
break;
}
/*
* At this point, it has to be a function name so find the
* openening '('.
* CSV_QUOTE(${CALLERID(name)})
* ^
* If we don't find one, it's something we don't recognise
* so bail.
*/
ptr = strchr(tmp_field, '(');
if (!ptr) {
result.parse_failed++;
continue;
}
/*
* Copy from the beginning to the '(' to func_name,
* not exceeding func_name's size.
*
* CSV_QUOTE(${CALLERID(name)})
* ^
* CSV_QUOTE
*
* Then check that it's a function we can handle.
* If not, bail.
*/
ast_copy_string(func_name, tmp_field, MIN(sizeof(func_name), ptr - tmp_field + 1));
if (strstr(allowed_functions[record_type], func_name) == NULL) {
result.parse_failed++;
result.unknown_functions++;
continue;
}
result.functions++;
/*
* If the function is CSV_QUOTE, we need to set the csv_quote flag.
*/
if (strcmp("CSV_QUOTE", func_name) == 0) {
result.csv_quote = 1;
} else if (strcmp("CDR", func_name) == 0) {
result.cdr = 1;
}
/*
* ptr still points to the opening '(' so now strip it and the
* matching parens.
*
* ${CALLERID(name)}
*
*/
tmp_field = ast_strip_quoted(ptr, "(", ")");
if (tmp_field[0] == '"' || tmp_field[0] == '\'') {
result.result = ast_strdup(ast_strip_quoted(tmp_field, "\"'", "\"'"));
result.csv_quote = 1;
result.is_literal = 1;
return result;
}
/* Repeat the loop until there are no more functions or variables */
}
/*
* If the parse failed we'll send back the entire template.
*/
if (result.parse_failed) {
tmp_field = input_field_template;
} else {
/*
* If there were no functions or variables parsed then we'll
* assume it's a literal.
*/
if (result.functions == 0) {
result.is_literal = 1;
}
}
result.result = ast_strdup(tmp_field);
if (result.result == NULL) {
result.parse_failed = 1;
}
return result;
}
/*!
* \internal
* \brief Parse a legacy DSV template string into a vector of individual strings.
*
* The resulting vector will look like it came from an advanced config and will
* be treated as such.
*
* \param record_type CDR or CEL.
* \param config_filename Config filename for logging purposes.
* \param output_filename Output filename for logging purposes.
* \param template The full template.
* \param fields A pointer to a string vector to receive the result.
* \retval 1 on success.
* \retval 0 on failure.
*/
static int parse_legacy_template(enum cdrel_record_type record_type, const char *config_filename,
const char *output_filename, const char *input_template, struct ast_vector_string *fields)
{
char *template = ast_strdupa(input_template);
char *field_template = NULL;
int res = 0;
/*
* We have no choice but to assume that a legacy config template uses commas
* as field delimiters. We don't have a reliable way to determine this ourselves.
*/
while((field_template = ast_strsep(&template, ',', AST_STRSEP_TRIM))) {
char *uservar = "";
char *literal = "";
/* Try to parse the field. */
struct field_parse_result result = parse_field(record_type, field_template);
ast_debug(2, "field: '%s' literal: %d quote: %d cdr: %d failed: %d funcs: %d unknfuncs: %d\n", result.result,
result.is_literal, result.csv_quote, result.cdr,
result.parse_failed, result.functions, result.unknown_functions);
/*
* If it failed,
*/
if (!result.result || result.parse_failed) {
ast_free(result.result);
return 0;
}
if (result.is_literal) {
literal = "literal^";
}
if (!get_registered_field_by_name(record_type, result.result)) {
ast_debug(3, " %s->%s: field '%s' not found\n", cdrel_basename(config_filename),
cdrel_basename(output_filename), result.result);
/*
* If the result was found in a CDR function, treat it as a CDR user variable
* otherwise treat it as a literal.
*/
if (result.cdr) {
uservar = "uservar^";
} else {
literal = "literal^";
}
}
res = ast_asprintf(&field_template, "%s(%s%s)", result.result, S_OR(literal,uservar), result.csv_quote ? "quote" : "noquote");
ast_free(result.result);
if (!field_template || res < 0) {
ast_free(field_template);
return 0;
}
res = AST_VECTOR_APPEND(fields, field_template);
if (res != 0) {
ast_free(field_template);
return 0;
}
ast_debug(2, " field template: %s\n", field_template);
}
return 1;
}
/*!
* \fn Parse an advanced field template and allocate a cdrel_field for it.
* \brief
*
* \param config Config object.
* \param input_field_template Trimmed advanced field template.
* \return
*/
static struct cdrel_field *field_alloc(struct cdrel_config *config, const char *input_field_template)
{
RAII_VAR(struct cdrel_field *, field, NULL, ast_free);
const struct cdrel_field *registered_field = NULL;
struct cdrel_field *rtn_field = NULL;
char *field_name = NULL;
char *data = NULL;
char *tmp_data = NULL;
char *closeparen = NULL;
char *qualifier = NULL;
enum cdrel_data_type forced_output_data_type = cdrel_data_type_end;
struct ast_flags field_flags = { 0 };
/*
* The database fields are specified field-by-field for legacy so we treat them
* as literals containing expressions which will be evaluated record-by-record.
*/
if (config->backend_type == cdrel_backend_db && config->config_type == cdrel_config_legacy) {
registered_field = get_registered_field_by_name(config->record_type, "literal");
ast_assert(registered_field != NULL);
rtn_field = ast_calloc(1, sizeof(*field) + strlen(input_field_template) + 1);
if (!rtn_field) {
return NULL;
}
memcpy(rtn_field, registered_field, sizeof(*registered_field));
strcpy(rtn_field->data, input_field_template); /* Safe */
return rtn_field;
}
/*
* If the field template is a quoted string, it's a literal.
* We don't check for qualifiers.
*/
if (input_field_template[0] == '"' || input_field_template[0] == '\'') {
data = ast_strip_quoted(ast_strdupa(input_field_template), "\"'", "\"'");
ast_set_flag(&field_flags, cdrel_flag_literal);
ast_debug(3, " Using qualifier 'literal' for field '%s' flags: %s\n", data,
ast_str_tmp(128, cdrel_get_field_flags(&field_flags, &STR_TMP)));
field_name = "literal";
} else {
field_name = ast_strdupa(input_field_template);
data = strchr(field_name, '(');
if (data) {
*data = '\0';
data++;
closeparen = strchr(data, ')');
if (closeparen) {
*closeparen = '\0';
}
}
}
if (!ast_strlen_zero(data) && !ast_test_flag(&field_flags, cdrel_flag_literal)) {
char *data_swap = NULL;
tmp_data = ast_strdupa(data);
while((qualifier = ast_strsep(&tmp_data, '^', AST_STRSEP_STRIP | AST_STRSEP_TRIM))) {
enum cdrel_data_type fodt = cdrel_data_type_end;
if (ast_strlen_zero(qualifier)) {
continue;
}
fodt = cdrel_data_type_from_str(qualifier);
if (fodt < cdrel_data_type_end) {
ast_set_flag(&field_flags, cdrel_flag_type_forced);
if (fodt == cdrel_type_uservar) {
ast_set_flag(&field_flags, cdrel_flag_uservar);
ast_debug(3, " Using qualifier '%s' for field '%s' flags: %s\n", qualifier,
field_name, ast_str_tmp(128, cdrel_get_field_flags(&field_flags, &STR_TMP)));
data_swap = ast_strdupa(field_name);
field_name = "uservar";
} else if (fodt == cdrel_type_literal) {
ast_set_flag(&field_flags, cdrel_flag_literal);
ast_debug(3, " Using qualifier '%s' for field '%s' flags: %s\n", qualifier,
field_name, ast_str_tmp(128, cdrel_get_field_flags(&field_flags, &STR_TMP)));
data_swap = ast_strdupa(field_name);
field_name = "literal";
} else {
forced_output_data_type = fodt;
ast_debug(3, " Using qualifier '%s' for field '%s' flags: %s\n", qualifier,
field_name, ast_str_tmp(128, cdrel_get_field_flags(&field_flags, &STR_TMP)));
}
continue;
}
if (strcasecmp(qualifier, "quote") == 0) {
ast_set_flag(&field_flags, cdrel_flag_quote);
ast_debug(3, " Using qualifier '%s' for field '%s' flags: %s\n", qualifier,
field_name, ast_str_tmp(128, cdrel_get_field_flags(&field_flags, &STR_TMP)));
continue;
}
if (strcasecmp(qualifier, "noquote") == 0) {
ast_set_flag(&field_flags, cdrel_flag_noquote);
ast_debug(3, " Using qualifier '%s' for field '%s' flags: %s\n", qualifier,
field_name, ast_str_tmp(128, cdrel_get_field_flags(&field_flags, &STR_TMP)));
continue;
}
if (strchr(qualifier, '%') != NULL) {
data_swap = ast_strdupa(qualifier);
ast_set_flag(&field_flags, cdrel_flag_format_spec);
forced_output_data_type = cdrel_type_string;
ast_debug(3, " Using qualifier '%s' for field '%s' flags: %s\n", qualifier,
field_name, ast_str_tmp(128, cdrel_get_field_flags(&field_flags, &STR_TMP)));
}
}
if (ast_test_flag(&field_flags, cdrel_flag_quote) && ast_test_flag(&field_flags, cdrel_flag_noquote)) {
ast_log(LOG_WARNING, "%s->%s: Field '%s(%s)' has both quote and noquote\n",
config->config_filename, config->output_filename, field_name, data);
return NULL;
}
data = data_swap;
}
/*
* Check again for literal.
*/
if (ast_test_flag(&field_flags, cdrel_flag_literal)) {
if (config->format_type == cdrel_format_json && !strchr(data, ':')) {
ast_log(LOG_WARNING, "%s->%s: Literal field '%s' must be formatted as \"name: value\" when using the 'json' format\n",
cdrel_basename(config->config_filename), cdrel_basename(config->output_filename),
input_field_template);
return NULL;
}
}
/*
* Now look the field up by just the field name without any data.
*/
registered_field = get_registered_field_by_name(config->record_type, field_name);
if (!registered_field) {
ast_log(LOG_WARNING, "%s->%s: Field '%s' not found\n",
cdrel_basename(config->config_filename), cdrel_basename(config->output_filename), field_name);
return NULL;
}
if (ast_test_flag(&field_flags, cdrel_flag_format_spec)
&& registered_field->input_data_type != cdrel_type_timeval) {
ast_log(LOG_WARNING, "%s->%s: Custom format '%s' ignored for field '%s'."
" Only timeval types can use custom format strings.\n",
cdrel_basename(config->config_filename), cdrel_basename(config->output_filename),
data, field_name);
forced_output_data_type = cdrel_data_type_end;
ast_clear_flag(&field_flags, cdrel_flag_format_spec);
data = NULL;
}
field = ast_calloc(1, sizeof(*registered_field) + strlen(input_field_template) + 1);
if (!field) {
return NULL;
}
memcpy(field, registered_field, sizeof(*field));
if (!ast_strlen_zero(data)) {
strcpy(field->data, data); /* Safe */
}
/*
* For user variables, we use the field name from the data
* we set above.
*/
if (field->input_data_type == cdrel_type_uservar) {
field->name = field->data;
}
if (field->input_data_type == cdrel_type_literal && config->format_type == cdrel_format_json) {
/*
* data should look something like this... lname: lvalue
* We'll need to make field->name point to "lname" and
* field->data point to "lvalue" so that when output the
* json will look like... "lname": "lvalue".
* Since field->data is already long enough to to handle both,
* we'll do this...
* field->data = lvalue\0lname\0
* field->name = ^
*/
char *ptr = strchr(data, ':');/* Safe since we checked data for a ':' above */
*ptr = '\0';
ptr++;
/*
* data: lname\0 lvalue
* ptr: ^
*/
strcpy(field->data, ast_strip_quoted(ptr, "\"", "\"")); /* Safe */
/*
* field->data: lvalue\0
*/
ptr = field->data + strlen(field->data);
ptr++;
/*
* field->data: lvalue\0
* ptr: ^
* data: lname\0 lvalue
*/
strcpy(ptr, data); /* Safe */
/*
* field->data: lvalue\0lname\0
*/
field->name = ptr;
/*
* field->data: lvalue\0lname\0
* field->name: ^
*/
}
if (forced_output_data_type < cdrel_data_type_end) {
field->output_data_type = forced_output_data_type;
}
field->flags = field_flags;
/*
* Unless the field has the 'noquote' flag, we'll set the 'quote'
* flag if quoting method is 'all' or 'non_numeric'.
*/
if (!ast_test_flag(&field->flags, cdrel_flag_noquote)) {
if (config->quoting_method == cdrel_quoting_method_all) {
ast_set_flag(&field->flags, cdrel_flag_quote);
} else if (config->quoting_method == cdrel_quoting_method_non_numeric) {
if (field->output_data_type > cdrel_data_type_strings_end) {
ast_set_flag(&field->flags, cdrel_flag_noquote);
} else {
ast_set_flag(&field->flags, cdrel_flag_quote);
}
}
}
if (config->quoting_method == cdrel_quoting_method_none) {
ast_clear_flag(&field->flags, cdrel_flag_quote);
ast_set_flag(&field->flags, cdrel_flag_noquote);
}
ast_debug(2, "%s->%s: Field '%s' processed -> name:'%s' input_type:%s output_type:%s flags:'%s' data:'%s'\n",
cdrel_basename(config->config_filename), cdrel_basename(config->output_filename), input_field_template,
field->name, DATA_TYPE_STR(field->input_data_type),
DATA_TYPE_STR(field->output_data_type),
ast_str_tmp(128, cdrel_get_field_flags(&field->flags, &STR_TMP)),
field->data);
rtn_field = field;
field = NULL;
return rtn_field;
}
static void field_template_vector_free(struct ast_vector_string *fields) {
AST_VECTOR_RESET(fields, ast_free);
AST_VECTOR_PTR_FREE(fields);
}
/*!
* \internal
* \brief Load all the fields in the string vector.
*
* \param config Config object
* \param fields String vector.
* \retval 0 on success.
* \retval -1 on failure.
*/
static int load_fields(struct cdrel_config *config, struct ast_vector_string *fields)
{
int res = 0;
int ix = 0;
ast_debug(1, "%s->%s: Loading fields\n", cdrel_basename(config->config_filename),
cdrel_basename(config->output_filename));
for (ix = 0; ix < AST_VECTOR_SIZE(fields); ix++) {
char *field_name = AST_VECTOR_GET(fields, ix);
struct cdrel_field *field = NULL;
field = field_alloc(config, field_name);
if (!field) {
res = -1;
continue;
}
if (AST_VECTOR_APPEND(&config->fields, field) != 0) {
ast_free(field);
return -1;
}
}
return res;
}
static void config_free(struct cdrel_config *config)
{
if (!config) {
return;
}
if (config->insert) {
sqlite3_finalize(config->insert);
config->insert = NULL;
}
if (config->db) {
sqlite3_close(config->db);
config->db = NULL;
}
ast_mutex_destroy(&config->lock);
ast_string_field_free_memory(config);
AST_VECTOR_RESET(&config->fields, ast_free);
AST_VECTOR_FREE(&config->fields);
ast_free(config);
}
/*!
* \internal
* \brief Allocate a config object.
*
* You should know what these are by now :)
*
* \param record_type
* \param backend_type
* \param config_type
* \param config_filename
* \param output_filename
* \param template
* \return
*/
static struct cdrel_config *config_alloc(enum cdrel_record_type record_type,
enum cdrel_backend_type backend_type, enum cdrel_config_type config_type,
const char *config_filename, const char *output_filename, const char *template)
{
RAII_VAR(struct cdrel_config *, config, NULL, config_free);
struct cdrel_config *rtn_config = NULL;
const char *file_suffix = "";
int res = 0;
ast_debug(1, "%s->%s: Loading\n", cdrel_basename(config_filename), cdrel_basename(output_filename));
config = ast_calloc_with_stringfields(1, struct cdrel_config, 1024);
if (!config) {
return NULL;
}
if (ast_string_field_set(config, config_filename, config_filename) != 0) {
return NULL;
}
config->record_type = record_type;
config->backend_type = backend_type;
config->dummy_channel_alloc = cdrel_dummy_channel_allocators[record_type];
config->config_type = config_type;
/* Set defaults */
config->format_type = cdrel_format_dsv;
config->separator[0] = ',';
switch(backend_type) {
case cdrel_backend_text:
config->quote[0] = '"';
config->quoting_method = cdrel_quoting_method_all;
break;
case cdrel_backend_db:
config->quote[0] = '\0';
config->format_type = cdrel_format_sql;
config->quoting_method = cdrel_quoting_method_none;
if (!ast_ends_with(output_filename, ".db")) {
file_suffix = ".db";
}
break;
default:
ast_log(LOG_ERROR, "%s->%s: Unknown backend type '%d'\n", cdrel_basename(config_filename),
cdrel_basename(output_filename), backend_type);
break;
}
config->quote_escape[0] = config->quote[0];
res = ast_string_field_set(config, template, template);
if (res != 0) {
return NULL;
}
if (output_filename[0] == '/') {
res = ast_string_field_build(config, output_filename, "%s%s", output_filename, file_suffix);
} else {
const char *subdir = dirname_map[backend_type][record_type];
res = ast_string_field_build(config, output_filename, "%s/%s%s%s%s",
ast_config_AST_LOG_DIR, S_OR(subdir, ""), ast_strlen_zero(subdir) ? "" : "/", output_filename, file_suffix);
}
if (res != 0) {
return NULL;
}
ast_mutex_init(&config->lock);
rtn_config = config;
config = NULL;
return rtn_config;
}
/*!
* \internal
* \brief Load the "columns" parameter from a database config.
*
* \param config Config object
* \param columns The columns parameter.
* \param column_count Set to the count of columns parsed.
* \retval 0 on success.
* \retval -1 on failure.
*/
static int load_database_columns(struct cdrel_config *config, const char *columns, int *column_count)
{
char *col = NULL;
char *cols = NULL;
RAII_VAR(struct ast_str *, column_string, NULL, ast_free);
ast_debug(1, "%s->%s: Loading columns\n", cdrel_basename(config->config_filename),
cdrel_basename(config->output_filename));
if (!(column_string = ast_str_create(1024))) {
return -1;
}
cols = ast_strdupa(columns);
*column_count = 0;
/* We need to trim and remove any single or double quotes from each column name. */
while((col = ast_strsep(&cols, ',', AST_STRSEP_TRIM))) {
col = ast_strip(ast_strip_quoted(col, "'\"", "'\""));
if (ast_str_append(&column_string, 0, "%s%s", *column_count ? "," : "", col) <= 0) {
return -1;
}
(*column_count)++;
}
if (ast_string_field_set(config, db_columns, ast_str_buffer(column_string)) != 0) {
return -1;
}
return 0;
}
static char *make_stmt_placeholders(int columns)
{
char *placeholders = ast_malloc(2 * columns), *c = placeholders;
if (placeholders) {
for (; columns; columns--) {
*c++ = '?';
*c++ = ',';
}
*(c - 1) = 0;
}
return placeholders;
}
/*!
* \internal
* \brief Open an sqlite3 database and create the table if needed.
*
* \param config Config object.
* \retval 0 on success.
* \retval -1 on failure.
*/
static int open_database(struct cdrel_config *config)
{
char *sql = NULL;
int res = 0;
char *placeholders = NULL;
ast_debug(1, "%s->%s: opening database\n", cdrel_basename(config->config_filename),
cdrel_basename(config->output_filename));
res = sqlite3_open(config->output_filename, &config->db);
if (res != SQLITE_OK) {
ast_log(LOG_WARNING, "%s->%s: Could not open database\n", cdrel_basename(config->config_filename),
cdrel_basename(config->output_filename));
return -1;
}
sqlite3_busy_timeout(config->db, config->busy_timeout);
/* is the table there? */
sql = sqlite3_mprintf("SELECT COUNT(*) FROM %q;", config->db_table);
if (!sql) {
return -1;
}
res = sqlite3_exec(config->db, sql, NULL, NULL, NULL);
sqlite3_free(sql);
if (res != SQLITE_OK) {
/*
* Create the table.
* We don't use %q for the column list here since we already escaped when building it
*/
sql = sqlite3_mprintf("CREATE TABLE %q (AcctId INTEGER PRIMARY KEY, %s)",
config->db_table, config->db_columns);
res = sqlite3_exec(config->db, sql, NULL, NULL, NULL);
sqlite3_free(sql);
if (res != SQLITE_OK) {
ast_log(LOG_WARNING, "%s->%s: Unable to create table '%s': %s\n",
cdrel_basename(config->config_filename), cdrel_basename(config->output_filename),
config->db_table, sqlite3_errmsg(config->db));
return -1;
}
} else {
/*
* If the table exists, make sure the number of columns
* matches the config.
*/
sqlite3_stmt *get_stmt;
int existing_columns = 0;
int config_columns = AST_VECTOR_SIZE(&config->fields);
sql = sqlite3_mprintf("SELECT * FROM %q;", config->db_table);
if (!sql) {
return -1;
}
res = sqlite3_prepare_v2(config->db, sql, -1, &get_stmt, NULL);
sqlite3_free(sql);
if (res != SQLITE_OK) {
ast_log(LOG_WARNING, "%s->%s: Unable to get column count for table '%s': %s\n",
cdrel_basename(config->config_filename), cdrel_basename(config->output_filename),
config->db_table, sqlite3_errmsg(config->db));
return -1;
}
/*
* prepare figures out the number of columns that would be in a result
* set. We don't need to execute the statement.
*/
existing_columns = sqlite3_column_count(get_stmt);
sqlite3_finalize(get_stmt);
/* config_columns doesn't include the sequence field */
if ((config_columns + 1) != existing_columns) {
ast_log(LOG_WARNING, "%s->%s: The number of fields in the config (%d) doesn't equal the"
" nummber of data columns (%d) in the existing %s table. This config is disabled.\n",
cdrel_basename(config->config_filename), cdrel_basename(config->output_filename),
config_columns, existing_columns - 1, config->db_table);
return -1;
}
}
placeholders = make_stmt_placeholders(AST_VECTOR_SIZE(&config->fields));
if (!placeholders) {
return -1;
}
/* Inserting NULL in the ID column still generates an ID */
sql = sqlite3_mprintf("INSERT INTO %q VALUES (NULL,%s)", config->db_table, placeholders);
ast_free(placeholders);
if (!sql) {
return -1;
}
#if SQLITE_VERSION_NUMBER >= 3020000
res = sqlite3_prepare_v3(config->db, sql, -1, SQLITE_PREPARE_PERSISTENT, &config->insert, NULL);
#else
res = sqlite3_prepare_v2(config->db, sql, -1, &config->insert, NULL);
#endif
if (res != SQLITE_OK) {
ast_log(LOG_ERROR, "%s->%s: Unable to prepare INSERT statement '%s': %s\n",
cdrel_basename(config->config_filename), cdrel_basename(config->output_filename),
sql, sqlite3_errmsg(config->db));
return -1;
}
return 0;
}
/*!
* \internal
* \brief Load a database config from a config file category.
*
* \param record_type CDR or CEL.
* \param category The category (becomes the database file name).
* \param config_filename The config filename for logging purposes.
* \return config or NULL.
*/
static struct cdrel_config *load_database_config(enum cdrel_record_type record_type,
struct ast_category *category, const char *config_filename)
{
const char *category_name = ast_category_get_name(category);
RAII_VAR(struct cdrel_config *, config, NULL, config_free);
struct cdrel_config *rtn_config = NULL;
int res = 0;
int column_count = 0;
int value_count = 0;
int field_check_passed = 0;
const char *template = ast_variable_find(category, "values");
enum cdrel_config_type config_type;
const char *value;
char *tmp_fields = NULL;
RAII_VAR(struct ast_vector_string *, field_templates, ast_calloc(1, sizeof(*field_templates)), field_template_vector_free);
if (!ast_strlen_zero(template)) {
config_type = cdrel_config_legacy;
} else {
template = ast_variable_find(category, "fields");
if (!ast_strlen_zero(template)) {
config_type = cdrel_config_advanced;
}
}
if (ast_strlen_zero(template)) {
ast_log(LOG_WARNING, "%s->%s: Neither 'values' nor 'fields' specified\n",
cdrel_basename(config_filename), cdrel_basename(category_name));
return NULL;
}
res = AST_VECTOR_INIT(field_templates, 25);
if (res != 0) {
return NULL;
}
/*
* Let's try and and parse a legacy config to see if we can turn
* it into an advanced condig.
*/
if (config_type == cdrel_config_legacy) {
field_check_passed = parse_legacy_template(record_type, config_filename,
category_name, template, field_templates);
if (field_check_passed) {
config_type = cdrel_config_advanced;
ast_log(LOG_NOTICE, "%s->%s: Legacy config upgraded to advanced\n",
cdrel_basename(config_filename), cdrel_basename(category_name));
} else {
AST_VECTOR_RESET(field_templates, ast_free);
ast_log(LOG_NOTICE, "%s->%s: Unable to upgrade legacy config to advanced. Continuing to process as legacy\n",
cdrel_basename(config_filename), cdrel_basename(category_name));
}
}
/*
* If we could, the fields vector will be populated so we don't need to do it again.
* If it was an advanced config or a legacy one we couldn't parse,
* we need to split the string into the vector.
*/
if (AST_VECTOR_SIZE(field_templates) == 0) {
tmp_fields = ast_strdupa(template);
while((value = ast_strsep(&tmp_fields, ',', AST_STRSEP_TRIM))) {
res = AST_VECTOR_APPEND(field_templates, ast_strdup(value));
if (res != 0) {
return NULL;
}
}
}
config = config_alloc(record_type, cdrel_backend_db, config_type,
config_filename, category_name, template);
if (!config) {
return NULL;
}
config->busy_timeout = 1000;
res = ast_string_field_set(config, db_table,
S_OR(ast_variable_find(category, "table"), config->record_type == cdrel_record_cdr ? "cdr" : "cel"));
if (res != 0) {
return NULL;
}
/* sqlite3_busy_timeout in miliseconds */
if ((value = ast_variable_find(category, "busy_timeout")) != NULL) {
if (ast_parse_arg(value, PARSE_INT32|PARSE_DEFAULT, &config->busy_timeout, 1000) != 0) {
ast_log(LOG_WARNING, "%s->%s: Invalid busy_timeout value '%s' specified. Using 1000 instead.\n",
cdrel_basename(config->config_filename), cdrel_basename(config->output_filename), value);
}
}
/* Columns */
value = ast_variable_find(category, "columns");
if (ast_strlen_zero(value)) {
ast_log(LOG_WARNING, "%s->%s: The 'columns' parameter is missing",
cdrel_basename(config->config_filename), cdrel_basename(config->output_filename));
return NULL;
}
if (load_database_columns(config, value, &column_count) != 0) {
return NULL;
}
if (AST_VECTOR_INIT(&config->fields, AST_VECTOR_SIZE(field_templates)) != 0) {
return NULL;
}
if (load_fields(config, field_templates) != 0) {
return NULL;
}
value_count = AST_VECTOR_SIZE(&config->fields);
if (value_count != column_count) {
ast_log(LOG_WARNING, "%s->%s: There are %d columns but %d values\n",
cdrel_basename(config->config_filename), cdrel_basename(config->output_filename),
column_count, value_count);
return NULL;
}
res = open_database(config);
if (res != 0) {
return NULL;
}
ast_log(LOG_NOTICE, "%s->%s: Logging %s records to table '%s'\n",
cdrel_basename(config->config_filename), cdrel_basename(config->output_filename),
RECORD_TYPE_STR(config->record_type),
config->db_table);
rtn_config = config;
config = NULL;
return rtn_config;
}
/*!
* \internal
* \brief Load all the categories in a database config file.
*
* \param record_type
* \param configs
* \param config_filename
* \param reload
* \retval 0 on success or reload not needed.
* \retval -1 on failure.
*/
static int load_database_config_file(enum cdrel_record_type record_type, struct cdrel_configs *configs,
const char *config_filename, int reload)
{
struct ast_config *cfg;
struct ast_flags config_flags = { reload ? CONFIG_FLAG_FILEUNCHANGED : 0 };
struct ast_category *category = NULL;
ast_debug(1, "%s: %soading\n", config_filename, reload ? "Rel" : "L");
cfg = ast_config_load(config_filename, config_flags);
if (!cfg || cfg == CONFIG_STATUS_FILEINVALID) {
ast_log(LOG_ERROR, "Unable to load %s. Not logging %ss to custom database\n",
config_filename, RECORD_TYPE_STR(record_type));
return -1;
} else if (cfg == CONFIG_STATUS_FILEUNCHANGED) {
ast_debug(1, "%s: Config file unchanged, not reloading\n", config_filename);
return 1;
}
while ((category = ast_category_browse_filtered(cfg, NULL, category, NULL))) {
struct cdrel_config *config = NULL;
config = load_database_config(record_type, category, config_filename);
if (!config) {
continue;
}
if (AST_VECTOR_APPEND(configs, config) != 0) {
config_free(config);
break;
}
}
ast_config_destroy(cfg);
ast_log(LOG_NOTICE, "%s: Loaded %d configs\n", config_filename, (int)AST_VECTOR_SIZE(configs));
/* Only fail if no configs were valid. */
return AST_VECTOR_SIZE(configs) > 0 ? 0 : -1;
}
/*!
* \internal
* \brief Load a legacy config from a single entry in the "mappings" castegory.
*
* \param record_type
* \param config_filename
* \param output_filename
* \param template
* \return config or NULL.
*/
static struct cdrel_config *load_text_file_legacy_config(enum cdrel_record_type record_type,
const char *config_filename, const char *output_filename, const char *template)
{
struct cdrel_config *config = NULL;
int field_check_passed = 0;
int res = 0;
RAII_VAR(struct ast_vector_string *, fields, ast_calloc(1, sizeof(*fields)), field_template_vector_free);
res = AST_VECTOR_INIT(fields, 25);
if (res != 0) {
return NULL;
}
/*
* Let's try and and parse a legacy config to see if we can turn
* it into an advanced condig.
*/
field_check_passed = parse_legacy_template(record_type, config_filename,
output_filename, template, fields);
/*
* If we couldn't, treat as legacy.
*/
if (!field_check_passed) {
config = config_alloc(record_type, cdrel_backend_text, cdrel_config_legacy,
config_filename, output_filename, template);
ast_log(LOG_NOTICE, "%s->%s: Logging legacy %s records\n",
cdrel_basename(config->config_filename), cdrel_basename(config->output_filename),
RECORD_TYPE_STR(config->record_type));
return config;
}
config = config_alloc(record_type, cdrel_backend_text, cdrel_config_advanced,
config_filename, output_filename, template);
if (!config) {
return NULL;
}
config->format_type = cdrel_format_dsv;
config->quote[0] = '"';
config->quote_escape[0] = '"';
config->separator[0] = ',';
config->quoting_method = cdrel_quoting_method_all;
if (AST_VECTOR_INIT(&config->fields, AST_VECTOR_SIZE(fields)) != 0) {
return NULL;
}
if (load_fields(config, fields) != 0) {
return NULL;
}
ast_log(LOG_NOTICE, "%s->%s: Logging legacy %s records as advanced\n",
cdrel_basename(config->config_filename), cdrel_basename(config->output_filename),
RECORD_TYPE_STR(config->record_type));
return config;
}
/*!
* \internal
* \brief Load an advanced config from a config file category.
*
* \param record_type
* \param category
* \param config_filename
* \return config or NULL.
*/
static struct cdrel_config *load_text_file_advanced_config(enum cdrel_record_type record_type,
struct ast_category *category, const char *config_filename)
{
const char *category_name = ast_category_get_name(category);
RAII_VAR(struct cdrel_config *, config, NULL, config_free);
struct cdrel_config *rtn_config = NULL;
const char *value;
int res = 0;
const char *fields_value = ast_variable_find(category, "fields");
char *tmp_fields = NULL;
RAII_VAR(struct ast_vector_string *, fields, ast_calloc(1, sizeof(*fields)), field_template_vector_free);
if (ast_strlen_zero(fields_value)) {
ast_log(LOG_WARNING, "%s->%s: Missing 'fields' parameter\n",
cdrel_basename(config_filename), category_name);
return NULL;
}
config = config_alloc(record_type, cdrel_backend_text, cdrel_config_advanced,
config_filename, category_name, fields_value);
value = ast_variable_find(category, "format");
if (!ast_strlen_zero(value)) {
if (ast_strings_equal(value, "json")) {
config->format_type = cdrel_format_json;
config->separator[0] = ',';
config->quote[0] = '"';
config->quote_escape[0] = '\\';
config->quoting_method = cdrel_quoting_method_non_numeric;
} else if (ast_strings_equal(value, "dsv")) {
config->format_type = cdrel_format_dsv;
} else {
ast_log(LOG_WARNING, "%s->%s: Invalid format '%s'\n",
cdrel_basename(config->config_filename), cdrel_basename(config->output_filename), value);
return NULL;
}
}
if (config->format_type != cdrel_format_json) {
value = ast_variable_find(category, "separator_character");
if (!ast_strlen_zero(value)) {
ast_copy_string(config->separator, ast_unescape_c(ast_strdupa(value)), 2);
}
value = ast_variable_find(category, "quote_character");
if (!ast_strlen_zero(value)) {
ast_copy_string(config->quote, value, 2);
}
value = ast_variable_find(category, "quote_escape_character");
if (!ast_strlen_zero(value)) {
ast_copy_string(config->quote_escape, value, 2);
}
value = ast_variable_find(category, "quoting_method");
if (!ast_strlen_zero(value)) {
if (ast_strings_equal(value, "all")) {
config->quoting_method = cdrel_quoting_method_all;
} else if (ast_strings_equal(value, "minimal")) {
config->quoting_method = cdrel_quoting_method_minimal;
} else if (ast_strings_equal(value, "non_numeric")) {
config->quoting_method = cdrel_quoting_method_non_numeric;
} else if (ast_strings_equal(value, "none")) {
config->quoting_method = cdrel_quoting_method_none;
} else {
ast_log(LOG_WARNING, "%s->%s: Invalid quoting method '%s'\n",
cdrel_basename(config->config_filename), cdrel_basename(config->output_filename), value);
return NULL;
}
}
}
res = AST_VECTOR_INIT(fields, 20);
if (res != 0) {
return NULL;
}
tmp_fields = ast_strdupa(fields_value);
while((value = ast_strsep(&tmp_fields, ',', AST_STRSEP_TRIM))) {
res = AST_VECTOR_APPEND(fields, ast_strdup(value));
if (res != 0) {
return NULL;
}
}
if (AST_VECTOR_INIT(&config->fields, AST_VECTOR_SIZE(fields)) != 0) {
return NULL;
}
if (load_fields(config, fields) != 0) {
return NULL;
}
ast_log(LOG_NOTICE, "%s->%s: Logging %s records\n",
cdrel_basename(config->config_filename), cdrel_basename(config->output_filename),
RECORD_TYPE_STR(config->record_type));
rtn_config = config;
config = NULL;
return rtn_config;
}
/*!
* \internal
* \brief Load a legacy configs from the "mappings" category.
*
* \param record_type
* \param configs
* \param category
* \param config_filename
* \retval 0 on success.
* \retval -1 on failure.
*/
static int load_text_file_legacy_mappings(enum cdrel_record_type record_type,
struct cdrel_configs *configs, struct ast_category *category,
const char *config_filename)
{
struct ast_variable *var = NULL;
for (var = ast_category_first(category); var; var = var->next) {
struct cdrel_config *config = NULL;
if (ast_strlen_zero(var->name) || ast_strlen_zero(var->value)) {
ast_log(LOG_WARNING, "%s: %s mapping must have both a filename and a template at line %d\n",
cdrel_basename(config_filename), RECORD_TYPE_STR(config->record_type), var->lineno);
continue;
}
config = load_text_file_legacy_config(record_type, config_filename, var->name, var->value);
if (!config) {
continue;
}
if (AST_VECTOR_APPEND(configs, config) != 0) {
config_free(config);
return -1;
}
}
return 0;
}
/*!
* \internal
* \brief Load all text file backend configs from a config file.
*
* \param record_type
* \param configs
* \param config_filename
* \param reload
* \return
*/
static int load_text_file_config_file(enum cdrel_record_type record_type,
struct cdrel_configs *configs, const char *config_filename, int reload)
{
struct ast_config *cfg;
struct ast_flags config_flags = { reload ? CONFIG_FLAG_FILEUNCHANGED : 0 };
struct ast_category *category = NULL;
ast_debug(1, "%s: %soading\n", config_filename, reload ? "Rel" : "L");
cfg = ast_config_load(config_filename, config_flags);
if (!cfg || cfg == CONFIG_STATUS_FILEINVALID) {
ast_log(LOG_ERROR, "Unable to load %s. Not logging %ss to custom files\n",
config_filename, RECORD_TYPE_STR(record_type));
return -1;
} else if (cfg == CONFIG_STATUS_FILEUNCHANGED) {
ast_debug(1, "%s: Config file unchanged, not reloading\n", config_filename);
return 1;
}
while ((category = ast_category_browse_filtered(cfg, NULL, category, NULL))) {
const char *category_name = ast_category_get_name(category);
if (ast_strings_equal(category_name, "mappings")) {
load_text_file_legacy_mappings(record_type, configs, category, config_filename);
} else {
struct cdrel_config * config = load_text_file_advanced_config(record_type, category,
config_filename);
if (!config) {
continue;
}
if (AST_VECTOR_APPEND(configs, config) != 0) {
config_free(config);
return -1;
}
}
}
ast_config_destroy(cfg);
ast_log(LOG_NOTICE, "%s: Loaded %d configs\n", config_filename, (int)AST_VECTOR_SIZE(configs));
/* Only fail if no configs were valid. */
return AST_VECTOR_SIZE(configs) > 0 ? 0 : -1;
}
static int register_backend(enum cdrel_record_type record_type, const char *backend_name, void *log_cb)
{
switch(record_type) {
case cdrel_record_cdr:
return ast_cdr_register(backend_name, "", log_cb);
case cdrel_backend_db:
return ast_cel_backend_register(backend_name, log_cb);
default:
return -1;
}
}
static int unregister_backend(enum cdrel_record_type record_type, const char *backend_name)
{
switch(record_type) {
case cdrel_record_cdr:
return ast_cdr_unregister(backend_name);
case cdrel_record_cel:
return ast_cel_backend_unregister(backend_name);
default:
return -1;
}
}
static int load_config_file(enum cdrel_backend_type output_type, enum cdrel_record_type record_type,
struct cdrel_configs *configs, const char *filename, int reload)
{
switch(output_type) {
case cdrel_backend_text:
return load_text_file_config_file(record_type, configs, filename, reload);
case cdrel_backend_db:
return load_database_config_file(record_type, configs, filename, reload);
default:
return -1;
}
}
int cdrel_reload_module(enum cdrel_backend_type output_type, enum cdrel_record_type record_type,
struct cdrel_configs **configs, const char *filename)
{
int res = 0;
struct cdrel_configs *old_configs = *configs;
struct cdrel_configs *new_configs = NULL;
/*
* Save new config to a temporary vector to make sure the
* configs are valid before swapping them in.
*/
new_configs = ast_malloc(sizeof(*new_configs));
if (!new_configs) {
return AST_MODULE_LOAD_DECLINE;
}
if (AST_VECTOR_INIT(new_configs, AST_VECTOR_SIZE(old_configs)) != 0) {
return AST_MODULE_LOAD_DECLINE;
}
res = load_config_file(output_type, record_type, new_configs, filename, 1);
if (res < 0) {
AST_VECTOR_RESET(new_configs, config_free);
AST_VECTOR_PTR_FREE(new_configs);
return AST_MODULE_LOAD_DECLINE;
}
if (res == 0) {
/* Now swap the new ones in. */
*configs = new_configs;
/* Free the old ones. */
AST_VECTOR_RESET(old_configs, config_free);
AST_VECTOR_PTR_FREE(old_configs);
}
return AST_MODULE_LOAD_SUCCESS;
}
struct cdrel_configs *cdrel_load_module(enum cdrel_backend_type backend_type,
enum cdrel_record_type record_type, const char *filename, const char *backend_name,
void *log_cb)
{
struct cdrel_configs *configs = ast_calloc(1, sizeof(*configs));
if (!configs) {
return NULL;
}
ast_debug(1, "Loading %s %s\n", RECORD_TYPE_STR(record_type), MODULE_TYPE_STR(backend_type));
if (AST_VECTOR_INIT(configs, 5) != 0) {
cdrel_unload_module(backend_type, record_type, configs, backend_name);
return NULL;
}
if (load_config_file(backend_type, record_type, configs, filename, 0) != 0) {
cdrel_unload_module(backend_type, record_type, configs, backend_name);
return NULL;
}
if (register_backend(record_type, backend_name, log_cb)) {
cdrel_unload_module(backend_type, record_type, configs, backend_name);
return NULL;
}
return configs;
}
int cdrel_unload_module(enum cdrel_backend_type backend_type, enum cdrel_record_type record_type,
struct cdrel_configs *configs, const char *backend_name)
{
if (unregister_backend(record_type, backend_name) != 0) {
return -1;
}
AST_VECTOR_RESET(configs, config_free);
AST_VECTOR_PTR_FREE(configs);
return 0;
}