#!/usr/bin/php
<?php
/*
  vim: set expandtab tabstop=4 softtabstop=4 shiftwidth=4:
  Codificación: UTF-8
  +----------------------------------------------------------------------+
  | Elastix version 2.0                                                  |
  | http://www.elastix.org                                               |
  +----------------------------------------------------------------------+
  | Copyright (c) 2006 Palosanto Solutions S. A.                         |
  +----------------------------------------------------------------------+
  | Cdla. Nueva Kennedy Calle E 222 y 9na. Este                          |
  | Telfs. 2283-268, 2294-440, 2284-356                                  |
  | Guayaquil - Ecuador                                                  |
  | http://www.palosanto.com                                             |
  +----------------------------------------------------------------------+
  | The contents of this file are subject to the General Public License  |
  | (GPL) Version 2 (the "License"); you may not use this file except in |
  | compliance with the License. You may obtain a copy of the License at |
  | http://www.opensource.org/licenses/gpl-license.php                   |
  |                                                                      |
  | Software distributed under the License is distributed on an "AS IS"  |
  | basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See  |
  | the License for the specific language governing rights and           |
  | limitations under the License.                                       |
  +----------------------------------------------------------------------+
  | The Original Code is: Elastix Open Source.                           |
  | The Initial Developer of the Original Code is PaloSanto Solutions    |
  +----------------------------------------------------------------------+
  $Id: dhcpconfig.php,v 1.1 2007/01/09 23:49:36 alex Exp $
*/

/*******************************************************************************

All mentions of "current" or "currently" refer to the state of the code as of 
2012-06-23 in the 2.3.0 branch of the Elastix SVN repository.

This program is an attempt to improve on the current situation of the Endpoint
Configurator. The Endpoint Configurator (EC) is an Elastix module that tries to 
centralize the discovery and configuration of VoIP phones on the LAN, to save an
administrator the trouble of having to log into the administration interface of
each phone in order to point the phone to the Elastix server. Currently the 
Endpoint Configurator is sort of adequate for configuring at most a dozen phones
at a time, or to change the configuration of a single phone. It, however, fails
to be of help when configuring hundreds or even thousands of phones at a time.
The most obvious symptom is that the GUI locks up for minutes at a time when
configuring less than a dozen phones at once.

The design goals of this program are the following:
1) Parallel configuration: the EC needs to contact the phone in order to reboot
   it and make it read the configuration from the Elastix server (for the common 
   TFTP configuration case). Currently this operation is synchronous, so each
   phone of the list needs to wait until the previous phone has been contacted 
   in order to start configuration. This program aims to run the configuration 
   of all the phones in parallel, to save configuration time for hundreds of 
   phones.
2) Asynchronous operation: related to the first point. Due to the use of 
   ordinary blocking sockets, the configuration must wait for network I/O for a 
   single operation of a single phone, sequentially, for each phone. This 
   program tries to use non-blocking I/O for all network configuration.
3) Effective polymorphism: the current implementation tries to separate the 
   functions that deal with various phone vendors and models in separate files.
   However, the function dispatcher needs to know about the names of each of the
   functions, and therefore needs to know about the names of every single 
   vendor. Additionally the function dispatches encodes vendor-specific 
   knowledge for some phone models. This spreads out knowledge of the vendors 
   and models in several places and makes it more difficult to implement a new 
   vendor or model properly. This program uses inheritance properly so that only
   the implementation class knows about a particular phone vendor or model. 
   Ideally, implementing a new vendor should just involve adding a new class 
   file in /usr/share/elastix/endpoint-vendors with no changes to the core 
   program.
4) Template-based configuration: currently most, if not all implementations of
   phone configurators replace a few variables in a largely-static configuration
   file. This program takes this to the logical conclusion and uses Smarty 
   templates as much as possible, in order to simplify the file generation.

For each endpoint to configure, the following needs to take place.
- Some external agent, most commonly the web interface, fills the tables in
  /var/www/db/endpoint.db with the information about the extension to associate
  with the endpoint. It is assumed that each endpoint is an Ethernet device and
  has an unique MAC that will identify the phone in all the following steps.
- This program receives a file with the list of MACs and IPv4s of all phones to 
  configure via the --maclistfile parameter, separated by a single space. Each 
  MAC is in the format: aa:bb:cc:dd:ee:ff. Each IPv4 is in the format 
  aa.bb.cc.dd .
- The MAC is looked up in endpoint.db to get the associated extension, and the
  specified vendor and model. An instance of the EndpointVENDOR class is 
  created, loading the class if required.
- The program queries Asterisk for the IPs of all of the registered extensions,
  with 'sip show peers' and 'iax2 show peers'.
- The EndpointVENDOR instance is initialized with the chosen model, extension,
  and additional phone information read from both the endpoint.db file and the
  'asterisk' database from FreePBX. Additionally, if the endpoint has an IPv4 
  address that matches one of the registered IPs, then the 'registered' flag 
  will be set on the EndpointVENDOR instance.
- Each EndpointVENDOR must receive a reference to the MultiplexServer in order
  to be able to create asynchronous connections. Additionally each 
  EndpointVENDOR exposes a read-only property localConfigStatus that can have
  the values ENDPOINT_INPROGRESS, ENDPOINT_SUCCESS, ENDPOINT_FAILURE.
- The chosen vendor and model may require system-wide configuration files to be
  created at /tftpboot, and therefore the static method 
  EndpointVENDOR::updateGlobalConfig() is called for each vendor class. It is not
  expected that this method requires network I/O, and therefore this step should
  be synchronous.
- For each EndpointVENDOR, the method updateLocalConfig() is invoked. This method
  returns nothing. The localConfigStatus property gets updated to either
  ENDPOINT_SUCCESS if the configuration can complete synchronously, or
  ENDPOINT_INPROGRESS if one or more asynchronous clients are started. The 
  asynchronous client(s) are added to the MultiplexServer. Each async client
  has a reference to the EndpointVENDOR that started it.
- The MultiplexServer message pump is run regularly. Each async client will get
  its procesarPaquete() method invoked. When apropriate, this method will invoke
  the updateLocalConfig() method of the parent EndpointVENDOR. This should
  cause the EndpointVENDOR to fetch results from the async client, and the state
  machine to be updated, also updating localConfigStatus when required.
- After each MultiplexServer message loop, the EndpointVENDOR list is scanned.
  Any instance with localConfigStatus set to ENDPOINT_SUCCESS or 
  ENDPOINT_FAILURE is removed from the list. The loop terminates when the 
  EndpointVENDOR list becomes empty.

To achieve paralellism, a few network clients are provided: AsyncHTTPClient,
AsyncTelnetClient, AMIQueueClient. All of them require a reference to the 
endpoint instance that is interested in updating its state machine when some
activity happens. In particular, all implementations will instantiate an 
AMIQueueClient in order to send an unregister command if the endpoint was
already registered.

*******************************************************************************/

declare(ticks=1);
ini_set('memory_limit', -1);        // Allow the program to use all the memory it needs
ini_set('display_errors', 0);       // Prevent error in STDERR from being copied to STDOUT
ini_set('error_reporting', E_ALL & ~E_STRICT);  // Display warnings in addition to errors

// Add directory of endpoint implementations to search path
define('ENDPOINT_DIR', '/usr/share/elastix/endpoint-vendors');
ini_set('include_path', ENDPOINT_DIR.'/class:/var/www/html/libs/smarty/libs:'.ini_get('include_path'));

// Data URLs for various databases used in the program
define('DSN_ENDPOINT', 'sqlite:/var/www/db/endpoint.db');
define('FREEPBX_CONFIG', '/etc/amportal.conf');

// Auto-cargado de las clases en base al nombre de archivo
function __autoload($sNombreClase) {
    $sNombreBase = $sNombreClase.'.class.php';
    foreach (explode(':', ini_get('include_path')) as $sDirInclude) {
        if (file_exists($sDirInclude.'/'.$sNombreBase)) {
            require_once($sNombreBase);
            return;
        }
    }
}

require_once 'Console/Getopt.php';

load_default_timezone();

$goAppLog = NULL;
$opt = Console_Getopt::getopt($argv, '', array(
    'scan',             // Scan network for endpoints
    'applyconfig',      // Apply configuration changes from databases

    'netmask=',         // Network mask for nmap scan
    'maclistfile=',     // File with list of MACs to configure
    'progressfile=',    // File with progress report for configuration
));
if (PEAR::isError($opt)) error_exit($opt->getMessage()."\n");
validateOptions($opt);
foreach ($opt[0] as $option) switch ($option[0]) {
case '--scan':
    exit(action_scan($opt) ? 0 : 1);
case '--applyconfig':
    exit(action_applyconfig($opt) ? 0 : 1);
}
error_exit("No action specified (--scan or --applyconfig)\n");

function error_exit($sMsg, $errorcode = 1)
{
    fwrite(STDERR, $sMsg);
    exit($errorcode);
}

function load_default_timezone()
{
    $sDefaultTimezone = @date_default_timezone_get();
    if ($sDefaultTimezone == 'UTC') {
        $sDefaultTimezone = 'America/New_York';
        if (file_exists('/etc/sysconfig/clock')) {
            foreach (file('/etc/sysconfig/clock') as $s) {
                $regs = NULL;
                if (preg_match('/^ZONE\s*=\s*"(.+)"/', $s, $regs)) {
                    $sDefaultTimezone = $regs[1];
                }
            }
        }
    }
    date_default_timezone_set($sDefaultTimezone);
}

// Parse and validate known command-line options
function validateOptions($opt)
{
    foreach ($opt[0] as $option) switch ($option[0]) {
    case '--netmask':
        $regs = NULL;
        if (!preg_match('|^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})$|', $option[1], $regs))
            error_exit('Option '.$option[0].": Invalid IP netmask\n");
        for ($i = 1; $i <= 4; $i++) if ($regs[$i] > 255)
            error_exit('Option '.$option[0].": Invalid IP address\n");
        if ($regs[5] > 32)
            error_exit('Option '.$option[0].": Invalid IP netmask bits\n");
        break;
    }
}

function action_scan($opt)
{
    error_exit("FATAL: unimplemented\n");
}

function action_applyconfig($opt)
{
    global $goAppLog;
    $bSuccess = TRUE;

    $sInputFile = NULL;
    $sOutputFile = NULL;
    foreach ($opt[0] as $option) switch ($option[0]) {
    case '--maclistfile':
        $sInputFile = $option[1];
        break;
    case '--progressfile':
        $sOutputFile = $option[1];
        break;
    }

    if (is_null($sInputFile)) {
        fputs(STDERR, "FATAL: no --maclistfile specified\n");
    	return FALSE;
    }

    // Open logfile for progress
    $coreLog = NULL;
    $hLogFile = NULL;
    $hLogStream = NULL;
    if (!is_null($sOutputFile)) {
    	daemon(TRUE, FALSE);
        $hLogFile = fopen($sOutputFile, 'w');
        $hLogStream = $hLogFile;
    } else {
        $hLogStream = STDOUT;
    }
    $coreLog = new FileLogger($hLogStream, 'CORE');
    $goAppLog = $coreLog;
    $old_errorHandler = set_error_handler('daemonErrorHandler');
    
    // Load the MAC/IP pairs from input file
    $endpointList = array();
    if ($bSuccess) $bSuccess = loadEndpointMAC($coreLog, $endpointList, $sInputFile);

    // Load endpoint ID for each MAC into memory
    if ($bSuccess) $bSuccess = loadEndpointID($coreLog, $endpointList);
    
    // Load account information for each account
    if ($bSuccess) $bSuccess = loadEndpointAccountFPBX($coreLog, $endpointList);


    /*  
     *  At this point, each entry in $endpointList should have the following:
     *      MAC         (file)      Ethernet MAC for the device
     *      IP          (file)      IP address of the device
     *      id_device   (sqlite)    Device identifier on FreePBX
     *      vendor_name (sqlite)    Name of the vendor, used for endpoint class
     *      model_name  (sqlite)    Model name for vendor
     *      tech        (freepbx)   'sip' or 'iax2'
     *      description (freepbx)   Long name for the endpoint
     *      account     (freepbx)   Account name for the endpoint
     *      secret      (freepbx)   Password for the account
     *      parameters  (sqlite)    Extra configuration parameters for endpoint
     */

    // Create the object instances for the endpoint-specific configurators
    if ($bSuccess) {
        $serveriplist = listIpAddresses();
        
        $endpointClasses = array();
        $endpointToStart = array();
        foreach ($endpointList as $endpointInfo) {
        	$sEndpointClass = "Endpoint".str_replace('-', '', $endpointInfo['vendor_name']);
            if (!isset($endpointClasses[$endpointInfo['vendor_name']])) {
                if (!file_exists(ENDPOINT_DIR."/class/$sEndpointClass.class.php")) {
            		$bSuccess = FALSE;
                    $coreLog->output('ERR: unimplemented configurator for vendor: '.$endpointInfo['vendor_name']);
                    break;
            	}
                $endpointClasses[$endpointInfo['vendor_name']] = $sEndpointClass;
            }
            $endpoint = new $sEndpointClass(
                new FileLogger($hLogStream, "{$endpointInfo['vendor_name']}@{$endpointInfo['IP']}"),
                chooseServerIP($serveriplist, $endpointInfo['IP']),
                $endpointInfo['MAC'],
                $endpointInfo['IP'],
                $endpointInfo['id_device'],
                $endpointInfo['tech'],
                $endpointInfo['description'],
                $endpointInfo['account'],
                $endpointInfo['secret']);
            
            if (!$endpoint->setModel($endpointInfo['model_name'])) {
                $bSuccess = FALSE;
                $coreLog->output('ERR: vendor '.$endpointInfo['vendor_name'].
                    ' does not implement model: '.$endpointInfo['model_name']);
            	break;
            }
            
            if (!$endpoint->setExtraParameters($endpointInfo['parameters'])) {
                $bSuccess = FALSE;
                $coreLog->output('ERR: vendor '.$endpointInfo['vendor_name'].
                    ' model '.$endpointInfo['model_name'].' rejected parameters: '.
                    print_r($endpointInfo['parameters'], TRUE));
                break;
            }
            
            $endpointToStart[$endpointInfo['IP']] = $endpoint;
        }
    }

    // Connect to AMI and query all registered peers with known technologies
    if ($bSuccess) {
        $multiplex = new MultiplexServer(NULL, $coreLog);
        $ami = new AMIClientConn($multiplex, $coreLog);
        $amiconf = loadConfigManager($coreLog);
        if (is_null($amiconf)) {
    	   $bSuccess = FALSE;
        } elseif (!$ami->connect('127.0.0.1', $amiconf[0], $amiconf[1])) {
            $coreLog->output('ERR: unable to connect to Asterisk Manager Interface');
            $bSuccess = FALSE;
        } else {
            // Check current IPs registered as SIP
            $r = $ami->Command('sip show peers');
            foreach (explode("\n", $r['data']) as $s) {
                $l = preg_split('/\s+/', $s);
                // 1064/1064    192.168.3.1  D   N  A  5060  OK (13 ms)
                if (count($l) > 6 && preg_match('/^\d+\.\d+\.\d+\.\d+$/', $l[1])) {
                	$peerIP = $l[1];
                    if (isset($endpointToStart[$peerIP])) {
                        $extArray = explode('/', $l[0]);
                        $coreLog->output("INFO: SIP/$peerIP registered in Asterisk as extension {$extArray[0]}");
                        $endpointToStart[$peerIP]->registered = array('sip', $extArray[0]);
                    }
                }
            }
            
            // Check current IPs registered as IAX
            $r = $ami->Command('iax2 show peers');
            foreach (explode("\n", $r['data']) as $s) {
                $l = preg_split('/\s+/', $s);
                // 2002   127.0.0.1  (D)  255.255.255.255  40001    OK (1 ms)
                if (count($l) > 5 && preg_match('/^\d+\.\d+\.\d+\.\d+$/', $l[1]) && $l[5] == 'OK') {
                    $peerIP = $l[1];
                    if (isset($endpointToStart[$peerIP])) {
                        $coreLog->output("INFO: IAX2/$peerIP registered in Asterisk as extension {$l[0]}");
                        $endpointToStart[$peerIP]->registered = array('iax2', $l[0]);
                    }
                }
            }
        }
    }
    
    // Update global configuration for all the endpoint vendors
    if ($bSuccess) {
        $iTotalSteps = count($endpointClasses) + 2 * count($endpointToStart);
        $iCurrentStep = 1;
    
    	foreach ($endpointClasses as $endpointVendor => $endpointClass) {
    		$coreLog->output("INFO: ($iCurrentStep/$iTotalSteps) global configuration update for $endpointVendor...");
            $iCurrentStep++;
            eval("\$bSuccess = $endpointClass::updateGlobalConfig();");
            if (!$bSuccess) {
                $coreLog->output("ERR: global configuration update failed for $endpointVendor");
            	break;
            }
    	}
    }

    // Update local configuration for all the endpoints
    if ($bSuccess) {
        $endpointRunning = array();

        while (count($endpointToStart) > 0 || count($endpointRunning) > 0) {
            if ($multiplex->procesarPaquetes()) {
                // Some endpoint was busy receiving data
                $multiplex->procesarActividad(0);
            } else {
                // No endpoint received data, try starting one more endpoint
                if (count($endpointToStart) > 0) {
                	$endpoint = array_shift($endpointToStart);
                    $coreLog->output("INFO: ($iCurrentStep/$iTotalSteps) ".
                        "starting configuration for endpoint ".
                        "{$endpoint->vendor_name}@{$endpoint->ip}...");
                    $iCurrentStep++;
                    $endpoint->multiplex = $multiplex;
                    $endpoint->ami = $ami;
                    $endpoint->updateLocalConfig(NULL, FALSE);
                    $endpointRunning[] = $endpoint;
                }                
            	$multiplex->procesarActividad(0.1);
            }
            
            foreach (array_keys($endpointRunning) as $k) {
            	if ($endpointRunning[$k]->localConfigStatus == ENDPOINT_SUCCESS) {
            		$coreLog->output("INFO: ($iCurrentStep/$iTotalSteps) ".
                        "finished configuration for endpoint ".
                        "{$endpoint->vendor_name}@{$endpoint->ip}...");
                    $iCurrentStep++;
                    unset($endpointRunning[$k]);
            	} elseif ($endpointRunning[$k]->localConfigStatus == ENDPOINT_FAILURE) {
                    $coreLog->output("WARN: ($iCurrentStep/$iTotalSteps) ".
                        "failed configuration for endpoint ".
                        "{$endpoint->vendor_name}@{$endpoint->ip}...");
                    $iCurrentStep++;
                    unset($endpointRunning[$k]);
            	}
            }
        }
        
        $ami->disconnect();
    }

    return $bSuccess;
}

function loadEndpointMAC($coreLog, &$endpointList, $sInputFile)
{
	$bSuccess = TRUE;
    
    $coreLog->output('INFO: Loading MACs from file '.$sInputFile.' ...');
    $hMacFile = fopen($sInputFile, 'r');
    if (!$hMacFile) {
        $coreLog->output('ERR: unable to open file for reading');
        return FALSE;
    }

    $endpointList = array();
    $iLineCount = 1;
    while ($bSuccess && $row = fgetcsv($hMacFile, 4096, ' ')) {
        $regs = NULL;
        if (count($row) < 2) {
            $coreLog->output("ERR: line $iLineCount: not enough fields, expected at least 2");
            $bSuccess = FALSE;
        } elseif (!preg_match('/^([[:xdigit:]]{2}(:[[:xdigit:]]{2}){5})$/', $row[0])) {
            $coreLog->output("ERR: line $iLineCount: field 1: invalid MAC address");
            $bSuccess = FALSE;
        } elseif (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $row[1], $regs)) {
            $coreLog->output("ERR: line $iLineCount: field 2: invalid IPv4 address");
            $bSuccess = FALSE;
        } else {
            for ($i = 1; $i <= 4; $i++) if ($regs[$i] > 255) $bSuccess = FALSE;
            if (!$bSuccess) {
                $coreLog->output("ERR: line $iLineCount: field 2: invalid IPv4 address");
            } else {
                $endpointList[] = array(
                    'MAC'   =>  $row[0],
                    'IP'    =>  $row[1]
                );
            }
        }
        $iLineCount++;
    }
    fclose($hMacFile);

    if ($bSuccess) {
        if (count($endpointList) <= 0) {
            $coreLog->output("ERR: no endpoints to configure, expected at least 1");
            $bSuccess = FALSE;
        } else {
            $coreLog->output("INFO: loaded ".count($endpointList)." endpoints");
        }
    }
    
    return $bSuccess;
}

function loadEndpointID($coreLog, &$endpointList)
{
    $coreLog->output("INFO: loading endpoint information from DSN_ENDPOINT");
    try {
        $conn = new PDO(DSN_ENDPOINT);
        $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $sth = $conn->prepare(
            'SELECT endpoint.id, endpoint.id_device, vendor.name AS '.
                'vendor_name, model.name AS model_name '.
            'FROM endpoint, vendor, model '.
            'WHERE endpoint.id_vendor = vendor.id '.
                'AND endpoint.id_model = model.id '.
                'AND endpoint.mac_adress = ?');
        $sthParam = $conn->prepare(
            'SELECT name, value FROM parameter WHERE id_endpoint = ?');
        for ($i = 0; $i < count($endpointList); $i++) {
            $sth->execute(array($endpointList[$i]['MAC']));
            $row = $sth->fetch(PDO::FETCH_ASSOC);
            $sth->closeCursor();
            if (!$row) {
                $coreLog->output("ERR: MAC ".$endpointList[$i]['MAC']." not found in endpoint database");
                return FALSE;
            }
            $endpointList[$i]['id_device'] = (trim($row['id_device']) != '') ? $row['id_device'] : NULL;
            $endpointList[$i]['vendor_name'] = $row['vendor_name'];
            $endpointList[$i]['model_name'] = $row['model_name'];
            $endpointList[$i]['parameters'] = array();
            
            $sthParam->execute(array($row['id']));
            $sthParam->setFetchMode(PDO::FETCH_ASSOC);
            foreach ($sthParam as $rowParam) {
                $endpointList[$i]['parameters'][$rowParam['name']] = $rowParam['value'];
            }
        }
    } catch (PDOException $e) {
        $coreLog->output("ERR: failed to query account for MAC - ".$e->getMessage());
        return FALSE;
    }

    return TRUE;
}

function loadEndpointAccountFPBX($coreLog, &$endpointList)
{
    $bSuccess = TRUE;

    $coreLog->output("INFO: loading FreePBX database connection parameters from ".FREEPBX_CONFIG.' ...');
    
    /* Tests show that parse_ini_file is unable to parse /etc/amportal.conf,
     * so it must be opened directly. */
    $dbParams = array();
    $hConfig = fopen(FREEPBX_CONFIG, 'r');
    if (!$hConfig) {
        $coreLog->output('ERR: unable to open file '.FREEPBX_CONFIG.' for reading of FreePBX parameters.');
        return FALSE;
    }
    while ($bSuccess && !feof($hConfig)) {
        $sLinea = fgets($hConfig);
        if ($sLinea === FALSE) break;
        $sLinea = trim($sLinea);
        if ($sLinea == '') continue;
        if ($sLinea{0} == '#') continue;
        
        $regs = NULL;
        if (preg_match('/^([[:alpha:]]+)[[:space:]]*=[[:space:]]*(.*)$/', $sLinea, $regs)) switch ($regs[1]) {
        case 'AMPDBHOST':
        case 'AMPDBUSER':
        case 'AMPDBENGINE':
        case 'AMPDBPASS':
            $dbParams[$regs[1]] = $regs[2];
            break;
        }
    }
    if ($hConfig) fclose($hConfig);
    if (!$bSuccess) return FALSE;
    
    $coreLog->output("INFO: loading account information from FreePBX ...");
    
    // Open database connection after collecting all parameters
    if (count($dbParams) < 4) {
        $coreLog->output('ERR: file '.FREEPBX_CONFIG.
            ' does not have all required parameters for connection');
        return FALSE;
    }
    if ($dbParams['AMPDBENGINE'] != 'mysql' && $dbParams['AMPDBENGINE'] != 'mysqli') {
        $coreLog->output('ERR: file '.FREEPBX_CONFIG.
            ' specifies untest AMPDBENGINE='.$dbParams['AMPDBENGINE']);
        return FALSE;
    }
    try {
        $conn = new PDO(
            "mysql:host={$dbParams['AMPDBHOST']};dbname=asterisk", 
            $dbParams['AMPDBUSER'], $dbParams['AMPDBPASS']);
        $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $sth = $conn->prepare('SELECT tech, description FROM devices WHERE id = ?');
        $tableTech = array(
            'iax2'  =>  'iax',
            'sip'   =>  'sip',
        );
        $sthTech = array();
        foreach ($tableTech as $sTech => $sTable) {
        	$sthTech[$sTech] = $conn->prepare(
                "SELECT keyword, data FROM $sTable ".
                "WHERE id = ? AND (keyword = 'account' OR keyword = 'secret')");
        }

        for ($i = 0; $i < count($endpointList); $i++) {
            if (is_null($endpointList[$i]['id_device'])) {
            	/* This device is not linked to an Asterisk account (Patton) */
                $endpointList[$i]['tech'] = NULL;
                $endpointList[$i]['description'] = NULL;
                $endpointList[$i]['account'] = NULL;
                $endpointList[$i]['secret'] = NULL;
            } else {
                $sth->execute(array($endpointList[$i]['id_device']));
                $row = $sth->fetch(PDO::FETCH_ASSOC);
                $sth->closeCursor();
                if (!$row) {
                    $coreLog->output("ERR: cannot find device information for ".$endpointList[$i]['id_device']);
                    return FALSE;
                }
                $endpointList[$i]['tech'] = $row['tech'];
                $endpointList[$i]['description'] = $row['description'];
                if (!isset($sthTech[$row['tech']])) {
                    $coreLog->output("ERR: unimplemented tech ".$row['tech']." for device ".$endpointList[$i]['id_device']);
                    return FALSE;
                }
                $sthTech[$row['tech']]->execute(array($endpointList[$i]['id_device']));
                $sthTech[$row['tech']]->setFetchMode(PDO::FETCH_ASSOC);
                foreach ($sthTech[$row['tech']] as $rowTech) {
                    $endpointList[$i][$rowTech['keyword']] = $rowTech['data'];
                }
            }
        }
    } catch (PDOException $e) {
        $coreLog->output("ERR: failed to query account information - ".$e->getMessage());
        return FALSE;
    }

	return TRUE;
}

// Implementación de daemon() usando pcntl_fork()
function daemon($nochdir, $noclose)
{
    $iPid = pcntl_fork();
    if ($iPid != -1) {
        if ($iPid != 0) exit(0);    // Terminar el proceso padre
        if (!$noclose) {
            fclose(STDIN);
            fclose(STDOUT);
        }
        if (posix_setsid() >= 0) {
            if (!$nochdir) chdir('/');
            return 0;
        } else {
            return -1;
        }
    } else {
        return -1;
    }
}

function loadConfigManager($log)
{
    $sNombreArchivo = '/etc/asterisk/manager.conf';
    if (!file_exists($sNombreArchivo)) {
        $log->output("WARN: $sNombreArchivo no se encuentra.");
        return NULL;
    }
    if (!is_readable($sNombreArchivo)) {
        $log->output("WARN: $sNombreArchivo no puede leerse por usuario de marcador.");
        return NULL;            
    }

    $infoConfig = parse_ini_file_literal($sNombreArchivo);
    if (is_array($infoConfig)) {
        foreach ($infoConfig as $login => $infoLogin) {
            if ($login != 'general') {
                if (isset($infoLogin['secret']) && 
                    isset($infoLogin['read']) && 
                    isset($infoLogin['write'])) {
                    return array($login, $infoLogin['secret']);
                }
            }
        }
    } else {
        $log->output("ERR: $sNombreArchivo no puede parsearse correctamente.");         
    }
    return NULL;
}


function parse_ini_file_literal($sNombreArchivo)
{
    $h = fopen($sNombreArchivo, 'r');
    if (!$h) return FALSE;
    $r = array();
    $seccion = NULL;
    while (!feof($h)) {
        $s = fgets($h);
        $s = rtrim($s, " \r\n");
        $regs = NULL;
        if (preg_match('/^\s*\[(\w+)\]/', $s, $regs)) {
            $seccion = $regs[1];
        } elseif (preg_match('/^(\w+)\s*=\s*(.*)/', $s, $regs)) {
            if (is_null($seccion))
                $r[$regs[1]] = $regs[2];
            else
                $r[$seccion][$regs[1]] = $regs[2];
        }
    }
    fclose($h);
    return $r;
}

// Procedimiento que usa el log abierto para reportar el error que haya ocurrido
function daemonErrorHandler($errno, $errmsg, $filename, $linenum, $vars)
{
    global $goAppLog;
    $errortype = array (
        E_ERROR              => 'Error',
        E_WARNING            => 'Warning',
        E_PARSE              => 'Parsing Error',
        E_NOTICE             => 'Notice',
        E_CORE_ERROR         => 'Core Error',
        E_CORE_WARNING       => 'Core Warning',
        E_COMPILE_ERROR      => 'Compile Error',
        E_COMPILE_WARNING    => 'Compile Warning',
        E_USER_ERROR         => 'User Error',
        E_USER_WARNING       => 'User Warning',
        E_USER_NOTICE        => 'User Notice',
        E_STRICT             => 'Runtime Notice',
//        E_RECOVERABLE_ERROR  => 'Catchable Fatal Error'
        );
    if (!is_null($goAppLog) && $errno != E_STRICT) {
        $goAppLog->output("$errortype[$errno]: $filename line $linenum - $errmsg");
    }
}

// List IPv4/Netmask for all Ethernet interfaces
function listIpAddresses()
{
    $iflist = array(); $if = NULL;
    $output = NULL;
    exec('/sbin/ip addr show', $output);
    $bIsEther = FALSE;
    $ip = NULL;
    foreach ($output as $s) {
        $regs = NULL;
        if (preg_match('/^\d+:\s+(\w+):\s*<(.*)>/', $s, $regs)) {
            $if = $regs[1];
            $bIsEther = FALSE;
        } elseif (strpos($s, 'link/ether') !== FALSE) {
            $bIsEther = TRUE;
        } elseif (preg_match('|\s*inet (\d+\.\d+\.\d+.\d+)/(\d+) brd (\d+\.\d+\.\d+.\d+).+\s((\w+)(:(\d+))?)\s*$|', trim($s), $regs)) {
            // Calculate netmask from number of bits
            $iMaskBits = $regs[2];
            $iMask = (0xFFFFFFFF << (32 - $iMaskBits)) & 0xFFFFFFFF;

            // Calculate network as binary value
            $sIpAddr = $regs[1];
            $ip = explode('.', $sIpAddr);
            $ipval = ($ip[0] << 24) | ($ip[1] << 16) | ($ip[2] << 8) | ($ip[3]);
        	$iflist[$sIpAddr] = array(
                'netmask'   =>  $iMask,
                'network'   =>  ($ipval & $iMask),
            );
        }
    }
    return $iflist;
}

// Choose a server IP whose network matches the one of the endpoint
function chooseServerIP($iplist, $endpointIp)
{
	$defaultip = NULL;
    $ip = explode('.', $endpointIp);
    $ipval = ($ip[0] << 24) | ($ip[1] << 16) | ($ip[2] << 8) | ($ip[3]);
    foreach ($iplist as $serverip => $netinfo) {
        if (is_null($defaultip)) $defaultip = $serverip;
    	if (($ipval & $netinfo['netmask']) == $netinfo['network'])
            return $serverip;
    }
    return $defaultip;
}
?>