|
#!/usr/bin/env php
<?php
/*----------------------------------------------------------------------------
* Script: whitelist.agi
* Author: Brian <genius@groupbcl.ca> :)
* Date: January 2020
*
* AGI script to handle call whitelisting in FreePBX.
*----------------------------------------------------------------------------
* This script stores the following keys in the Asterisk database
* ('whitelist' family):
* hardfail_regexp (can be altered by the admin as needed)
* last_run_time Last time this script ran (seconds since epoch)
* cm_no_name_id ID of NO_NAME entry in Contact Manager Whitelist-AGI
* artificial_cnam From CID Superfecta 'abandon lookup' method
* dest_success (cached from extensions_additional.conf)
* dest_softfail (cached from extensions_additional.conf)
* dest_hardfail (cached from extensions_additional.conf)
*----------------------------------------------------------------------------
* Copyright (c) 2020 by the author. This program is licensed under the
* terms of the Gnu Public License version 2 or any later version.
*---------------------------------------------------------------------------*/
include '/etc/freepbx.conf';
require_once "phpagi.php";
require_once "sql.php";
const NO_NAME = 'zzzzz no name';
$debug = 0;
/*----------------------------------------------------------------------------
* verbose: write a [VERBOSE] message to the Asterisk log
*---------------------------------------------------------------------------*/
function verbose($log_fn, $call_num, $level, $msg) {
global $debug;
$x = date('[Y-m-d H:i:s]') . ' ' . $level . '[' . getmypid() .
"]$call_num whitelist.agi: $msg";
$fh = fopen($log_fn, "a"); fwrite($fh, "$x\n"); fclose($fh);
if ($debug) print "$x\n";
}
/*----------------------------------------------------------------------------
* log_cm_result: log the result of a FreePBX::Contactmanager method
*---------------------------------------------------------------------------*/
function log_cm_result($r, $success_op, $fail_op, $message, $log_fn = '', $call_num = '') {
global $agi;
global $debug;
$x = "";
if ($r['status'] == 1) {
$level = 'VERBOSE';
$x = "$success_op $message";
} else {
$level = 'ERROR';
$x = "ERROR: failed to $fail_op $message; type=" . $r['_sql_type'] .
", message=\"" . $r['_sql_msg'] . "\"";
}
if ($agi) $agi->verbose($x);
if ($log_fn) verbose($log_fn, $call_num, $level, $x);
}
/*----------------------------------------------------------------------------
* error_exit: write an error message to the Asterisk log and exit
*---------------------------------------------------------------------------*/
function error_exit($log_fn, $call_num, $msg) {
verbose($log_fn, $call_num, 'ERROR', $msg);
exit(1);
}
/*----------------------------------------------------------------------------
* get_or_set_up_cm_group: ensure the Contact Manager group Whitelist-AGI
* exists and has an NO_NAME entry
*---------------------------------------------------------------------------*/
function get_or_set_up_cm_group($log_fn, $call_num) {
// Ensure there's a Whitelist-AGI group in the Contact Manager
$cm = FreePBX::Contactmanager();
$gid = -1; # 1 = found group 'Whitelist-AGI'
foreach ($cm->getGroups() as $g)
if ($g['name'] == 'Whitelist-AGI') $gid = $g['id'];
if ($gid == -1) {
$r = $cm->addGroup('Whitelist-AGI', 'external', -1);
log_cm_result($r, "Created", "create", "Contact Manager group 'Whitelist-AGI'");
$gid = $r['id'];
}
// Ensure there's an NO_NAME entry in the Whitelist-AGI group
$x_cnum = '999999999999999';
$entry = $cm->getNameByNumber($x_cnum, array($gid));
if (!$entry) {
$a = array("displayname" => NO_NAME, "type" => "external");
$entry = $cm->addEntryByGroupID($gid, $a);
log_cm_result($entry, 'Added', 'add', "no name numbers to the Whitelist-AGI group",
$log_fn, $call_num);
$r = $cm->addNumbersByEntryID($entry['id'], array(
array( "number" => $x_cnum, "extension" => "", "type" => "other", "flags" => array()
)));
log_cm_result($r, "Attached", "attach", "number $x_cnum to no name numbers" .
" in the Whitelist-AGI group", $log_fn, $call_num);
}
return(array($gid, $entry['id']));
}
/*----------------------------------------------------------------------------
* check_cache: update cached variables if Asterisk has been restarted
* since the last time this script ran.
*---------------------------------------------------------------------------*/
function check_cache($cdr_db) {
global $agi;
global $db;
global $last_run_time;
// Determine how long Asterisk has been running
preg_match_all("/Last reload: (\d+)/", `asterisk -x 'core show uptime seconds'`, $a);
$ast_uptime = $a[1][0];
// Has Asterisk restarted since the last time this script ran?
$res = $agi->database_get('whitelist', 'last_run_time');
$last_run_time = $res['data'] ? $res['data'] : 0;
if (time() - $last_run_time < $ast_uptime ) return 0;
$agi->verbose("Asterisk has been restarted since the last time this script " .
"ran; updating cache values");
if (! $last_run_time) $last_run_time = time();
// Set up the default hardfail regular expression
$res = $agi->database_get('whitelist', 'hardfail_regexp');
if (! $res['data']) $agi->database_put('whitelist', 'hardfail_regexp',
'[0-9 -]+$|anonymous|blocked|spam|unknown|withheld');
/* If the administrator enabled CID Superfecta and is using its
* "Abandon Lookup" data source, there may be a value set for
* "Artificial CNAM." Look up that value in the database. */
$sql = "SELECT `value` FROM superfectaconfig " .
"WHERE source='Default_Abandon_lookup' AND field='Artificial_CNAM'";
$res = $db->sql($sql, "ASSOC");
$agi->database_put('whitelist', 'artificial_cnam', $res[0]['value']);
// Scan the [customdests] context in /etc/asterisk/extensions_additional
// to get the correct "dest-#" for the success/softfail/hardfail results
$regexp = "/^exten => (dest-[0-9]+),n,Gosub\(whitelist,s,(success|softfail|hardfail)\(\)\)/";
$fd = fopen('/etc/asterisk/extensions_additional.conf', 'r');
$i = 0; // Count of regexp matches so far; bail out at 3
while (($line = fgets($fd, 1024)) && $i < 3) {
if (preg_match_all("$regexp", $line, $a)) {
$agi->database_put('whitelist', 'dest_'.$a[2][0], $a[1][0]);
$i++;
}
}
fclose($fh);
// Cache the Whitelist-AGI group ID and its NO_NAME entry ID
$id_arr = get_or_set_up_cm_group('', '');
$res = $agi->database_get('whitelist', 'Whitelist_AGI_group_id');
if (! $res['data']) {
$agi->database_put('whitelist', 'Whitelist_AGI_group_id', $id_arr[0]);
$agi->verbose("Contact Manager Whitelist-AGI group ID is $id_arr[0]");
}
$res = $agi->database_get('whitelist', 'cm_no_name_id');
if (! $res['data']) {
$agi->database_put('whitelist', 'cm_no_name_id', $id_arr[1]);
$agi->verbose("NO_NAME entry ID is $id_arr[1]");
}
return 1;
}
/*----------------------------------------------------------------------------
* whitelist_check: look up the CallerID Name as determined by the route's
* CID Lookup Source and set WHITELIST_GOTO to the destination named by
* the admin for the 'success', 'softfail', or 'hardfail' results.
*---------------------------------------------------------------------------*/
function whitelist_check() {
global $agi;
global $db;
$result = 'softfail';
$rc = 0;
$trace = 1;
// Get the CallerID name as set by the trunk and possibly updated by a
// route's 'CID Lookup Source' and/or CallerID Superfecta
$cid_num = $agi->request['agi_callerid'];
if ($trace && $cid_num) $agi->verbose(">> CALLERID(number) is '$cid_num'");
$cid_name = "";
$res = $agi->get_variable('CALLERID(name)');
if ($res['result']) $cid_name = $res['data'];
if ($trace) $agi->verbose(">> CALLERID(name) is '$cid_name'");
// Canada411 returns "Company Name111 Main St,aaCity,aaXXaaaaaaaaPostCde".
// Attempt to fix these
$res = $agi->get_variable('SUPERFECTA_OLD');
$superfecta_old = $res['result'] ? $res['data'] : '';
$old_new = 'OLD';
foreach (array(1, 2) as $i) {
// Track down and remove extraneous 'a'
$x = preg_replace("/,a+/", ", ", $res['data']);
$x = preg_replace("/([[:alnum:]])aa+([A-Z])/", "$1 $2", $x);
$x = preg_replace("/(,.*?)a*$/", "$1", $x);
// Separate "LastnameCity" to "Lastname, City" or "Company50 Main" to
// "Company, 50 Main". This can tripped by (eg) "MacNeil" but is good
// for 99% of cases.
$x = preg_replace("/( [A-Z][a-z]+\.?)([0-9]+|[A-Z][a-z]+,? )/", "$1, $2", $x);
if ($res['data'] != $x) {
$agi->set_variable("SUPERFECTA_$old_new", $x);
if ($trace) $agi->verbose(">> Changed SUPERFECTA_$old_new to '$x'");
if ($old_new == 'NEW') $agi->set_variable('CALLERID(name)', $x);
$cid_name = $x;
}
$old_new = 'NEW'; $res = $agi->get_variable("SUPERFECTA_$old_new");
}
// Check $cid_name against the CID Superfecta 'Abandon Lookup' Artificial
// CNAM; if a match, replace $cid_name with ${SUPERFECTA_OLD}
# $res = $agi->get_variable('SUPERFECTA_OLD'); // Currently in
# $superfecta_old = $res['result'] ? $res['data'] : ''; // prev block
$res = $agi->database_get('whitelist', 'artificial_cnam');
if ($res['result'] && $cid_name == $res['data']) {
if ($superfecta_old) {
$cid_name = $superfecta_old;
$agi->set_variable('SUPERFECTA_NEW', '');
if ($trace) {
$agi->verbose(">> Name matches CID Superfecta 'Abandon Lookup'" .
" Artificial CNAM");
$agi->verbose(">> Changed name to SUPERFECTA_OLD value '$cid_name'");
}
}
}
// Get the hardfail regular expresssion
$res = $agi->database_get('whitelist', 'hardfail_regexp');
$hardfail_re = $res['data'];
/* First try the NO_NAME list. This is essentially a continuation of the
* `WhitelistGroup` CallerID Lookup Source, with the twist that we don't
* want to set $cid_name to the returned value */
$cm_res = $agi->database_get('whitelist', 'cm_no_name_id');
$uwl_params = array("cm_no_name_id" => $cm_res['data']);
set_uwl_params($uwl_params);
$xmpps_select = $uwl_params['xmpps_select'];
$xmpps_select->bindValue(':xmpp', "$cid_num%", PDO::PARAM_STR);
$xmpps_select->bindValue(':xmpp1', "1$cid_num%", PDO::PARAM_STR);
$xmpps_select->bindValue(':xmpp9', "9$cid_num%", PDO::PARAM_STR);
$xmpps_select->bindValue(':xmpp_plus1', "+1$cid_num%", PDO::PARAM_STR);
$xmpps_select->execute();
$res = $xmpps_select->fetch();
/* If found, ask the background process to update the name attached to the
* number. To prevent polluting the main list with unwanted information,
* the move is not done if the name matches the hardfail regexp. */
if ($res) {
$result = "success";
$trace_msg = "number found in NO_NAME list, value '" . $res[1] . "'";
$rc = (preg_match("/^($hardfail_re)/i", $cid_name) ? 0 : 1);
if ($rc) $trace_msg .= "; requesting move to $cid_name";
} elseif ($trace)
$agi->verbose(">> Number was not found in the Whiitelst-AGI group's NO_NAME entry");
// No success with the NO_NAME list; is this a hardfail?
if ($result != 'success' && preg_match("/^($hardfail_re)/i", $cid_name)) {
$result = 'hardfail';
$trace_msg = "CID name matched 'hardfail' regular expression";
}
// Not a hardfail; can we upgrade softfail to success? (We can upgrade
// *only* if the primary CallerID Lookup Source returned a good result)
if ($result == 'softfail') {
$trace_msg = "number was found in the route's CallerID Lookup Source";
$reason = ''; // Reason for failing to upgrade result to 'success'
$res = $agi->get_variable['CMID']; // Result from 'WhitelistGroup' CID Lookup Source
if ($res['result'] && $res['data'] == 'Unknown')
$reason = "result 'Unknown'";
if (substr($cid_name,0,13) == 'LOOKUP_FAILED') {
$reason = "a value starting with 'LOOKUP_FAILED'";
$cid_name = substr($cid_name, 14);
}
if ($superfecta_old && substr($superfecta_old,0,13) == 'LOOKUP_FAILED')
$reason = "(via SUPERFECTA_OLD) a value starting with 'LOOKUP_FAILED'";
if ($reason)
$trace_msg = "the CID Lookup Source returned $reason";
else
$result = 'success';
}
if ($trace) $agi->verbose(">> $result: $trace_msg");
// Tell the whitelist context where it can go :)
$agi->verbose("CID name='$cid_name' result=$result", 1);
$agi->set_variable('CALLERID(name)', $cid_name);
$res = $agi->database_get('whitelist', "dest_$result");
$agi->set_variable("WHITELIST_RESULT ", $result);
$agi->set_variable("WHITELIST_GOTO ", $res['data']);
return $rc;
}
/*----------------------------------------------------------------------------
* set_uwl_params: set some parameters for calling update_whitelist
*---------------------------------------------------------------------------*/
function set_uwl_params(&$uwl_params) {
// Get the Asterisk log file name so we can write messages to it
if (! array_key_exists('log_fn', $uwl_params)) {
$ast_conf = parse_ini_file('/etc/asterisk/asterisk.conf');
$log_fn = $ast_conf['astlogdir'].'/full';
$uwl_params['ast_conf'] = $ast_conf; // (for background_process)
$uwl_params['log_fn'] = $log_fn;
}
// Bail out here if 'cm_no_name_id' is not yet set
if (! array_key_exists('cm_no_name_id', $uwl_params)) return;
/* Prepare SELECT, INSERT, and DELETE statements for contactmanager_entry_xmpps
* For SELECT, the stored text is similar to "<number> Called From|Whitelisted
* by <name> (<exten>) YYYY-MM-DD HH:MM", but we need to match only on the
* number. Testing has shown that using "LIKE '<number>%' OR LIKE '1<number>%'
* OR LIKE '9<number>%' OR LIKE '+1<number>%'" is much faster than REGEXP
* '^\+\?[19]<number>' when the xmpp column is indexed. */
$cm = FreePBX::Contactmanager();
$fpbx_db = $cm->db;
$t = 'contactmanager_entry_xmpps';
$eid = $uwl_params['cm_no_name_id'];
$xmpps_select = $fpbx_db->prepare("SELECT id, xmpp FROM $t WHERE entryid=$eid
AND (xmpp LIKE :xmpp OR xmpp LIKE :xmpp1 OR xmpp LIKE :xmpp9 OR xmpp LIKE :xmpp_plus1)");
$xmpps_insert = $fpbx_db->prepare("INSERT INTO $t VALUES (NULL, $eid, :xmpp)");
$xmpps_delete = $fpbx_db->prepare("DELETE FROM $t WHERE id=:id");
$uwl_params['xmpps_select'] = $xmpps_select;
$uwl_params['xmpps_insert'] = $xmpps_insert;
$uwl_params['xmpps_delete'] = $xmpps_delete;
$uwl_params['fpbx_db'] = $fpbx_db; // (for background_process)
// Bail out here if 'gid' is not yet set
if (! array_key_exists('gid', $uwl_params)) return;
// Generate a hash of group displaynames and IDs
$entry_id_cache = (array_key_exists('entry_id_cache', $uwl_params)
? $uwl_params['entry_id_cache'] : array());
$entry_id_cache = array();
foreach ($cm->getEntriesByGroupID($uwl_params['gid']) as $e)
$entry_id_cache[strtolower($e['displayname'])] = $e['uid'];
$uwl_params['cm'] = $cm;
$uwl_params['entry_id_cache'] = $entry_id_cache;
}
/*----------------------------------------------------------------------------
* background_process: check for new called numbers since the last time
* this script ran and if any are found update the whitelist. To speed
* the whitelist lookup process, this part runs as a separate process from
* the inbound call whitelist handler. It doesn't inherit stdin/stdout from
* the parent process, so it doesn't have access to the normal set of AGI
* functions.
*---------------------------------------------------------------------------*/
function background_process() {
global $argv;
global $bmo; // The main FreePBX object
global $amp_conf;
global $lock_fn;
global $debug;
$uwl_params = array(); // Parameter list for update_whitelist
// Parse the command line options
$options = getopt("", array('background::', 'debug', 'last-run-time:',
'check-cdrdb', 'add-number:', 'to-name:', 'no-name-text::'));
if (array_key_exists('debug', $options)) {
$debug = 1;
@ini_set('display_errors', 1);
error_reporting(E_ALL);
}
// Get the name of the asterisk log file
set_uwl_params($uwl_params);
$log_fn = $uwl_params['log_fn'];
// If we got a channel ID in the --background parameter, search the log
// file for it so we can get the C-0000xxxx call number
$call_num = '';
if (array_key_exists('background', $options) && $options['background']) {
$fh = fopen($log_fn, "r");
if ($fh) {
$channel = str_replace("/", "\\/", $options['background']);
fseek($fh, -16384, SEEK_END);
while ((($l = fgets($fh, 4096)) !== false) && ! $call_num) {
if (preg_match("/(\[C-[0-9a-f]+\]).*$channel/", $l, $arr))
$call_num = $arr[1];
}
fclose($fh);
}
}
$uwl_params['call_num'] = $call_num;
// Guard against two instances running simultaneously
$lock_fn = '/tmp/whitelist-agi-background-process.lock';
if (file_exists($lock_fn)) {
verbose($log_fn, $call_num, 'WARNING', "lock file $lock_fn found; exiting");
return;
}
$lock_fh = fopen($lock_fn, 'w');
fclose($lock_fh);
// Ensure the lock file is removed when the script exits
register_shutdown_function('remove_lock_file');
// Determine the last time this script ran
$last_run_time = time();
if (array_key_exists('last-run-time', $options))
$last_run_time = $options['last-run-time'];
// Connect to the FreePBX Call Detail Records database
$conf=($bmo->Config->freepbx_conf->conf);
$cdrdb_name = $conf['CDRDBNAME'] ? $conf['CDRDBNAME'] : 'asteriskcdrdb';
$cdr_db = new mysqli(
$conf['CDRDBHOST'] ? $conf['CDRDBHOST'] : 'localhost',
$conf['CDRDBUSER'] ? $conf['CDRDBUSER'] : $conf['AMPDBUSER'],
$conf['CDRDBPASS'] ? $conf['CDRDBPASS'] : $conf['AMPDBPASS'],
$cdrdb_name
);
if (! $cdr_db)
error_exit($log_fn, $call_num, "failed to connect to $cdrdb_name database");
// Query the FreePBX database for the trunk names
$fpbx_db = new mysqli($amp_conf['AMPDBHOST'], $amp_conf['AMPDBUSER'],
$amp_conf['AMPDBPASS'], $amp_conf['AMPDBNAME']);
if (! $fpbx_db) error_exit($log_fn,
'failed to connect to FreePBX ' . $amp_conf['AMPDBNAME'] . ' database');
$res = $fpbx_db->query("SELECT name FROM trunks", MYSQLI_USE_RESULT);
$trunk_regexp = "";
while ($row = $res->fetch_row())
$trunk_regexp .= ($trunk_regexp ? '|' : '') . $row[0];
$fpbx_db->close();
// Get the ID of the Contact Manager Whitelist-AGI group
$ast_db_fn = $uwl_params['ast_conf']['astvarlibdir'] . '/astdb.sqlite3';
$ast_db = new sqlite3($ast_db_fn);
if (! $ast_db) error_exit($log_fn, $call_num,
"failed to connect to Asterisk database $ast_db_fn");
$i = 0;
$gid = $ast_db->querySingle("SELECT value FROM astdb WHERE key='/whitelist/Whitelist_AGI_group_id'");
if ($gid)
$cm_no_name_id = $ast_db->querySingle("SELECT value FROM astdb WHERE key='/whitelist/cm_no_name_id'");
// The queries can fail because Asterisk has the database locked or this is
// the very first run of this program and the cache isn't set up yet.
if (! $gid) {
verbose($log_fn, $call_num, 'VERBOSE', "Failed to read Whitelist_AGI_group_id from $ast_db_fn");
verbose($log_fn, $call_num, 'VERBOSE', "Requesting information using get_or_set_up_cm_group()");
$id_arr = get_or_set_up_cm_group($log_fn, '');
$gid = $id_arr[0];
$cm_no_name_id = $id_arr[1];
$uwl_params['entry_id_cache'] = array("strtolower(NO_NAME)" => $id_arr[1]);
}
$uwl_params['gid'] = $gid;
$uwl_params['cm_no_name_id'] = $cm_no_name_id;
// Set up 'cm' and 'xmpps_(select|update|delete)' objects
set_uwl_params($uwl_params);
/* Now that we have handles for both the CDR and FreePBX databases, ensure
* the 'cdr.cnum' column in the CDR database is indexed, as well as the
* 'contactmanager_entry_xmpps.xmpp' column in the FreePBX database */
if (array_key_exists('check-cdrdb', $options)) {
$needs_index = 0;
$fpbx_db = $uwl_params['fpbx_db'];
$t = 'contactmanager_entry_xmpps';
$res = $fpbx_db->query("DESCRIBE $t");
while ($r = $res->fetch())
if ($r['Field'] == 'xmpp' && ! $r['Key']) $needs_index = 1;
if ($needs_index) {
$res = $fpbx_db->query("ALTER TABLE $t ADD INDEX xmpp(xmpp)");
verbose($log_fn, $call_num, 'VERBOSE', "Added index to the 'xmpp' column of" .
" the '$t' table");
}
$needs_index = 0;
$res = $cdr_db->query('DESCRIBE cdr', MYSQLI_USE_RESULT);
while ($r = $res->fetch_row())
if ($r[0] == 'cnum' && ! $r[3]) $needs_index = 1;
if ($needs_index) {
$res = $cdr_db->query("ALTER TABLE cdr ADD INDEX cnum(cnum)");
verbose($log_fn, $call_num, 'VERBOSE', "Added index to the 'cnum' column of" .
" the 'cdr' table in database ".$cdrdb_conf['database']);
}
}
// If --add-number and --to-name were passed, update the Whitelist-AGI group
$no_name_text = array_key_exists('no-name-text', $options) ? $options['no-name-text'] : '';
if (array_key_exists('add-number', $options) &&
array_key_exists('to-name', $options))
update_whitelist($options['add-number'], $options['to-name'],
$uwl_params, $no_name_text);
// Check the call detail records for numbers that were called since the
// last time this script ran
$cdr_res = $cdr_db->query("SELECT a.calldate, a.src, a.cnam, a.dst,
IF (b.cnam IS NOT NULL, b.cnam, '".NO_NAME."') AS cnam
FROM cdr AS a
LEFT JOIN cdr AS b ON b.cnum=a.dst
WHERE a.calldate > FROM_UNIXTIME($last_run_time)
AND a.dstchannel REGEXP '($trunk_regexp)'
AND a.dst REGEXP '^[0-9]{7,}$'
GROUP BY a.dst");
// Check the called numbers against the Whitelist-AGI entries
while ($r = $cdr_res->fetch_row()) {
$no_name_text = "Called by ".$r[2]." (".$r[1].") ".substr($r[0],0,16);
update_whitelist($r[3], $r[4], $uwl_params, $no_name_text);
}
$cdr_db->close();
verbose($log_fn, $call_num, 'VERBOSE', 'Background process exit');
}
/*----------------------------------------------------------------------------
* update_whitelist: add a cnam entry to the Whitelist-AGI group if it
* doesn't already exist, then add the passed cnum to it. If the cnum
* already exists in the NO_NAME entry, move it to the correct entry
* and delete it from the NO NAME numbers.
*
* Return values:
* 0 = Number added to whitelist
* 1 = Number is already whitelisted under a different name
* 2 = Database error locating or adding 'cnam'
* 3 = Database error adding 'cnum' to 'cnam's entry
* 2xxx = SQL error removing number from NO_NAME list
*---------------------------------------------------------------------------*/
function update_whitelist($cnum, $cnam, $params, $no_name_text = '') {
$call_num = $params['call_num'];
$cm = $params['cm'];
$entry_id_cache = $params['entry_id_cache'];
$gid = $params['gid'];
$log_fn = $params['log_fn'];
if (array_key_exists('xmpps_select', $params)) {
$xmpps_select = $params['xmpps_select'];
$xmpps_insert = $params['xmpps_insert'];
$xmpps_delete = $params['xmpps_delete'];
}
$r = 0;
$x_cnum = ($cnam == NO_NAME ? 'no name' : "'$cnam'");
verbose($log_fn, $call_num, 'VERBOSE', "Update whitelist: number $cnum" .
($cnam == NO_NAME ? '' : ", name $x_cnum"));
$entry = 0; # The cnam's entry in Whitelist-AGI group
$update_needed = 1;
// Search the Whitelist-AGI group for the number
$search_res = $cm->getNameByNumber($cnum, array($gid));
/* If not found, search for the number in the NO_NAME entry. */
if (! $search_res && isset($xmpps_select)) {
$xmpps_select->bindValue(':xmpp', "$cnum%", PDO::PARAM_STR);
$xmpps_select->bindValue(':xmpp1', "1$cnum%", PDO::PARAM_STR);
$xmpps_select->bindValue(':xmpp9', "9$cnum%", PDO::PARAM_STR);
$xmpps_select->bindValue(':xmpp_plus1', "+1$cnum%", PDO::PARAM_STR);
$xmpps_select->execute();
$search_res = $xmpps_select->fetch();
if ($search_res) $search_res['displayname'] = NO_NAME;
}
// CallerID number was found in the Whitelist-AGI group
if ($search_res) {
// If the number is assigned to NO_NAME, remove it from the NO NAME
// list and add it to the name that wat passed
if ($search_res['displayname'] == NO_NAME) {
if ($cnam != NO_NAME) {
// Remove the cnum from the XMPP list
$xmpps_delete->bindParam(':id', $search_res['id']);
$xmpps_delete->execute();
$x = $xmpps_delete->errorCode();
$res = array("_sql_status" => $x, "status" => ($x == '0000' ? 1 : 0));
if ($x != '0000') {
$a = $xmpps_insert->errorInfo();
$res['_sql_type'] = $a[0]; $res['_sql_msg'] = $a[2];
}
log_cm_result($res, 'Removed', 'remove', "$cnum from no name" .
" numbers in the Whitelist-AGI group", $log_fn, $call_num);
if ($res['status'] != 1) return '2'.$x;
$entry = $search_res;
$search_res = NULL;
} else {
// Return 0 (success) if the to-name is NO_NAME and the number
// was found in NO_NAME
verbose($log_fn, $call_num, 'VERBOSE', "No update needed (already in no name list)");
return 0;
}
} else if (strtolower($search_res['displayname']) != strtolower($cnam)) {
// Return an error if the number is already in the database but
// under a different name from the one that was requested
verbose($log_fn, $call_num, 'ERROR', "Error: number is currently assigned to '" .
$search_res['displayname'] . "'");
return 1;
} else {
// Return 0 (success) if the requested number is already assinged
// to the requested name
verbose($log_fn, $call_num, 'VERBOSE', "No update needed (number is" .
" already assigned to '" . $search_res['displayname'] . "')");
return 0;
}
}
// Do we have an entry with requested 'to' name?
if (array_key_exists(strtolower($cnam), $entry_id_cache)) {
$entry = $cm->getEntryById($entry_id_cache[strtolower($cnam)]);
$entry['status'] = 1;
} else { // No: add one
$a = array("displayname" => $cnam, "type" => "external");
$entry = $cm->addEntryByGroupID($gid, $a);
log_cm_result($entry, 'Added', 'add', "$x_cnum to the Whitelist-AGI group",
$log_fn, $call_num);
$entry_id_cache[strtolower($cnam)] = $entry['id'];
}
if ($entry['status'] != 1) return 2;
/* Attach the number to the entry. Numbers added to the NO_NAME entry are
* stored as XMPP instead of telephone numbers to prevent subsequent CID
* Lookup Source queries from matching the number and returning NO_NAME as
* the CallerID name, in the process wiping out the CallerID name we got
* from the trunk. */
if ($cnam == NO_NAME) {
$x = $cnum . ($no_name_text ? " $no_name_text" : "");
$xmpps_insert->bindParam(':xmpp', $x);
$xmpps_insert->execute();
$x = $xmpps_insert->errorCode();
$res = array("_sql_status" => $x, "status" => ($x == '0000' ? 1 : 0));
if ($x != '0000') {
$a = $xmpps_insert->errorInfo();
$res['_sql_type'] = $a[0]; $res['_sql_msg'] = $a[2];
}
} else {
$res = $cm->addNumbersByEntryID($entry['id'], array(
array( "number" => $cnum, "extension" => "", "type" => "other", "flags" => array()
)));
}
log_cm_result($res, "Attached", "attach", "number $cnum to $x_cnum in the" .
" Whitelist-AGI group", $log_fn, $call_num);
// Return success or error
return ($res['status'] == 1 ? 0 : 3);
}
/*----------------------------------------------------------------------------
* Remove the lock file at exit
*---------------------------------------------------------------------------*/
function remove_lock_file() {
global $lock_fn;
unlink($lock_fn);
}
/*----------------------------------------------------------------------------
* add_from_context: Context 'whitelist,add,1' to allow the caller to add
* the last inbound number or an arbitraty number to the whitelist.
*
* The IVR can exit with the following (verbal) cause codes:
* 101 - Failed to connect to CDR database
* 105 - User failed to confirm number after 5 tries
*---------------------------------------------------------------------------*/
function add_from_context() {
global $agi;
global $agdb;
global $bmo; // The main FreePBX object
pcntl_signal(SIGHUP, "handle_SIGHUP()");
$agi->answer();
sleep(1);
// Get the number of the extension that originated the call
$r = $agi->get_variable('AMPUSER');
$exten = $r['data'];
$exten = 338; # FIXME: This is for DEVELOPMENT
// Connect to the FreePBX Call Detail Records database
$conf=($bmo->Config->freepbx_conf->conf);
$cdrdb_name = $conf['CDRDBNAME'] ? $conf['CDRDBNAME'] : 'asteriskcdrdb';
$cdr_db = new mysqli(
$conf['CDRDBHOST'] ? $conf['CDRDBHOST'] : 'localhost',
$conf['CDRDBUSER'] ? $conf['CDRDBUSER'] : $conf['AMPDBUSER'],
$conf['CDRDBPASS'] ? $conf['CDRDBPASS'] : $conf['AMPDBPASS'],
$cdrdb_name
);
if (! $cdr_db)
sorry_and_disconnect(101, "Failed to connect to CDR database $cdrdb_name");
// Prompt the caller for 1 to whitelist last caller, 2 to enter another number
$r = get_dtmf("1 to whitelist last caller, 2 to enter another number",
array('privacy-to-whitelist-last-caller', 'press-1',
'to-enter-a-diff-number','press-2'),
array(1,2)
);
$cnum = -1;
// Get the last number this caller dialled
if ($r == 1) {
$stm = $cdr_db->query("SELECT src FROM cdr WHERE dst='$exten'
AND src REGEXP '^[0-9]{7,}$' ORDER BY calldate DESC LIMIT 1");
$res = $stm->fetch_row();
if (! $res) {
$agi->stream_file('sorry');
$agi->stream_file('no-info-about-number');
$agi->stream_file('goodbye');
exit();
}
$cnum = $res[0];
$agi->stream_file('privacy-last-caller-was');
}
// Create the confirmation IVR menu
$menu = array('privacy-to-whitelist-this-number', 'press-1',
'to-enter-a-diff-number', 'press-2');
$options = array(1, 2);
$inbound_cnum = $cnum;
if ($inbound_cnum != -1) {
$menu[] = 'to-hear-callerid'; $menu[] = 'press-3';
$options[] = 3;
}
// Verify this is the number the user wants to add to the whitelist
for ($i = 0; $i < 5; $i++) {
if ($cnum == -1) {
$cnum = get_dtmf("Prompt caller for a telephone number",
array('please-enter-the', 'telephone-number', 'vm-then-pound'),
array('#')
);
}
$agi->say_digits($cnum);
$dtmf = get_dtmf("Verify number", $menu, $options);
if ($dtmf == 1) break;
if ($dtmf == 2) $cnum = -1;
if ($dtmf == 3) $cnum = $inbound_cnum;
}
if ($i >= 5)
sorry_and_disconnect(105, "Caller failed to validate number after five tries");
// Search the call detail records for a name
$stm = $cdr_db->query("SELECT cnam FROM cdr WHERE cnum='$cnum'
ORDER BY calldate DESC LIMIT 1", MYSQLI_USE_RESULT);
$res = $stm->fetch_row();
if (! $res) { // No name found: use NO NAME with data 'Whitelisted by <extn> YYYY-MM-DD HH:MM'
$res = array(NO_NAME);
$exten = $agi->request['agi_callerid'];
$caller = $agi->request['agi_calleridname'];
$no_number_text = "Whitelisted by $caller ($exten) " . date('Y-m-d H:i');
}
// Set up parameters to pass to update_whitelist and call it
$uwl_params = array();
$x = $agi->database_get('whitelist', 'Whitelist_AGI_group_id');
$uwl_params['gid'] = $x['data'];
$x = $agi->database_get('whitelist', 'cm_no_name_id');
$uwl_params['cm_no_name_id'] = $x['data'];
set_uwl_params($uwl_params);
$agi->verbose("Calling update_whitelist($cnum, '" . $res[0] . "')");
$r = update_whitelist($cnum, $res[0], $uwl_params, $no_number_text);
// Play back the result
if ($r == 0)
$msg = array('num-was-successfully', 'privacy-whitelisted', 'goodbye');
elseif ($r == 1)
$msg = array( 'were-sorry', 'duplication', 'that-number', 'is-currently',
'privacy-whitelisted', 'please-contact-tech-supt', 'descending-2tone');
else
sorry_and_disconnect($r, "update_whitelist returned code $r");
foreach($msg as $sound) $agi->stream_file($sound);
$sleep(1);
}
/*----------------------------------------------------------------------------
* get_dtmf: play a prompt and wait for the caller to respond. Validate
* the response and return if it's good. Try three times before giving up.
*---------------------------------------------------------------------------*/
function get_dtmf($purpose, $sounds, $escape_digits) {
global $agi;
$max_digits = ($escape_digits[0]=='#' ? 15 : 1);
$dtmf = -1;
for ($i=0; $i < 3 && $dtmf < 0; $i++) {
$r = array("result" => '');
$save_digit = '';
for ($j = 0; $j < count($sounds)-1 && $r['result']==''; $j++)
$r = $agi->get_data($sounds[$j], 1, 1);
if ($r['result']=='' || $max_digits > 1) {
if ($max_digits) $save_digit = $r['result'];
$r = $agi->get_data($sounds[$j], 10000, $max_digits);
}
if ($max_digits == 1) { // Menu-style (press 1 or 2)
if (in_array($r['result']+0, $escape_digits))
$dtmf = $r['result'];
} else { // Get a telephone number ending with '#'
if ($r['result'])
$dtmf = $save_digit . $r['result'];
}
}
if ($i >= 3)
sorry_and_disconnect(101, "$purpose: did not get a good response; bailing out");
$agi->verbose("$purpose: returning $dtmf");
return $dtmf;
}
/*----------------------------------------------------------------------------
* sorry_and_disconnect: Play "We're sorry; connnection failed. Cause code
* <num>. Please contact technical support. <be-doop>" then exit.
* Dialplan hangs up the channel.
*---------------------------------------------------------------------------*/
function sorry_and_disconnect($code, $reason) {
global $agi;
$agi->verbose($reason);
$agi->stream_file('were-sorry');
$agi->stream_file('connection-failed');
$agi->stream_file('cause-code');
$agi->say_digits($code);
$agi->stream_file('please-contact-tech-supt');
$agi->stream_file('descending-2tone');
sleep(1);
exit();
}
/* handle_SIGHUP: bail out if the channel hangs up */
function handle_SIGHUP($signo) {
global $agi;
$agi->verbose("SIGHUP: exiting");
exit();
}
/*----------------------------------------------------------------------------
* M A I N P R O C E S S I N G
*---------------------------------------------------------------------------*/
if (! array_key_exists(1, $argv)) {
global $last_run_time;
$agi = new AGI();
$db = new AGIDB($agi);
// Update some cache values if Asterisk was restarted since the last
// time this script ran
$rc = check_cache();
$bg_params = "--background=" . $agi->request['agi_channel'] .
" --last-run-time=$last_run_time";
if ($rc) $bg_params .= " --check-cdrdb";
// Determine the whitelist result
$rc = whitelist_check();
if ($rc == 1) {
$res = $agi->get_variable('CALLERID(name)');
$bg_params .= ' --add-number=' . ($agi->request['agi_callerid']);
$bg_params .= " --to-name='" . $res['data'] . "'";
}
// Update the Asterisk database with the last run time
$agi->database_put('whitelist', 'last_run_time', time());
/* Create a subprocess to run independently of this one to check for
* outbound numbers that should be added to the Whitelist-AGI group. */
exec("nohup php " . $argv[0] . " $bg_params >/dev/null 2>&1 &");
$agi->verbose("Started background process; parameters $bg_params");
$agi->verbose("exit; returning '$rc'");
// User called *33 (or whatever number was set up in the dialplan) to add a
// number to the whitelist
} else if ($argv[1] == 'a') {
$agi = new AGI();
$db = new AGIDB($agi);
check_cache();
add_from_context();
// Check for outbound numbers that should be added to the Whitelist-AGI
// contact manager group
} else {
background_process($cdr_db);
}
# vim: tabstop=4
|