ModuleHooks
Modules can add to the GUI and adjust the dialplan logic created by other modules. This is done using ModuleHooks.
Getting Hooked
A module developer has control over where foreign module html is displayed. A $module_hook object is available to all modules, and should be included in their display by calling its hookHtml function.
Putting the following in your module will display any options provided by other modules:
// implementation of module hook echo $module_hook->hookHtml;
Providing Hook Html
Calling the hookHtml method above will not display anything unless another module defines what should be displayed. This is done by creating an appropriately named "hook" function.
This "hook" function should be defined in the functions.inc.php of the module that wants to hook a target module. The return from the function must be html that can be directly displayed in the target module's output.
The name of the "hook" function is very important. It must be called:
hooking-module-name_hook_target-module-name
So, if a module called "myhookmod" is adding to the Inbound Routes (DID) admin, which is part of "core", the function would be named myhookmod_hook_core:
function myhookmod_hook_core($viewing_itemid, $target_menuid) {
return "hello world!";
}
The arguments $viewing_itemid and $target_menuid will contain the currently viewed item (ie: DID #1234567) and the currently viewed page within the module (ie: "did" - remember that a single module can have more than one page/menu-item). Using these arguments, our "hook" function can intelligently return the proper html options and default values:
function myhookmod_hook_core($viewing_itemid, $target_menuid) {
switch ($target_menuid) {
// only provide display for inbound routing
case 'did':
//get the current setting for this display (if any)
$alertinfo = myhookmod_get_currentOption($viewing_itemid);
return '
<tr>
<td><a href="#" class="info">'._("Alert Info").'<span>'._('ALERT_INFO can be used for distinctive ring with SIP devices.').'</span></a>:</td>
<td><input type="text" name="alertinfo" size="10" value="'.(($alertinfo) ? $alertinfo : "") .'"></td>
</tr>
';
break;
default:
return false;
break;
}
}
Processing submits (hookProcess)
In the above example, we added a text input to the core_DID's form. When that form is submitted, we need a way for our hooking module to process the posted data. To do this, we must also define a "hookProcess" function.
Similar to the "hook" function, the "hookProcess" function must be named:
hooking-module-name_hookProcess_target-module-name
Again, if a module called "myhookmod" is adding to the Inbound Routes (DID) admin, which is part of "core", the function would be named myhookmod_hookProcess_core:
function myhookmod_hookProcess_core($viewing_itemid, $request) {
// store the selection in the DB
myhookmod_add($viewing_itemid, $request['alertinfo']);
}
The options $viewing_itemid and $request contain the id of the item being modified, and all the available _REQUEST variables.
Generating Dialplan
Most useful modules will probably need to also generate Asterisk dialplan. In the case of ModuleHooks, that dialplan logic probably only makes sense if the priorities exist directly in the target module's context.
The extensions class provides a "splice()" method that allows a module to insert dialplan priorities into that created by another module.
After all the module dialplan is generated, the retrieve_conf? script will look for and execute active modulename_hookGet_config functions. By defining this function for our hooking module, we are able to make use of the "splice()" method.
myhookmod_hookGet_config($engine) {
global $ext;
switch($engine) {
case "asterisk":
$hooklist = myhookmod_list();
if(is_array($hooklist)) {
foreach($hooklist as $item) {
$thisitem = myhookmod_get(ltrim($item['hook_id']));
// add dialplan
$ext->splice('ext-did', $thisitem['itemid'], 1,
new ext_setvar("__ALERT_INFO", $thisitem['alertinfo']));
}
}
break;
}
}
The above function will insert a SetVar? command after the 1st priority for each Inbound Route.
Because modulename_hookGet_config() is called after all modules have been loaded, that is, after all the normal modulename_get_config(), you have access to the almost complete dial-plan here. This means one can not only edit one's own dial plan, but any other modules' dial plans, even if they are loaded by _get_config() after your plan. In OOP terms, _hookGet_config() is an "after" method.
Suppose you wanted to trace the flow through the whole dial-plan, for every module, for any given call.
(Something you would only do on a DEV box that you can actually successfully call in the first place, and not on a production box, and certainly not on a BUSY production box.)
An AGI script named dialplan_log.agi in /var/lib/asterisk/agi-bin something like this (untested code):
#!/usr/bin/php
<?php
//Make sure this file is executable! chmod w+x /var/lib/asterisk/agi-bin/dialplan_log.agi
//capture AGI stdin data
$agivars = array();
while (!feof(STDIN)) {
$agivar = trim(fgets(STDIN));
if ($agivar === '') {
error_log("--------------------\n", 3, "/var/log/asterisk/dialplan.log");
break;
}
error_log("$agivar\n", 3, "/var/log/asterisk/dialplan.log");
}
execute_agi("SET VARIABLE AGISTATUS SUCCESS");
exit(0);
function execute_agi($command) {
fwrite(STDOUT, "$command\n");
fflush(STDOUT);
$result = trim(fgets(STDIN));
$ret = array('code'=> -1, 'result'=> -1, 'timeout'=> false, 'data'=> '');
if (preg_match("/^([0-9]{1,3}) (.*)/", $result, $matches)) {
$ret['code'] = $matches[1];
$ret['result'] = 0;
if (preg_match('/^result=([0-9a-zA-Z]*)\s?(?:\(?(.*?)\)?)?$/', $matches[2], $match)) {
$ret['result'] = $match[1];
$ret['timeout'] = ($match[2] === 'timeout') ? true : false;
$ret['data'] = $match[2];
}
}
if (!is_array($ret) || !count($ret) || $ret['code'] != 200){
error_log(__FILE__.': '.__LINE__.": $command returned: " . print_r($ret, 1)."\n", 3, "/var/log/asterisk/dialplan.log");
}
return $ret;
}
?>
And now you just need to splice in a call for every context:
function modulename_hookGet_config($engine){
global $ext;
foreach($ext as $section => $dial_plan){
if (is_array($dial_plan)) foreach ($dial_plan as $context => $contents){
foreach($contents as $extension => $lines){
foreach($lines as $index => $line){
//For fun, you can inspect each line of the dial plan on an Orange "reload" by un-commenting:
//error_log("$context: $extension => " . print_r($line, 1), 3, "/var/log/asterisk/dialplan.log");
}
//splice in a call to your AGI at the 2nd index of every context
$ext_agi = new ext_agi("dialplan_log.agi"); //make sure you use the same name as your agi-bin/*.agi file
$ext->splice($context, $extension, 2, $ext_agi); //that hard-coded 2 is maybe not the BEST idea ever...
}
}
}
}
This will NOT track all the internal logic of the dial-plan within a given context, but it does provide an overview of the path of a call.
Of course, to do something useful to change the dial plan of another module, you'd have to have a good understanding of what that module does, and how its dial plan works.
Plus, you really wouldn't want to hard-code 2 or 16 or 42, since a new version of the module would possibly add or delete lines.
This is why good modules that want to provide customization have "tags" in their dial plan:
[good-context] exten => s,1,DoSomething() exten => s,n,SomethingElse() exten => s,n(tag),ProvideALabelTagHEREForOthersToHackMe() exten => s,n,SomeMoreStuff() exten => s,n(tag2),AnotherEasyTargetForAlteringWithSplice()
Of course, "tag" and "tag2" are horrible names. They should be like a good variable name, short and sweet and describing what's going on in the dial plan on that line.
Finally, if you absolutely must splice/replace a line, drop an email to the module owner explaining why they should tag that line for you, and don't just hard code the number!
Use something in the $line data structure (see "fun" above) to uniquely identify the line by its contents to search out the $index where that line lives. This will (hopefully) future proof your splice against the module owner adding/deleting lines. Of course, if they completely alter their algorithm, and the contents of the $line data structure aren't at all the same, it will break. At that point, your splice hack is probably due for review anyway... See above paragraph regarding getting the other module owner to add a tag to the line you need to splice.
Needless to say, this puts a great deal of power in your hands to wreak havoc throughout your dial-plan for any module you want to sabotage. Use this wisely, or not at all...
NOTE: $ext->replace() is the same as $ext->splice() except that splice adds a line, but replace changes the line to whatever you want.
