TFTP read caused a hang if no server available Set to DONTWAIT and loop and then check for valid data Use metadat to set system defaults
526 lines
20 KiB
PHP
526 lines
20 KiB
PHP
<?php
|
|
|
|
/**
|
|
*
|
|
* Core Comsnd Interface
|
|
*
|
|
* https://www.voip-info.org/asterisk-manager-example-php/
|
|
*/
|
|
|
|
namespace FreePBX\modules\Sccp_manager;
|
|
|
|
class aminterface
|
|
{
|
|
|
|
var $_socket;
|
|
var $_error;
|
|
var $_config;
|
|
var $_test;
|
|
private $_connect_state;
|
|
private $_lastActionClass;
|
|
private $_lastActionId;
|
|
private $_lastRequestedResponseHandler;
|
|
private $_ProcessingMessage;
|
|
private $_DumpMessage;
|
|
private $debug_level = 1;
|
|
private $_incomingRawMessage;
|
|
private $eventListEndEvent;
|
|
|
|
public function load_subspace($parent_class = null)
|
|
{
|
|
$driverNamespace = "\\FreePBX\\Modules\\Sccp_manager\\aminterface";
|
|
|
|
$drivers = array('Message' => 'Message.class.php',
|
|
'Response' => 'Response.class.php',
|
|
'Event' => 'Event.class.php'
|
|
);
|
|
foreach ($drivers as $key => $value) {
|
|
$class = $driverNamespace . "\\" . $key;
|
|
$driver = __DIR__ . "/amInterfaceClasses/" . $value;
|
|
if (!class_exists($class, false)) {
|
|
if (file_exists($driver)) {
|
|
include(__DIR__ . "/amInterfaceClasses/" . $value);
|
|
} else {
|
|
throw new \Exception("Class required but file not found " . $driver);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public function __construct($parent_class = null)
|
|
{
|
|
global $amp_conf;
|
|
$this->paren_class = $parent_class;
|
|
$this->_socket = false;
|
|
$this->_connect_state = false;
|
|
$this->_error = array();
|
|
$this->_config = array('host' => 'localhost',
|
|
'user' => '',
|
|
'pass' => '',
|
|
'port' => '5038',
|
|
'tsoket' => 'tcp://',
|
|
'timeout' => 30,
|
|
'enabled' => true
|
|
);
|
|
$this->_eventListeners = array();
|
|
$this->_incomingMsgObjectList = array();
|
|
$this->_lastActionId = false;
|
|
$this->_incomingRawMessage = array();
|
|
$this->eventListEndEvent = '';
|
|
|
|
$fld_conf = array('user' => 'AMPMGRUSER', 'pass' => 'AMPMGRPASS');
|
|
if (isset($amp_conf['AMPMGRUSER'])) {
|
|
foreach ($fld_conf as $key => $value) {
|
|
if (isset($amp_conf[$value])) {
|
|
$this->_config[$key] = $amp_conf[$value];
|
|
}
|
|
}
|
|
}
|
|
if ($this->_config['enabled']) {
|
|
$this->load_subspace();
|
|
}
|
|
|
|
if ($this->_config['enabled']) {
|
|
// Ami is not hard disabled in __construct line 63.
|
|
if ($this->open()) {
|
|
// Can open a connection. Now check compatibility with chan-sccp.
|
|
// will return true if compatible.
|
|
if (!$this->get_compatible_sccp(true)[1]) {
|
|
// Close the open socket as will not use
|
|
$this->close();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public function info()
|
|
{
|
|
$Ver = '13.0.4';
|
|
if ($this->_config['enabled']){
|
|
return array('Version' => $Ver,
|
|
'about' => 'AMI data ver: ' . $Ver, 'test' => get_declared_classes());
|
|
} else {
|
|
return array('Version' => $Ver,
|
|
'about' => 'Disabled AMI ver: ' . $Ver);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Opens a socket connection to ami.
|
|
*/
|
|
public function open()
|
|
{
|
|
$cString = $this->_config['tsoket'] . $this->_config['host'] . ':' . $this->_config['port'];
|
|
$this->_context = stream_context_create();
|
|
$errno = 0;
|
|
$errstr = '';
|
|
$this->_ProcessingMessage = '';
|
|
$this->_socket = @stream_socket_client(
|
|
$cString,
|
|
$errno,
|
|
$errstr,
|
|
$this->_config['timeout'],
|
|
STREAM_CLIENT_CONNECT,
|
|
$this->_context
|
|
);
|
|
if ($this->_socket === false) {
|
|
$this->_errorException('Error connecting to ami: ' . $errstr . $cString);
|
|
return false;
|
|
}
|
|
$msg = new aminterface\LoginAction($this->_config['user'], $this->_config['pass']);
|
|
$response = $this->send($msg);
|
|
|
|
if ($response != false) {
|
|
if (!$response->isSuccess()) {
|
|
$this->_errorException('Could not connect: ' . $response->getMessage());
|
|
return false;
|
|
} else {
|
|
@stream_set_blocking($this->_socket, 0);
|
|
$this->_connect_state = true;
|
|
$this->_ProcessingMessage = '';
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Closes the connection to ami.
|
|
*/
|
|
public function close()
|
|
{
|
|
$this->_connect_state = false;
|
|
$this->_ProcessingMessage = '';
|
|
@stream_socket_shutdown($this->_socket, STREAM_SHUT_RDWR);
|
|
}
|
|
/*
|
|
* Send action message to ami, and wait for Response
|
|
*/
|
|
public function send($message)
|
|
{
|
|
$_incomingRawMessage = array();
|
|
$messageToSend = $message->serialize();
|
|
$length = strlen($messageToSend);
|
|
$this->_DumpMessage = '';
|
|
$this->_lastActionId = $message->getActionID();
|
|
$this->_lastRequestedResponseHandler = $message->getResponseHandler();
|
|
$this->_lastActionClass = $message;
|
|
$this->_incomingRawMessage[$this->_lastActionId] = '';
|
|
$this->eventListIsCompleted = array();
|
|
if (@fwrite($this->_socket, $messageToSend) < $length) {
|
|
$this->_errorException('Could not send message');
|
|
return false;
|
|
}
|
|
// Have sent a message and now have to wait for and read the reply
|
|
// The below infinite loop waits for $this->completed to be true.
|
|
// The loop calls readBuffer, which calls GetMessages, which calls Process
|
|
// This loop then continues until we have _thisComplete as an object variable
|
|
$this->eventListIsCompleted[$this->_lastActionId] = false;
|
|
while (true) {
|
|
stream_set_timeout($this->_socket, 1);
|
|
$this->readBuffer();
|
|
$info = stream_get_meta_data($this->_socket);
|
|
if ($info['timed_out'] == true) {
|
|
$this->_errorException("Read waittime: " . ($this->socket_param['timeout']) . " exceeded (timeout).\n");
|
|
return false;
|
|
}
|
|
if ($this->eventListIsCompleted[$this->_lastActionId]) {
|
|
$response = $this->_incomingMsgObjectList[$this->_lastActionId];
|
|
// need to test that the list was successfully completed here
|
|
$allReceived = $response->getClosingEvent()
|
|
->listCorrectlyReceived($this->_incomingRawMessage[$this->_lastActionId],
|
|
$response->getCountOfEvents());
|
|
// now tidy up removing any temp variables or objects
|
|
$response->removeClosingEvent();
|
|
unset($_incomingRawMessage[$this->_lastActionId]);
|
|
unset($this->_incomingMsgObjectList[$this->_lastActionId]);
|
|
unset($this->_lastActionId);
|
|
if ($allReceived) {
|
|
return $response;
|
|
}
|
|
// Something is missing from the events list received via AMI, or
|
|
// the control parameter at the end of the list has changed.
|
|
// This will cause an exception as returning a boolean instead of a Response
|
|
// Maybe should handle better, but
|
|
// need to break out of the loop as nothing more coming.
|
|
try {
|
|
throw new \invalidArgumentException("Counts do not match on returned AMI Result");
|
|
} catch ( \invalidArgumentException $e) {
|
|
echo substr(strrchr(get_class($response), '\\'), 1), " ", $e->getMessage(), "\n";
|
|
}
|
|
return $response;
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function readBuffer ()
|
|
{
|
|
$read = @fread($this->_socket, 65535);
|
|
// AMI never returns EOF
|
|
if ($read === false ) {
|
|
$this->_errorException('Error reading');
|
|
}
|
|
// Do not return empty Messages
|
|
while ($read == "" ) {
|
|
$read = @fread($this->_socket, 65535);
|
|
}
|
|
// Add read to the rest of buffer from previous read
|
|
$this->_ProcessingMessage .= $read;
|
|
$this->getMessages();
|
|
}
|
|
|
|
protected function getMessages()
|
|
{
|
|
$msgs = array();
|
|
// Extract any complete messages and leave remainder for next read
|
|
while (($marker = strpos($this->_ProcessingMessage, aminterface\Message::EOM))) {
|
|
$msg = substr($this->_ProcessingMessage, 0, $marker);
|
|
$this->_ProcessingMessage = substr(
|
|
$this->_ProcessingMessage,
|
|
$marker + strlen(aminterface\Message::EOM)
|
|
);
|
|
$msgs[] = $msg;
|
|
}
|
|
$this->process($msgs);
|
|
}
|
|
|
|
public function process(array $msgs)
|
|
{
|
|
foreach ($msgs as $aMsg) {
|
|
// 2 types of message; Response or Event. Response only incudes data
|
|
// for JSON response and Command response. All other responses expect
|
|
// data in an event list - these events need to be attached to the response.
|
|
$resPos = strpos($aMsg, 'Response: '); // Have a Response message. This may not be 0.
|
|
$evePos = strpos($aMsg, 'Event: '); // Have an Event Message. This should always be 0.
|
|
// Add the incoming message to a string that can be checked
|
|
// against the completed message event when it is received.
|
|
$this->_incomingRawMessage[$this->_lastActionId] .= "\r\n\r\n" . $aMsg;
|
|
if (($resPos !== false) && (($resPos < $evePos) || $evePos === false)) {
|
|
$response = $this->_responseObjFromMsg($aMsg); // resp Ok
|
|
$this->eventListEndEvent = $response->getKey('eventlistendevent');
|
|
$this->_incomingMsgObjectList[$this->_lastActionId] = $response;
|
|
$this->eventListIsCompleted[$this->_lastActionId] = $response->isComplete();
|
|
} elseif ($evePos === 0) { // Event must be at the start of the msg.
|
|
$event = $this->_eventObjFromMsg($aMsg); // Event Ok
|
|
$this->eventListIsCompleted[$this->_lastActionId] = $event->isComplete();
|
|
$this->_incomingMsgObjectList[$this->_lastActionId]->addEvent($event);
|
|
} else {
|
|
// broken ami most probably through changes in chan_sccp_b.
|
|
// AMI is sending a message which is neither a response nor an event.
|
|
$this->_msgToDebug(1, 'resp broken ami');
|
|
$bMsg = 'Event: ResponseEvent' . "\r\n";
|
|
$bMsg .= 'ActionId: ' . $this->_lastActionId . "\r\n" . $aMsg;
|
|
$event = $this->_eventObjFromMsg($bMsg);
|
|
$this->_incomingMsgObjectList[$this->_lastActionId]->addEvent($event);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function _msgToDebug($level, $msg)
|
|
{
|
|
if ($level > $this->debug_level) {
|
|
return;
|
|
}
|
|
print_r('<br> level: '.$level.' ');
|
|
print_r($msg);
|
|
print_r('<br>');
|
|
}
|
|
|
|
private function _responseObjFromMsg($message)
|
|
{
|
|
$_className = false;
|
|
|
|
$responseClass = '\\FreePBX\\modules\\Sccp_manager\\aminterface\\Generic_Response';
|
|
if ($this->_lastRequestedResponseHandler != false) {
|
|
$_className = '\\FreePBX\\modules\\Sccp_manager\\aminterface\\' . $this->_lastRequestedResponseHandler . '_Response';
|
|
}
|
|
if ($_className) {
|
|
if (class_exists($_className, true)) {
|
|
$responseClass = $_className;
|
|
} elseif ($responseHandler != false) {
|
|
$this->_errorException('Response Class ' . $_className . ' requested via responseHandler, could not be found');
|
|
}
|
|
}
|
|
$response = new $responseClass($message);
|
|
$actionId = $response->getActionID();
|
|
if ($actionId === null) {
|
|
$response->setActionId($this->_lastActionId);
|
|
}
|
|
return $response;
|
|
}
|
|
public function _eventObjFromMsg($message)
|
|
{
|
|
$eventType = explode(aminterface\Message::EOL,$message,2);
|
|
$name = trim(explode(':',$eventType[0],2)[1]);
|
|
$className = '\\FreePBX\\modules\\Sccp_manager\\aminterface\\' . $name . '_Event';
|
|
if (class_exists($className, true) === false) {
|
|
$className = '\\FreePBX\\modules\\Sccp_manager\\aminterface\\UnknownEvent';
|
|
}
|
|
return new $className($message);
|
|
}
|
|
|
|
protected function dispatch($message)
|
|
{
|
|
print_r("<br>------------dispatch----------<br>");
|
|
print_r($message);
|
|
return false;
|
|
die();
|
|
foreach ($this->_eventListeners as $data) {
|
|
$listener = $data[0];
|
|
$predicate = $data[1];
|
|
print_r($data);
|
|
|
|
if (is_callable($predicate) && !call_user_func($predicate, $message)) {
|
|
continue;
|
|
}
|
|
if ($listener instanceof \Closure) {
|
|
$listener($message);
|
|
} elseif (is_array($listener)) {
|
|
$listener[0]->$listener[1]($message);
|
|
} else {
|
|
$listener->handle($message);
|
|
}
|
|
}
|
|
print_r("<br>------------E dispatch----------<br>");
|
|
}
|
|
//-------------------Adaptive Function ------------------------------------------------------------
|
|
|
|
function core_list_hints()
|
|
{
|
|
$result = array();
|
|
if ($this->_connect_state) {
|
|
$_action = new \FreePBX\modules\Sccp_manager\aminterface\ExtensionStateListAction();
|
|
$_response = $this->send($_action);
|
|
$_res = $_response->getResult();
|
|
foreach ($_res as $key => $value) {
|
|
foreach ($value as $key2 => $value2) {
|
|
$result[$key2] = '@' . $key2;
|
|
}
|
|
}
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
function core_list_all_hints()
|
|
{
|
|
$result = array();
|
|
if ($this->_connect_state) {
|
|
$_action = new \FreePBX\modules\Sccp_manager\aminterface\ExtensionStateListAction();
|
|
$_res = $this->send($_action)->getResult();
|
|
foreach ($_res as $key => $value) {
|
|
foreach ($value as $key2 => $value2) {
|
|
$result[$key.'@'.$key2] = $key.'@'.$key2;
|
|
}
|
|
}
|
|
}
|
|
return $result;
|
|
}
|
|
// --------------------- SCCP Comands
|
|
function sccp_list_keysets()
|
|
{
|
|
$result = array();
|
|
if ($this->_connect_state) {
|
|
$_action = new \FreePBX\modules\Sccp_manager\aminterface\SCCPShowSoftkeySetsAction();
|
|
$_res = $this->send($_action)->getResult();
|
|
foreach ($_res as $key => $value) {
|
|
$result[$key] = $key;
|
|
}
|
|
}
|
|
return $result;
|
|
}
|
|
function sccp_get_active_device()
|
|
{
|
|
$result = array();
|
|
if ($this->_connect_state) {
|
|
$_action = new \FreePBX\modules\Sccp_manager\aminterface\SCCPShowDevicesAction();
|
|
$result = $this->send($_action)->getResult();
|
|
}
|
|
return $result;
|
|
}
|
|
function sccp_getdevice_info($devicename)
|
|
{
|
|
$result = array();
|
|
if ($this->_connect_state) {
|
|
$_action = new \FreePBX\modules\Sccp_manager\aminterface\SCCPShowDeviceAction($devicename);
|
|
$result = $this->send($_action)->getResult();
|
|
$result['MAC_Address'] = $result['macaddress'];
|
|
}
|
|
return $result;
|
|
}
|
|
function sccpDeviceReset($devicename, $action = '')
|
|
{
|
|
if ($this->_connect_state) {
|
|
if ($action == 'tokenack') {
|
|
$_action = new \FreePBX\modules\Sccp_manager\aminterface\SCCPTokenAckAction($devicename);
|
|
} else {
|
|
$_action = new \FreePBX\modules\Sccp_manager\aminterface\SCCPDeviceRestartAction($devicename, $action);
|
|
}
|
|
$_response = $this->send($_action);
|
|
$result['data'] = 'Device: '.$devicename.' Result: '.$_response->getMessage();
|
|
$result['Response']=$_response->getKey('Response');
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
//------------------- Core Comands ----
|
|
function core_sccp_reload()
|
|
{
|
|
$result = array();
|
|
if ($this->_connect_state) {
|
|
$_action = new \FreePBX\modules\Sccp_manager\aminterface\ReloadAction('chan_sccp');
|
|
$result = ['Response' => $this->send($_action)->getMessage(), 'data' => ''];
|
|
}
|
|
return $result;
|
|
}
|
|
function getSCCPConfigMetaData($segment = '') {
|
|
if ($this->_connect_state) {
|
|
$_action = new \FreePBX\modules\Sccp_manager\aminterface\SCCPConfigMetaDataAction($segment);
|
|
$metadata = $this->send($_action)->getResult();
|
|
}
|
|
return $metadata;
|
|
}
|
|
|
|
function getSCCPVersion()
|
|
{
|
|
//Initialise result array
|
|
$result = array( 'RevisionHash' => '', 'vCode' => 0, 'RevisionNum' => 0, 'futures' => '', 'Version' => 0);
|
|
$metadata = $this->getSCCPConfigMetaData();
|
|
|
|
if (isset($metadata['Version'])) {
|
|
$result['Version'] = $metadata['Version'];
|
|
$version_parts = explode('.', $metadata['Version']);
|
|
if ($version_parts[0] === 4) {
|
|
switch ($version_parts[1]) {
|
|
case 1:
|
|
$result['vCode'] = 410;
|
|
break;
|
|
case 2:
|
|
$result['vCode'] = 420;
|
|
break;
|
|
case 3. . .5:
|
|
$result['vCode'] = 430;
|
|
if($version_parts[2] == 3){
|
|
$result['vCode'] = 433;
|
|
}
|
|
break;
|
|
default:
|
|
$result['vCode'] = 400;
|
|
break;
|
|
}
|
|
}
|
|
if (array_key_exists("RevisionHash", $metadata)) {
|
|
$result['RevisionHash'] = $metadata["RevisionHash"];
|
|
}
|
|
if (isset($metadata['RevisionNum'])) {
|
|
if ($metadata['RevisionNum'] >= 11063) { // new method, RevisionNum is incremental
|
|
$result['vCode'] = 433;
|
|
}
|
|
$result['RevisionNum'] = $metadata["RevisionNum"];
|
|
}
|
|
if (isset($metadata['ConfigureEnabled'])) {
|
|
$result['futures'] = implode(';', $metadata['ConfigureEnabled']);
|
|
}
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
function getRealTimeStatus()
|
|
{
|
|
// Initialise array with default values to eliminate testing later
|
|
$result = array();
|
|
$cmd_res = array();
|
|
$cmd_res = ['sccp' => ['message' => 'default value', 'realm' => '', 'status' => 'ERROR']];
|
|
if ($this->_connect_state) {
|
|
$_action = new \FreePBX\modules\Sccp_manager\aminterface\CommandAction('realtime mysql status');
|
|
$result = $this->send($_action)->getResult();
|
|
}
|
|
if (is_array($result['Output'])) {
|
|
foreach ($result['Output'] as $aline) {
|
|
if (strlen($aline) > 3) {
|
|
$temp_strings = explode(' ', $aline);
|
|
$cmd_res_key = $temp_strings[0];
|
|
foreach ($temp_strings as $test_string) {
|
|
if (strpos($test_string, '@')) {
|
|
$this_realm = $test_string;
|
|
break;
|
|
}
|
|
}
|
|
$cmd_res[$cmd_res_key] = array('message' => $aline, 'realm' => $this_realm, 'status' => strpos($aline, 'connected') ? 'OK' : 'ERROR');
|
|
}
|
|
}
|
|
}
|
|
return $cmd_res;
|
|
}
|
|
|
|
public function get_compatible_sccp($revNumComp=false) {
|
|
// only called with args from installer to get revision and compatibility
|
|
$res = $this->getSCCPVersion();
|
|
if ($res['RevisionNum'] < 11063) {
|
|
$this->useAmiInterface = false;
|
|
}
|
|
if ($revNumComp) {
|
|
return array($res['vCode'], true);
|
|
}
|
|
return $res['vCode'];
|
|
}
|
|
}
|