Issues (126)

src/AGI.php (1 issue)

1
<?php
2
3
namespace Inok\phpagi;
4
5
/**
6
 * phpagi.php : PHP AGI Functions for Asterisk
7
 * @see https://github.com/welltime/phpagi
8
 * @filesource http://phpagi.sourceforge.net/
9
 *
10
 * $Id: phpagi.php,v 2.20 2010/09/30 02:21:00 masham Exp $
11
 *
12
 * Copyright (c) 2003 - 2010 Matthew Asham <[email protected]>, David Eder <[email protected]> and others
13
 * All Rights Reserved.
14
 *
15
 * This software is released under the terms of the GNU Lesser General Public License v2.1
16
 * A copy of which is available from http://www.gnu.org/copyleft/lesser.html
17
 *
18
 * We would be happy to list your phpagi based application on the phpagi
19
 * website.  Drop me an Email if you'd like us to list your program.
20
 *
21
 *
22
 * Written for PHP 4.3.4, should work with older PHP 4.x versions.
23
 *
24
 * Please submit bug reports, patches, etc to https://github.com/welltime/phpagi
25
 *
26
 *
27
 * @package phpAGI
28
 * @version 2.20
29
 */
30
31
/**
32
 * AGI class
33
 *
34
 * @package phpAGI
35
 * @link http://www.voip-info.org/wiki-Asterisk+agi
36
 * @example examples/dtmf.php Get DTMF tones from the user and say the digits
37
 * @example examples/input.php Get text input from the user and say it back
38
 * @example examples/ping.php Ping an IP address
39
 */
40
class AGI
41
{
42
  /**
43
   * Request variables read in on initialization.
44
   *
45
   * Often contains any/all of the following:
46
   *   agi_request - name of agi script
47
   *   agi_channel - current channel
48
   *   agi_language - current language
49
   *   agi_type - channel type (SIP, ZAP, IAX, ...)
50
   *   agi_uniqueid - unique id based on unix time
51
   *   agi_callerid - callerID string
52
   *   agi_dnid - dialed number id
53
   *   agi_rdnis - referring DNIS number
54
   *   agi_context - current context
55
   *   agi_extension - extension dialed
56
   *   agi_priority - current priority
57
   *   agi_enhanced - value is 1.0 if started as an EAGI script
58
   *   agi_accountcode - set by SetAccount in the dialplan
59
   *   agi_network - value is yes if this is a fastagi
60
   *   agi_network_script - name of the script to execute
61
   *
62
   * NOTE: program arguments are still in $_SERVER['argv'].
63
   *
64
   * @var array
65
   * @access public
66
   */
67
  public $request;
68
69
  /**
70
   * Config variables
71
   *
72
   * @var array
73
   * @access public
74
   */
75
  public $config;
76
77
  /**
78
   * Asterisk Manager
79
   *
80
   * @var AGI_AsteriskManager
81
   * @access public
82
   */
83
  public $asm;
84
85
  /**
86
   * Input Stream
87
   *
88
   * @access private
89
   */
90
  private $in;
91
92
  /**
93
   * Output Stream
94
   *
95
   * @access private
96
   */
97
  private $out;
98
99
  /**
100
   * Audio Stream
101
   *
102
   * @access public
103
   */
104
  public $audio = null;
105
106
107
  /**
108
   * Application option delimiter
109
   *
110
   * @access public
111
   */
112
  public $option_delim = ",";
113
114
  private $defaultConfig = ["error_handler" => true,
115
                            "debug" => false,
116
                            "admin" => null,
117
                            "tempdir" => AGI_Others::AST_TMP_DIR];
118
119
  /**
120
   * Constructor
121
   *
122
   * @param string $config is the name of the config file to parse
123
   * @param array $optconfig is an array of configuration vars and values, stuffed into $this->config['phpagi']
124
   */
125
  public function __construct($config = null, array $optconfig = []) {
126
    // load config
127
    if (!is_null($config) && file_exists($config)) {
128
      $this->config = parse_ini_file($config, true);
129
    } elseif (file_exists(AGI_Others::DEFAULT_PHPAGI_CONFIG)) {
130
      $this->config = parse_ini_file(AGI_Others::DEFAULT_PHPAGI_CONFIG, true);
131
    }
132
133
    // If optconfig is specified, stuff values and vars into 'phpagi' config array.
134
    foreach ($optconfig as $var => $val) {
135
      $this->config['phpagi'][$var] = $val;
136
    }
137
138
    // add default values to config for uninitialized values
139
    foreach ($this->defaultConfig as $name => $value) {
140
      $this->config["phpagi"][$name] = $this->config["phpagi"][$name] ?? $value;
141
    }
142
143
    // festival TTS config
144
    $this->config['festival']['text2wave'] = $this->config['festival']['text2wave'] ?? $this->which('text2wave');
145
146
    // swift TTS config
147
    $this->config['cepstral']['swift'] = $this->config['cepstral']['swift'] ?? $this->which('swift');
148
149
    ob_implicit_flush();
150
151
    // open stdin & stdout
152
    $this->in = defined('STDIN') ? STDIN : fopen('php://stdin', 'r');
153
    $this->out = defined('STDOUT') ? STDOUT : fopen('php://stdout', 'w');
154
155
    // initialize error handler
156
    if ($this->config['phpagi']['error_handler']) {
157
      set_error_handler('\\Inok\\phpagi\\AGI_Others::phpagi_error_handler');
158
      AGI_Others::$phpagi_error_handler_email = $this->config['phpagi']['admin'];
159
      error_reporting(E_ALL);
160
    }
161
162
    // make sure temp folder exists
163
    $this->make_folder($this->config['phpagi']['tempdir']);
164
165
    // read the request
166
    $str = fgets($this->in);
167
    while ($str != "\n") {
168
      $this->request[substr($str, 0, strpos($str, ':'))] = trim(substr($str, strpos($str, ':') + 1));
169
      $str = fgets($this->in);
170
    }
171
172
    // open audio if agi detected
173
    if ($this->request['agi_enhanced'] == '1.0') {
174
      if (file_exists('/proc/' . getmypid() . '/fd/3')) {
175
        $this->audio = fopen('/proc/' . getmypid() . '/fd/3', 'r');
176
      } elseif (file_exists('/dev/fd/3')) {
177
        // may need to mount fdescfs
178
        $this->audio = fopen('/dev/fd/3', 'r');
179
      } else {
180
        $this->conlog('Unable to open audio stream');
181
      }
182
183
      if ($this->audio) {
184
        stream_set_blocking($this->audio, 0);
185
      }
186
    }
187
188
    $this->conlog('AGI Request:');
189
    $this->conlog(/** @scrutinizer ignore-type */ print_r($this->request, true));
190
    $this->conlog('PHPAGI internal configuration:');
191
    $this->conlog(/** @scrutinizer ignore-type */ print_r($this->config, true));
192
  }
193
194
  // *********************************************************************************************************
195
  // **                             COMMANDS                                                                                            **
196
  // *********************************************************************************************************
197
198
  /**
199
   * Answer channel if not already in answer state.
200
   *
201
   * @link http://www.voip-info.org/wiki-answer
202
   * @example examples/dtmf.php Get DTMF tones from the user and say the digits
203
   * @example examples/input.php Get text input from the user and say it back
204
   * @example examples/ping.php Ping an IP address
205
   *
206
   * @return array, see evaluate for return information.  ['result'] is 0 on success, -1 on failure.
207
   */
208
  function answer(): array {
209
    return $this->evaluate('ANSWER');
210
  }
211
212
  /**
213
   * Get the status of the specified channel. If no channel name is specified, return the status of the current channel.
214
   *
215
   * @link http://www.voip-info.org/wiki-channel+status
216
   * @param string $channel
217
   * @return array, see evaluate for return information. ['data'] contains description.
218
   */
219
  function channel_status(string $channel = ''): array {
220
    $ret = $this->evaluate("CHANNEL STATUS $channel");
221
    switch ($ret['result']) {
222
      case -1:
223
        $ret['data'] = trim("There is no channel that matches $channel");
224
        break;
225
      case AGI_Others::AST_STATE_DOWN:
226
        $ret['data'] = 'Channel is down and available';
227
        break;
228
      case AGI_Others::AST_STATE_RESERVED:
229
        $ret['data'] = 'Channel is down, but reserved';
230
        break;
231
      case AGI_Others::AST_STATE_OFFHOOK:
232
        $ret['data'] = 'Channel is off hook';
233
        break;
234
      case AGI_Others::AST_STATE_DIALING:
235
        $ret['data'] = 'Digits (or equivalent) have been dialed';
236
        break;
237
      case AGI_Others::AST_STATE_RING:
238
        $ret['data'] = 'Line is ringing';
239
        break;
240
      case AGI_Others::AST_STATE_RINGING:
241
        $ret['data'] = 'Remote end is ringing';
242
        break;
243
      case AGI_Others::AST_STATE_UP:
244
        $ret['data'] = 'Line is up';
245
        break;
246
      case AGI_Others::AST_STATE_BUSY:
247
        $ret['data'] = 'Line is busy';
248
        break;
249
      case AGI_Others::AST_STATE_DIALING_OFFHOOK:
250
        $ret['data'] = 'Digits (or equivalent) have been dialed while offhook';
251
        break;
252
      case AGI_Others::AST_STATE_PRERING:
253
        $ret['data'] = 'Channel has detected an incoming call and is waiting for ring';
254
        break;
255
      default:
256
        $ret['data'] = "Unknown ({$ret['result']})";
257
        break;
258
    }
259
    return $ret;
260
  }
261
262
  /**
263
   * Deletes an entry in the Asterisk database for a given family and key.
264
   *
265
   * @link http://www.voip-info.org/wiki-database+del
266
   * @param string $family
267
   * @param string $key
268
   * @return array, see evaluate for return information. ['result'] is 1 on success, 0 otherwise.
269
   */
270
  function database_del(string $family, string $key): array {
271
    return $this->evaluate("DATABASE DEL \"$family\" \"$key\"");
272
  }
273
274
  /**
275
   * Deletes a family or specific keytree within a family in the Asterisk database.
276
   *
277
   * @link http://www.voip-info.org/wiki-database+deltree
278
   * @param string $family
279
   * @param string $keytree
280
   * @return array, see evaluate for return information. ['result'] is 1 on success, 0 otherwise.
281
   */
282
  function database_deltree(string $family, string $keytree = ''): array {
283
    $cmd = "DATABASE DELTREE \"$family\"";
284
    if ($keytree != '') {
285
      $cmd .= " \"$keytree\"";
286
    }
287
    return $this->evaluate($cmd);
288
  }
289
290
  /**
291
   * Retrieves an entry in the Asterisk database for a given family and key.
292
   *
293
   * @link http://www.voip-info.org/wiki-database+get
294
   * @param string $family
295
   * @param string $key
296
   * @return array, see evaluate for return information. ['result'] is 1 on success, 0 failure. ['data'] holds the value
297
   */
298
  function database_get(string $family, string $key): array {
299
    return $this->evaluate("DATABASE GET \"$family\" \"$key\"");
300
  }
301
302
  /**
303
   * Adds or updates an entry in the Asterisk database for a given family, key, and value.
304
   *
305
   * @param string $family
306
   * @param string $key
307
   * @param string $value
308
   * @return array, see evaluate for return information. ['result'] is 1 on success, 0 otherwise
309
   */
310
  function database_put(string $family, string $key, string $value): array {
311
    $value = str_replace("\n", '\n', addslashes($value));
312
    return $this->evaluate("DATABASE PUT \"$family\" \"$key\" \"$value\"");
313
  }
314
315
316
  /**
317
   * Sets a global variable, using Asterisk 1.6 syntax.
318
   *
319
   * @link http://www.voip-info.org/wiki/view/Asterisk+cmd+Set
320
   *
321
   * @param string $pVariable
322
   * @param string|int|float $pValue
323
   * @return array, see evaluate for return information. ['result'] is 1 on success, 0 otherwise
324
   */
325
  function set_global_var(string $pVariable, $pValue): array {
326
    if (is_numeric($pValue)) {
327
      return $this->evaluate("Set({$pVariable}={$pValue},g);");
328
    }
329
    return $this->evaluate("Set({$pVariable}=\"{$pValue}\",g);");
330
  }
331
332
333
  /**
334
   * Sets a variable, using Asterisk 1.6 syntax.
335
   *
336
   * @link http://www.voip-info.org/wiki/view/Asterisk+cmd+Set
337
   *
338
   * @param string $pVariable
339
   * @param string|int|float $pValue
340
   * @return array, see evaluate for return information. ['result'] is 1 on success, 0 otherwise
341
   */
342
  function set_var(string $pVariable, $pValue): array {
343
    if (is_numeric($pValue)) {
344
      return $this->evaluate("Set({$pVariable}={$pValue});");
345
    }
346
    return $this->evaluate("Set({$pVariable}=\"{$pValue}\");");
347
  }
348
349
350
  /**
351
   * Executes the specified Asterisk application with given options.
352
   *
353
   * @link http://www.voip-info.org/wiki-exec
354
   * @link http://www.voip-info.org/wiki-Asterisk+-+documentation+of+application+commands
355
   * @param string $application
356
   * @param mixed $options
357
   * @return array, see evaluate for return information. ['result'] is whatever the application returns, or -2 on failure to find application
358
   */
359
  function exec(string $application, $options): array {
360
    if (is_array($options)) {
361
      $options = join('|', $options);
362
    }
363
    return $this->evaluate("EXEC $application $options");
364
  }
365
366
  /**
367
   * Plays the given file and receives DTMF data.
368
   *
369
   * This is similar to STREAM FILE, but this command can accept and return many DTMF digits,
370
   * while STREAM FILE returns immediately after the first DTMF digit is detected.
371
   *
372
   * Asterisk looks for the file to play in /var/lib/asterisk/sounds by default.
373
   *
374
   * If the user doesn't press any keys when the message plays, there is $timeout milliseconds
375
   * of silence then the command ends.
376
   *
377
   * The user has the opportunity to press a key at any time during the message or the
378
   * post-message silence. If the user presses a key while the message is playing, the
379
   * message stops playing. When the first key is pressed a timer starts counting for
380
   * $timeout milliseconds. Every time the user presses another key the timer is restarted.
381
   * The command ends when the counter goes to zero or the maximum number of digits is entered,
382
   * whichever happens first.
383
   *
384
   * If you don't specify a time out then a default timeout of 2000 is used following a pressed
385
   * digit. If no digits are pressed then 6 seconds of silence follow the message.
386
   *
387
   * If you don't specify $max_digits then the user can enter as many digits as they want.
388
   *
389
   * Pressing the # key has the same effect as the timer running out: the command ends and
390
   * any previously keyed digits are returned. A side effect of this is that there is no
391
   * way to read a # key using this command.
392
   *
393
   * @param string $filename file to play. Do not include file extension.
394
   * @param integer $timeout milliseconds
395
   * @param integer $max_digits
396
   * @return array, see evaluate for return information. ['result'] holds the digits and ['data'] holds the timeout if present.
397
   *
398
   * This differs from other commands with return DTMF as numbers representing ASCII characters.
399
   * @example examples/ping.php Ping an IP address
400
   *
401
   * @link http://www.voip-info.org/wiki-get+data
402
   */
403
  function get_data(string $filename, $timeout = null, $max_digits = null): array {
404
    return $this->evaluate(rtrim("GET DATA $filename $timeout $max_digits"));
405
  }
406
407
  /**
408
   * Fetch the value of a variable.
409
   *
410
   * Does not work with global variables. Does not work with some variables that are generated by modules.
411
   *
412
   * @link http://www.voip-info.org/wiki-get+variable
413
   * @link http://www.voip-info.org/wiki-Asterisk+variables
414
   * @param string $variable name
415
   * @param boolean $getvalue return the value only
416
   * @return mixed, see evaluate for return information. ['result'] is 0 if variable hasn't been set, 1 if it has. ['data'] holds the value. returns value if $getvalue is TRUE
417
   */
418
  function get_variable(string $variable, bool $getvalue = false) {
419
    $res = $this->evaluate("GET VARIABLE $variable");
420
    return $getvalue ? $res['data'] : $res;
421
  }
422
423
424
  /**
425
   * Fetch the value of a full variable.
426
   *
427
   *
428
   * @link http://www.voip-info.org/wiki/view/get+full+variable
429
   * @link http://www.voip-info.org/wiki-Asterisk+variables
430
   * @param string $variable name
431
   * @param string $channel channel
432
   * @param boolean $getvalue return the value only
433
   * @return mixed, see evaluate for return information. ['result'] is 0 if variable hasn't been set, 1 if it has. ['data'] holds the value.  returns value if $getvalue is TRUE
434
   */
435
  function get_fullvariable(string $variable, $channel = false, bool $getvalue = false) {
436
    $req = ($channel == false) ? $variable : $variable . ' ' . $channel;
437
    $res = $this->evaluate('GET FULL VARIABLE ' . $req);
438
    return $getvalue ? $res['data'] : $res;
439
440
  }
441
442
  /**
443
   * Hangup the specified channel. If no channel name is given, hang up the current channel.
444
   *
445
   * With power comes responsibility. Hanging up channels other than your own isn't something
446
   * that is done routinely. If you are not sure why you are doing so, then don't.
447
   *
448
   * @link http://www.voip-info.org/wiki-hangup
449
   * @example examples/dtmf.php Get DTMF tones from the user and say the digits
450
   * @example examples/input.php Get text input from the user and say it back
451
   * @example examples/ping.php Ping an IP address
452
   *
453
   * @param string $channel
454
   * @return array, see evaluate for return information. ['result'] is 1 on success, -1 on failure.
455
   */
456
  function hangup(string $channel = ''): array {
457
    return $this->evaluate("HANGUP $channel");
458
  }
459
460
  /**
461
   * Does nothing.
462
   *
463
   * @link http://www.voip-info.org/wiki-noop
464
   * @param string $string
465
   * @return array, see evaluate for return information.
466
   */
467
  function noop(string $string = ""): array {
468
    return $this->evaluate("NOOP \"$string\"");
469
  }
470
471
  /**
472
   * Receive a character of text from a connected channel. Waits up to $timeout milliseconds for
473
   * a character to arrive, or infinitely if $timeout is zero.
474
   *
475
   * @link http://www.voip-info.org/wiki-receive+char
476
   * @param integer $timeout milliseconds
477
   * @return array, see evaluate for return information. ['result'] is 0 on timeout or not supported, -1 on failure. Otherwise
478
   * it is the decimal value of the DTMF tone. Use chr() to convert to ASCII.
479
   */
480
  function receive_char(int $timeout = -1): array {
481
    return $this->evaluate("RECEIVE CHAR $timeout");
482
  }
483
484
  /**
485
   * Record sound to a file until an acceptable DTMF digit is received or a specified amount of
486
   * time has passed. Optionally the file BEEP is played before recording begins.
487
   *
488
   * @link http://www.voip-info.org/wiki-record+file
489
   * @param string $file to record, without extension, often created in /var/lib/asterisk/sounds
490
   * @param string $format of the file. GSM and WAV are commonly used formats. MP3 is read-only and thus cannot be used.
491
   * @param string $escape_digits
492
   * @param integer $timeout is the maximum record time in milliseconds, or -1 for no timeout.
493
   * @param integer $offset to seek to without exceeding the end of the file.
494
   * @param boolean $beep
495
   * @param integer $silence number of seconds of silence allowed before the function returns despite the
496
   * lack of dtmf digits or reaching timeout.
497
   * @return array, see evaluate for return information. ['result'] is -1 on error, 0 on hangup, otherwise a decimal value of the
498
   * DTMF tone. Use chr() to convert to ASCII.
499
   */
500
  function record_file(string $file, string $format, string $escape_digits = '', int $timeout = -1,
501
                       $offset = null, bool $beep = false, $silence = null): array {
502
    $cmd = trim("RECORD FILE $file $format \"$escape_digits\" $timeout $offset");
503
    if ($beep) {
504
      $cmd .= ' BEEP';
505
    }
506
    if (!is_null($silence)) {
507
      $cmd .= " s=$silence";
508
    }
509
    return $this->evaluate($cmd);
510
  }
511
512
  /**
513
   * Say a given character string, returning early if any of the given DTMF digits are received on the channel.
514
   *
515
   * @link https://www.voip-info.org/say-alpha
516
   * @param string $text
517
   * @param string $escape_digits
518
   * @return array, see evaluate for return information. ['result'] is -1 on hangup or error, 0 if playback completes with no
519
   * digit received, otherwise a decimal value of the DTMF tone.  Use chr() to convert to ASCII.
520
   */
521
  function say_alpha(string $text, string $escape_digits = ''): array {
522
    return $this->evaluate("SAY ALPHA $text \"$escape_digits\"");
523
  }
524
525
  /**
526
   * Say the given digit string, returning early if any of the given DTMF escape digits are received on the channel.
527
   *
528
   * @link http://www.voip-info.org/wiki-say+digits
529
   * @param integer $digits
530
   * @param string $escape_digits
531
   * @return array, see evaluate for return information. ['result'] is -1 on hangup or error, 0 if playback completes with no
532
   * digit received, otherwise a decimal value of the DTMF tone.  Use chr() to convert to ASCII.
533
   */
534
  function say_digits(int $digits, string $escape_digits = ''): array {
535
    return $this->evaluate("SAY DIGITS $digits \"$escape_digits\"");
536
  }
537
538
  /**
539
   * Say the given number, returning early if any of the given DTMF escape digits are received on the channel.
540
   *
541
   * @link http://www.voip-info.org/wiki-say+number
542
   * @param integer $number
543
   * @param string $escape_digits
544
   * @return array, see evaluate for return information. ['result'] is -1 on hangup or error, 0 if playback completes with no
545
   * digit received, otherwise a decimal value of the DTMF tone.  Use chr() to convert to ASCII.
546
   */
547
  function say_number(int $number, string $escape_digits = ''): array {
548
    return $this->evaluate("SAY NUMBER $number \"$escape_digits\"");
549
  }
550
551
  /**
552
   * Say the given character string, returning early if any of the given DTMF escape digits are received on the channel.
553
   *
554
   * @link http://www.voip-info.org/wiki-say+phonetic
555
   * @param string $text
556
   * @param string $escape_digits
557
   * @return array, see evaluate for return information. ['result'] is -1 on hangup or error, 0 if playback completes with no
558
   * digit received, otherwise a decimal value of the DTMF tone.  Use chr() to convert to ASCII.
559
   */
560
  function say_phonetic(string $text, string $escape_digits = ''): array {
561
    return $this->evaluate("SAY PHONETIC $text \"$escape_digits\"");
562
  }
563
564
  /**
565
   * Say a given time, returning early if any of the given DTMF escape digits are received on the channel.
566
   *
567
   * @link http://www.voip-info.org/wiki-say+time
568
   * @param integer $time number of seconds elapsed since 00:00:00 on January 1, 1970, Coordinated Universal Time (UTC).
569
   * @param string $escape_digits
570
   * @return array, see evaluate for return information. ['result'] is -1 on hangup or error, 0 if playback completes with no
571
   * digit received, otherwise a decimal value of the DTMF tone.  Use chr() to convert to ASCII.
572
   */
573
  function say_time($time = null, string $escape_digits = ''): array {
574
    if (is_null($time)) {
575
      $time = time();
576
    }
577
    return $this->evaluate("SAY TIME $time \"$escape_digits\"");
578
  }
579
580
  /**
581
   * Send the specified image on a channel.
582
   *
583
   * Most channels do not support the transmission of images.
584
   *
585
   * @link http://www.voip-info.org/wiki-send+image
586
   * @param string $image without extension, often in /var/lib/asterisk/images
587
   * @return array, see evaluate for return information. ['result'] is -1 on hangup or error, 0 if the image is sent or
588
   * channel does not support image transmission.
589
   */
590
  function send_image(string $image): array {
591
    return $this->evaluate("SEND IMAGE $image");
592
  }
593
594
  /**
595
   * Send the given text to the connected channel.
596
   *
597
   * Most channels do not support transmission of text.
598
   *
599
   * @link http://www.voip-info.org/wiki-send+text
600
   * @param $text
601
   * @return array, see evaluate for return information. ['result'] is -1 on hangup or error, 0 if the text is sent or
602
   * channel does not support text transmission.
603
   */
604
  function send_text($text): array {
605
    return $this->evaluate("SEND TEXT \"$text\"");
606
  }
607
608
  /**
609
   * Cause the channel to automatically hangup at $time seconds in the future.
610
   * If $time is 0 then the auto hangup feature is disabled on this channel.
611
   *
612
   * If the channel is hangup prior to $time seconds, this setting has no effect.
613
   *
614
   * @link http://www.voip-info.org/wiki-set+autohangup
615
   * @param integer $time until automatic hangup
616
   * @return array, see evaluate for return information.
617
   */
618
  function set_autohangup(int $time = 0): array {
619
    return $this->evaluate("SET AUTOHANGUP $time");
620
  }
621
622
  /**
623
   * Changes the caller ID of the current channel.
624
   *
625
   * @link http://www.voip-info.org/wiki-set+callerid
626
   * @param string $cid example: "John Smith"<1234567>
627
   * This command will let you take liberties with the <caller ID specification> but the format shown in the example above works
628
   * well: the name enclosed in double quotes followed immediately by the number inside angle brackets. If there is no name then
629
   * you can omit it. If the name contains no spaces you can omit the double quotes around it. The number must follow the name
630
   * immediately; don't put a space between them. The angle brackets around the number are necessary; if you omit them the
631
   * number will be considered to be part of the name.
632
   * @return array, see evaluate for return information.
633
   */
634
  function set_callerid(string $cid): array {
635
    return $this->evaluate("SET CALLERID $cid");
636
  }
637
638
  /**
639
   * Sets the context for continuation upon exiting the application.
640
   *
641
   * Setting the context does NOT automatically reset the extension and the priority; if you want to start at the top of the new
642
   * context you should set extension and priority yourself.
643
   *
644
   * If you specify a non-existent context you receive no error indication (['result'] is still 0) but you do get a
645
   * warning message on the Asterisk console.
646
   *
647
   * @link http://www.voip-info.org/wiki-set+context
648
   * @param string $context
649
   * @return array, see evaluate for return information.
650
   */
651
  function set_context(string $context): array {
652
    return $this->evaluate("SET CONTEXT $context");
653
  }
654
655
  /**
656
   * Set the extension to be used for continuation upon exiting the application.
657
   *
658
   * Setting the extension does NOT automatically reset the priority. If you want to start with the first priority of the
659
   * extension you should set the priority yourself.
660
   *
661
   * If you specify a non-existent extension you receive no error indication (['result'] is still 0) but you do
662
   * get a warning message on the Asterisk console.
663
   *
664
   * @link http://www.voip-info.org/wiki-set+extension
665
   * @param string $extension
666
   * @return array, see evaluate for return information.
667
   */
668
  function set_extension(string $extension): array {
669
    return $this->evaluate("SET EXTENSION $extension");
670
  }
671
672
  /**
673
   * Enable/Disable Music on hold generator.
674
   *
675
   * @link http://www.voip-info.org/wiki-set+music
676
   * @param boolean $enabled
677
   * @param string $class
678
   * @return array, see evaluate for return information.
679
   */
680
  function set_music(bool $enabled = true, string $class = ''): array {
681
    $enabled = ($enabled) ? 'ON' : 'OFF';
682
    return $this->evaluate("SET MUSIC $enabled $class");
683
  }
684
685
  /**
686
   * Set the priority to be used for continuation upon exiting the application.
687
   *
688
   * If you specify a non-existent priority you receive no error indication (['result'] is still 0)
689
   * and no warning is issued on the Asterisk console.
690
   *
691
   * @link http://www.voip-info.org/wiki-set+priority
692
   * @param integer $priority
693
   * @return array, see evaluate for return information.
694
   */
695
  function set_priority(int $priority): array {
696
    return $this->evaluate("SET PRIORITY $priority");
697
  }
698
699
  /**
700
   * Sets a variable to the specified value. The variables so created can later be used by later using ${<variablename>}
701
   * in the dialplan.
702
   *
703
   * These variables live in the channel Asterisk creates when you pickup a phone and as such they are both local and temporary.
704
   * Variables created in one channel can not be accessed by another channel. When you hang up the phone, the channel is deleted
705
   * and any variables in that channel are deleted as well.
706
   *
707
   * @link http://www.voip-info.org/wiki-set+variable
708
   * @param string $variable is case sensitive
709
   * @param string $value
710
   * @return array, see evaluate for return information.
711
   */
712
  function set_variable(string $variable, string $value): array {
713
    $value = str_replace("\n", '\n', addslashes($value));
714
    return $this->evaluate("SET VARIABLE $variable \"$value\"");
715
  }
716
717
  /**
718
   * Play the given audio file, allowing playback to be interrupted by a DTMF digit. This command is similar to the GET DATA
719
   * command but this command returns after the first DTMF digit has been pressed while GET DATA can accumulated any number of
720
   * digits before returning.
721
   *
722
   * @param string $filename without extension, often in /var/lib/asterisk/sounds
723
   * @param string $escape_digits
724
   * @param integer $offset
725
   * @return array, see evaluate for return information. ['result'] is -1 on hangup or error, 0 if playback completes with no
726
   * digit received, otherwise a decimal value of the DTMF tone.  Use chr() to convert to ASCII.
727
   * @example examples/ping.php Ping an IP address
728
   *
729
   * @link http://www.voip-info.org/wiki-stream+file
730
   */
731
  function stream_file(string $filename, string $escape_digits = '', int $offset = 0): array {
732
    return $this->evaluate("STREAM FILE $filename \"$escape_digits\" $offset");
733
  }
734
735
  /**
736
   * Enable or disable TDD transmission/reception on the current channel.
737
   *
738
   * @link http://www.voip-info.org/wiki-tdd+mode
739
   * @param string $setting can be on, off or mate
740
   * @return array, see evaluate for return information. ['result'] is 1 on success, 0 if the channel is not TDD capable.
741
   */
742
  function tdd_mode(string $setting): array {
743
    return $this->evaluate("TDD MODE $setting");
744
  }
745
746
  /**
747
   * Sends $message to the Asterisk console via the 'verbose' message system.
748
   *
749
   * If the Asterisk verbosity level is $level or greater, send $message to the console.
750
   *
751
   * The Asterisk verbosity system works as follows. The Asterisk user gets to set the desired verbosity at startup time or later
752
   * using the console 'set verbose' command. Messages are displayed on the console if their verbose level is less than or equal
753
   * to desired verbosity set by the user. More important messages should have a low verbose level; less important messages
754
   * should have a high verbose level.
755
   *
756
   * @link http://www.voip-info.org/wiki-verbose
757
   * @param string $message
758
   * @param integer $level from 1 to 4
759
   * @return array, see evaluate for return information.
760
   */
761
  function verbose(string $message, int $level = 1): array {
762
    $ret = [];
763
    foreach (explode("\n", str_replace("\r\n", "\n", print_r($message, true))) as $msg) {
764
      @syslog(LOG_WARNING, $msg);
765
      $ret = $this->evaluate("VERBOSE \"$msg\" $level");
766
    }
767
    return $ret;
768
  }
769
770
  /**
771
   * Waits up to $timeout milliseconds for channel to receive a DTMF digit.
772
   *
773
   * @link http://www.voip-info.org/wiki-wait+for+digit
774
   * @param integer $timeout in milliseconds. Use -1 for the timeout value if you want the call to wait indefinitely.
775
   * @return array, see evaluate for return information. ['result'] is 0 if wait completes with no
776
   * digit received, otherwise a decimal value of the DTMF tone.  Use chr() to convert to ASCII.
777
   */
778
  function wait_for_digit(int $timeout = -1): array {
779
    return $this->evaluate("WAIT FOR DIGIT $timeout");
780
  }
781
782
783
  // *********************************************************************************************************
784
  // **                             APPLICATIONS                                                                                        **
785
  // *********************************************************************************************************
786
787
  /**
788
   * Set absolute maximum time of call.
789
   *
790
   * Note that the timeout is set from the current time forward, not counting the number of seconds the call has already been up.
791
   * Each time you call AbsoluteTimeout(), all previous absolute timeouts are cancelled.
792
   * Will return the call to the T extension so that you can playback an explanatory note to the calling party (the called party
793
   * will not hear that)
794
   *
795
   * @link http://www.voip-info.org/wiki-Asterisk+-+documentation+of+application+commands
796
   * @link http://www.dynx.net/ASTERISK/AGI/ccard/agi-ccard.agi
797
   * @param integer $seconds allowed, 0 disables timeout
798
   * @return array, see evaluate for return information.
799
   */
800
  function exec_absolutetimeout(int $seconds = 0): array {
801
    return $this->exec('AbsoluteTimeout', $seconds);
802
  }
803
804
  /**
805
   * Executes an AGI compliant application.
806
   *
807
   * @param string $command
808
   * @param string $args
809
   * @return array, see evaluate for return information. ['result'] is -1 on hangup or if application requested hangup, or 0 on non-hangup exit.
810
   */
811
  function exec_agi(string $command, string $args): array {
812
    return $this->exec("AGI $command", $args);
813
  }
814
815
  /**
816
   * Set Language.
817
   *
818
   * @param string $language code
819
   * @return array, see evaluate for return information.
820
   */
821
  function exec_setlanguage(string $language = 'en'): array {
822
    return $this->exec('Set', 'CHANNEL(language)=' . $language);
823
  }
824
825
  /**
826
   * Do ENUM Lookup.
827
   *
828
   * Note: to retrieve the result, use
829
   *   get_variable('ENUM');
830
   *
831
   * @param $exten
832
   * @return array, see evaluate for return information.
833
   */
834
  function exec_enumlookup($exten): array {
835
    return $this->exec('EnumLookup', $exten);
836
  }
837
838
  /**
839
   * Dial.
840
   *
841
   * Dial takes input from ${VXML_URL} to send XML Url to Cisco 7960
842
   * Dial takes input from ${ALERT_INFO} to set ring cadence for Cisco phones
843
   * Dial returns ${CAUSECODE}: If the dial failed, this is the errormessage.
844
   * Dial returns ${DIALSTATUS}: Text code returning status of last dial attempt.
845
   *
846
   * @link http://www.voip-info.org/wiki-Asterisk+cmd+Dial
847
   * @param string $type
848
   * @param string $identifier
849
   * @param integer $timeout
850
   * @param string $options
851
   * @param string $url
852
   * @return array, see evaluate for return information.
853
   */
854
  function exec_dial(string $type, string $identifier, $timeout = null, $options = null, $url = null): array {
855
    return $this->exec(
856
      'Dial', trim("$type/$identifier" . $this->option_delim . $timeout . $this->option_delim . $options . $this->option_delim . $url, $this->option_delim));
857
  }
858
859
  /**
860
   * Goto.
861
   *
862
   * This function takes three arguments: context,extension, and priority, but the leading arguments
863
   * are optional, not the trailing arguments.  Those goto($z) sets the priority to $z.
864
   *
865
   * @param string $a
866
   * @param string $b ;
867
   * @param string $c ;
868
   * @return array, see evaluate for return information.
869
   */
870
  function exec_goto(string $a, $b = null, $c = null): array {
871
    return $this->exec('Goto', trim($a . $this->option_delim . $b . $this->option_delim . $c, $this->option_delim));
872
  }
873
874
875
  // *********************************************************************************************************
876
  // **                             FAST PASSING                                                                                        **
877
  // *********************************************************************************************************
878
879
  /**
880
   * Say the given digit string, returning early if any of the given DTMF escape digits are received on the channel.
881
   * Return early if $buffer is adequate for request.
882
   *
883
   * @link http://www.voip-info.org/wiki-say+digits
884
   * @param string $buffer
885
   * @param integer $digits
886
   * @param string $escape_digits
887
   * @return array, see evaluate for return information. ['result'] is -1 on hangup or error, 0 if playback completes with no
888
   * digit received, otherwise a decimal value of the DTMF tone.  Use chr() to convert to ASCII.
889
   */
890
  function fastpass_say_digits(string &$buffer, int $digits, string $escape_digits = ''): array {
891
    if ($buffer == '' || $this->proceedDigits($buffer, $escape_digits)) {
892
      $res = $this->say_digits($digits, $escape_digits);
893
      $this->appendBuffer($buffer, $res);
894
      return $res;
895
    }
896
    return ['code' => AGI_Others::AGIRES_OK,
897
            'result' => $this->ordBuffer($buffer)];
898
  }
899
900
  /**
901
   * Say the given number, returning early if any of the given DTMF escape digits are received on the channel.
902
   * Return early if $buffer is adequate for request.
903
   *
904
   * @link http://www.voip-info.org/wiki-say+number
905
   * @param string $buffer
906
   * @param integer $number
907
   * @param string $escape_digits
908
   * @return array, see evaluate for return information. ['result'] is -1 on hangup or error, 0 if playback completes with no
909
   * digit received, otherwise a decimal value of the DTMF tone.  Use chr() to convert to ASCII.
910
   */
911
  function fastpass_say_number(string &$buffer, int $number, string $escape_digits = ''): array {
912
    if ($buffer == '' || $this->proceedDigits($buffer, $escape_digits)) {
913
      $res = $this->say_number($number, $escape_digits);
914
      $this->appendBuffer($buffer, $res);
915
      return $res;
916
    }
917
    return ['code' => AGI_Others::AGIRES_OK,
918
            'result' => $this->ordBuffer($buffer)];
919
  }
920
921
  /**
922
   * Say the given character string, returning early if any of the given DTMF escape digits are received on the channel.
923
   * Return early if $buffer is adequate for request.
924
   *
925
   * @link http://www.voip-info.org/wiki-say+phonetic
926
   * @param string $buffer
927
   * @param string $text
928
   * @param string $escape_digits
929
   * @return array, see evaluate for return information. ['result'] is -1 on hangup or error, 0 if playback completes with no
930
   * digit received, otherwise a decimal value of the DTMF tone.  Use chr() to convert to ASCII.
931
   */
932
  function fastpass_say_phonetic(string &$buffer, string $text, string $escape_digits = ''): array {
933
    if ($buffer == '' || $this->proceedDigits($buffer, $escape_digits)) {
934
      $res = $this->say_phonetic($text, $escape_digits);
935
      $this->appendBuffer($buffer, $res);
936
      return $res;
937
    }
938
    return ['code' => AGI_Others::AGIRES_OK,
939
            'result' => $this->ordBuffer($buffer)];
940
  }
941
942
  /**
943
   * Say a given time, returning early if any of the given DTMF escape digits are received on the channel.
944
   * Return early if $buffer is adequate for request.
945
   *
946
   * @link http://www.voip-info.org/wiki-say+time
947
   * @param string $buffer
948
   * @param integer $time number of seconds elapsed since 00:00:00 on January 1, 1970, Coordinated Universal Time (UTC).
949
   * @param string $escape_digits
950
   * @return array, see evaluate for return information. ['result'] is -1 on hangup or error, 0 if playback completes with no
951
   * digit received, otherwise a decimal value of the DTMF tone.  Use chr() to convert to ASCII.
952
   */
953
  function fastpass_say_time(string &$buffer, $time = null, string $escape_digits = ''): array {
954
    if ($buffer == '' || $this->proceedDigits($buffer, $escape_digits)) {
955
      $res = $this->say_time($time, $escape_digits);
956
      $this->appendBuffer($buffer, $res);
957
      return $res;
958
    }
959
    return ['code' => AGI_Others::AGIRES_OK,
960
            'result' => $this->ordBuffer($buffer)];
961
  }
962
963
  /**
964
   * Play the given audio file, allowing playback to be interrupted by a DTMF digit. This command is similar to the GET DATA
965
   * command but this command returns after the first DTMF digit has been pressed while GET DATA can accumulated any number of
966
   * digits before returning.
967
   * Return early if $buffer is adequate for request.
968
   *
969
   * @link http://www.voip-info.org/wiki-stream+file
970
   * @param string $buffer
971
   * @param string $filename without extension, often in /var/lib/asterisk/sounds
972
   * @param string $escape_digits
973
   * @param integer $offset
974
   * @return array, see evaluate for return information. ['result'] is -1 on hangup or error, 0 if playback completes with no
975
   * digit received, otherwise a decimal value of the DTMF tone.  Use chr() to convert to ASCII.
976
   */
977
  function fastpass_stream_file(string &$buffer, string $filename, string $escape_digits = '', int $offset = 0): array {
978
    if ($buffer == '' || $this->proceedDigits($buffer, $escape_digits)) {
979
      $res = $this->stream_file($filename, $escape_digits, $offset);
980
      $this->appendBuffer($buffer, $res);
981
      return $res;
982
    }
983
    return ['code' => AGI_Others::AGIRES_OK,
984
            'result' => $this->ordBuffer($buffer),
985
            'endpos' => 0];
986
  }
987
988
  /**
989
   * Use festival to read text.
990
   * Return early if $buffer is adequate for request.
991
   *
992
   * @link http://www.cstr.ed.ac.uk/projects/festival/
993
   * @param string $buffer
994
   * @param string $text
995
   * @param string $escape_digits
996
   * @param integer $frequency
997
   * @return array, see evaluate for return information.
998
   */
999
  function fastpass_text2wav(string &$buffer, string $text, string $escape_digits = '', int $frequency = 8000): array {
1000
    if ($buffer == '' || $this->proceedDigits($buffer, $escape_digits)) {
1001
      $res = $this->text2wav($text, $escape_digits, $frequency);
1002
      $this->appendBuffer($buffer, $res);
1003
      return $res;
1004
    }
1005
    return ['code' => AGI_Others::AGIRES_OK,
1006
            'result' => $this->ordBuffer($buffer),
1007
            'endpos' => 0];
1008
  }
1009
1010
  /**
1011
   * Use Cepstral Swift to read text.
1012
   * Return early if $buffer is adequate for request.
1013
   *
1014
   * @link http://www.cepstral.com/
1015
   * @param string $buffer
1016
   * @param string $text
1017
   * @param string $escape_digits
1018
   * @param integer $frequency
1019
   * @param $voice
1020
   * @return array, see evaluate for return information.
1021
   */
1022
  function fastpass_swift(string &$buffer, string $text, string $escape_digits = '', int $frequency = 8000, $voice = null): array {
1023
    if ($buffer == '' || $this->proceedDigits($buffer, $escape_digits)) {
1024
      $res = $this->swift($text, $escape_digits, $frequency, $voice);
1025
      $this->appendBuffer($buffer, $res);
1026
      return $res;
1027
    }
1028
    return ['code' => AGI_Others::AGIRES_OK,
1029
            'result' => $this->ordBuffer($buffer),
1030
            'endpos' => 0];
1031
  }
1032
1033
  /**
1034
   * Say Punctuation in a string.
1035
   * Return early if $buffer is adequate for request.
1036
   *
1037
   * @param string $buffer
1038
   * @param string $text
1039
   * @param string $escape_digits
1040
   * @param integer $frequency
1041
   * @return array, see evaluate for return information.
1042
   */
1043
  function fastpass_say_punctuation(string &$buffer, string $text, string $escape_digits = '', int $frequency = 8000): array {
1044
    if ($buffer == '' || $this->proceedDigits($buffer, $escape_digits)) {
1045
      $res = $this->say_punctuation($text, $escape_digits, $frequency);
1046
      $this->appendBuffer($buffer, $res);
1047
      return $res;
1048
    }
1049
    return ['code' => AGI_Others::AGIRES_OK,
1050
            'result' => $this->ordBuffer($buffer)];
1051
  }
1052
1053
  /**
1054
   * Plays the given file and receives DTMF data.
1055
   * Return early if $buffer is adequate for request.
1056
   *
1057
   * This is similar to STREAM FILE, but this command can accept and return many DTMF digits,
1058
   * while STREAM FILE returns immediately after the first DTMF digit is detected.
1059
   *
1060
   * Asterisk looks for the file to play in /var/lib/asterisk/sounds by default.
1061
   *
1062
   * If the user doesn't press any keys when the message plays, there is $timeout milliseconds
1063
   * of silence then the command ends.
1064
   *
1065
   * The user has the opportunity to press a key at any time during the message or the
1066
   * post-message silence. If the user presses a key while the message is playing, the
1067
   * message stops playing. When the first key is pressed a timer starts counting for
1068
   * $timeout milliseconds. Every time the user presses another key the timer is restarted.
1069
   * The command ends when the counter goes to zero or the maximum number of digits is entered,
1070
   * whichever happens first.
1071
   *
1072
   * If you don't specify a time out then a default timeout of 2000 is used following a pressed
1073
   * digit. If no digits are pressed then 6 seconds of silence follow the message.
1074
   *
1075
   * If you don't specify $max_digits then the user can enter as many digits as they want.
1076
   *
1077
   * Pressing the # key has the same effect as the timer running out: the command ends and
1078
   * any previously keyed digits are returned. A side effect of this is that there is no
1079
   * way to read a # key using this command.
1080
   *
1081
   * @link http://www.voip-info.org/wiki-get+data
1082
   * @param string $buffer
1083
   * @param string $filename file to play. Do not include file extension.
1084
   * @param integer $timeout milliseconds
1085
   * @param integer $max_digits
1086
   * @return array, see evaluate for return information. ['result'] holds the digits and ['data'] holds the timeout if present.
1087
   *
1088
   * This differs from other commands with return DTMF as numbers representing ASCII characters.
1089
   */
1090
  function fastpass_get_data(string &$buffer, string $filename, $timeout = null, $max_digits = null): array {
1091
    if (is_null($max_digits) || strlen($buffer) < $max_digits) {
1092
      if ($buffer == '') {
1093
        $res = $this->get_data($filename, $timeout, $max_digits);
1094
        if ($res['code'] == AGI_Others::AGIRES_OK) {
1095
          $buffer .= $res['result'];
1096
        }
1097
        return $res;
1098
      }
1099
      while (is_null($max_digits) || strlen($buffer) < $max_digits) {
1100
        $res = $this->wait_for_digit();
1101
        if ($res['code'] != AGI_Others::AGIRES_OK) {
1102
          return $res;
1103
        }
1104
        if ($res['result'] == ord('#')) {
1105
          break;
1106
        }
1107
        $buffer .= chr($res['result']);
1108
      }
1109
    }
1110
    return ['code' => AGI_Others::AGIRES_OK,
1111
            'result' => $buffer];
1112
  }
1113
1114
  // *********************************************************************************************************
1115
  // **                             DERIVED                                                                                             **
1116
  // *********************************************************************************************************
1117
1118
  /**
1119
   * Menu.
1120
   *
1121
   * This function presents the user with a menu and reads the response
1122
   *
1123
   * @param array $choices has the following structure:
1124
   *   array('1'=>'*Press 1 for this', // festival reads if prompt starts with *
1125
   *           '2'=>'some-gsm-without-extension',
1126
   *           '*'=>'*Press star for help');
1127
   * @param int $timeout
1128
   * @return mixed key pressed on success, -1 on failure
1129
   */
1130
  function menu(array $choices, int $timeout = 2000) {
1131
    $keys = join('', array_keys($choices));
1132
    $choice = null;
1133
    while (is_null($choice)) {
1134
      foreach ($choices as $prompt) {
1135
        if ($prompt[0] == '*') {
1136
          $ret = $this->text2wav(substr($prompt, 1), $keys);
1137
        } else {
1138
          $ret = $this->stream_file($prompt, $keys);
1139
        }
1140
1141
        if ($ret['code'] != AGI_Others::AGIRES_OK || $ret['result'] == -1) {
1142
          $choice = -1;
1143
          break;
1144
        }
1145
1146
        if ($ret['result'] != 0) {
1147
          $choice = chr($ret['result']);
1148
          break;
1149
        }
1150
      }
1151
1152
      if (is_null($choice)) {
1153
        $ret = $this->get_data('beep', $timeout, 1);
1154
        if ($ret['code'] != AGI_Others::AGIRES_OK || $ret['result'] == -1) {
1155
          $choice = -1;
1156
        } elseif ($ret['result'] != '' && strpos(' ' . $keys, $ret['result'])) {
1157
          $choice = $ret['result'];
1158
        }
1159
      }
1160
    }
1161
    return $choice;
1162
  }
1163
1164
  /**
1165
   * setContext - Set context, extension and priority.
1166
   *
1167
   * @param string $context
1168
   * @param string $extension
1169
   * @param int $priority
1170
   */
1171
  function setContext(string $context, string $extension = 's', int $priority = 1) {
1172
    $this->set_context($context);
1173
    $this->set_extension($extension);
1174
    $this->set_priority($priority);
1175
  }
1176
1177
  /**
1178
   * Parse caller id.
1179
   *
1180
   * @param string $callerid
1181
   * @return array('Name'=>$name, 'Number'=>$number)
1182
   * @example examples/dtmf.php Get DTMF tones from the user and say the digits
1183
   * @example examples/input.php Get text input from the user and say it back
1184
   *
1185
   * "name" <proto:user@server:port>
1186
   *
1187
   */
1188
  function parse_callerid($callerid = null): array {
1189
    if (is_null($callerid)) {
1190
      $callerid = $this->request['agi_callerid'];
1191
    }
1192
1193
    $ret = ['name' => '',
1194
            'protocol' => '',
1195
            'username' => '',
1196
            'host' => '',
1197
            'port' => ''];
1198
    $callerid = trim($callerid);
1199
1200
    if ($callerid[0] == '"' || $callerid[0] == "'") {
1201
      $d = $callerid[0];
1202
      $callerid = explode($d, substr($callerid, 1));
1203
      $ret['name'] = array_shift($callerid);
1204
      $callerid = join($d, $callerid);
1205
    }
1206
1207
    $callerid = explode('@', trim($callerid, '<> '));
1208
    $username = explode(':', array_shift($callerid));
1209
    if (count($username) == 1) {
1210
      $ret['username'] = $username[0];
1211
    } else {
1212
      $ret['protocol'] = array_shift($username);
1213
      $ret['username'] = join(':', $username);
1214
    }
1215
1216
    $callerid = join('@', $callerid);
1217
    $host = explode(':', $callerid);
1218
    if (count($host) == 1) {
1219
      $ret['host'] = $host[0];
1220
    } else {
1221
      $ret['host'] = array_shift($host);
1222
      $ret['port'] = join(':', $host);
1223
    }
1224
1225
    return $ret;
1226
  }
1227
1228
  /**
1229
   * Use festival to read text.
1230
   *
1231
   * @param string $text
1232
   * @param string $escape_digits
1233
   * @param integer $frequency
1234
   * @return array | bool, see evaluate for return information.
1235
   * @example examples/dtmf.php Get DTMF tones from the user and say the digits
1236
   * @example examples/input.php Get text input from the user and say it back
1237
   * @example examples/ping.php Ping an IP address
1238
   *
1239
   * @link http://www.cstr.ed.ac.uk/projects/festival/
1240
   */
1241
  function text2wav(string $text, string $escape_digits = '', int $frequency = 8000) {
1242
    $text = trim($text);
1243
    if ($text == '') {
1244
      return true;
1245
    }
1246
1247
    $hash = md5($text);
1248
    $fname = $this->config['phpagi']['tempdir'] . DIRECTORY_SEPARATOR;
1249
    $fname .= 'text2wav_' . $hash;
1250
1251
    // create wave file
1252
    if (!file_exists("$fname.wav")) {
1253
      // write text file
1254
      if (!file_exists("$fname.txt")) {
1255
        $fp = fopen("$fname.txt", 'w');
1256
        fputs($fp, $text);
1257
        fclose($fp);
1258
      }
1259
1260
      shell_exec("{$this->config['festival']['text2wave']} -F $frequency -o $fname.wav $fname.txt");
1261
    } else {
1262
      touch("$fname.txt");
1263
      touch("$fname.wav");
1264
    }
1265
1266
    // stream it
1267
    $ret = $this->stream_file($fname, $escape_digits);
1268
1269
    // clean up old files
1270
    $delete = time() - 2592000; // 1 month
1271
    foreach (glob($this->config['phpagi']['tempdir'] . DIRECTORY_SEPARATOR . 'text2wav_*') as $file) {
1272
      if (filemtime($file) < $delete) {
1273
        unlink($file);
1274
      }
1275
    }
1276
1277
    return $ret;
1278
  }
1279
1280
  /**
1281
   * Use Cepstral Swift to read text.
1282
   *
1283
   * @link http://www.cepstral.com/
1284
   * @param string $text
1285
   * @param string $escape_digits
1286
   * @param integer $frequency
1287
   * @param $voice
1288
   * @return array | bool, see evaluate for return information.
1289
   */
1290
  function swift(string $text, string $escape_digits = '', int $frequency = 8000, $voice = null) {
1291
    if (!is_null($voice)) {
1292
      $voice = "-n $voice";
1293
    } elseif (isset($this->config['cepstral']['voice'])) {
1294
      $voice = "-n {$this->config['cepstral']['voice']}";
1295
    }
1296
1297
    $text = trim($text);
1298
    if ($text == '') {
1299
      return true;
1300
    }
1301
1302
    $hash = md5($text);
1303
    $fname = $this->config['phpagi']['tempdir'] . DIRECTORY_SEPARATOR;
1304
    $fname .= 'swift_' . $hash;
1305
1306
    // create wave file
1307
    if (!file_exists("$fname.wav")) {
1308
      // write text file
1309
      if (!file_exists("$fname.txt")) {
1310
        $fp = fopen("$fname.txt", 'w');
1311
        fputs($fp, $text);
1312
        fclose($fp);
1313
      }
1314
1315
      shell_exec("{$this->config['cepstral']['swift']} -p audio/channels=1,audio/sampling-rate=$frequency $voice -o $fname.wav -f $fname.txt");
1316
    }
1317
1318
    // stream it
1319
    $ret = $this->stream_file($fname, $escape_digits);
1320
1321
    // clean up old files
1322
    $delete = time() - 2592000; // 1 month
1323
    foreach (glob($this->config['phpagi']['tempdir'] . DIRECTORY_SEPARATOR . 'swift_*') as $file) {
1324
      if (filemtime($file) < $delete) {
1325
        unlink($file);
1326
      }
1327
    }
1328
1329
    return $ret;
1330
  }
1331
1332
  /**
1333
   * Text Input.
1334
   *
1335
   * Based on ideas found at http://www.voip-info.org/wiki-Asterisk+cmd+DTMFToText
1336
   *
1337
   * Example:
1338
   *                  UC   H     LC   i        ,     SP   h     o        w    SP   a    r        e     SP   y        o        u     ?
1339
   *   $string = '*8'.'44*'.'*5'.'444*'.'00*'.'0*'.'44*'.'666*'.'9*'.'0*'.'2*'.'777*'.'33*'.'0*'.'999*'.'666*'.'88*'.'0000*';
1340
   *
1341
   * @link http://www.voip-info.org/wiki-Asterisk+cmd+DTMFToText
1342
   * @example examples/input.php Get text input from the user and say it back
1343
   *
1344
   * @param string $mode
1345
   * @return string
1346
   */
1347
  function text_input(string $mode = 'NUMERIC'): string {
1348
    $alpha = ['k0' => ' ',
1349
              'k00' => ',',
1350
              'k000' => '.',
1351
              'k0000' => '?',
1352
              'k00000' => '0',
1353
              'k1' => '!',
1354
              'k11' => ':',
1355
              'k111' => ';',
1356
              'k1111' => '#',
1357
              'k11111' => '1',
1358
              'k2' => 'A',
1359
              'k22' => 'B',
1360
              'k222' => 'C',
1361
              'k2222' => '2',
1362
              'k3' => 'D',
1363
              'k33' => 'E',
1364
              'k333' => 'F',
1365
              'k3333' => '3',
1366
              'k4' => 'G',
1367
              'k44' => 'H',
1368
              'k444' => 'I',
1369
              'k4444' => '4',
1370
              'k5' => 'J',
1371
              'k55' => 'K',
1372
              'k555' => 'L',
1373
              'k5555' => '5',
1374
              'k6' => 'M',
1375
              'k66' => 'N',
1376
              'k666' => 'O',
1377
              'k6666' => '6',
1378
              'k7' => 'P',
1379
              'k77' => 'Q',
1380
              'k777' => 'R',
1381
              'k7777' => 'S',
1382
              'k77777' => '7',
1383
              'k8' => 'T',
1384
              'k88' => 'U',
1385
              'k888' => 'V',
1386
              'k8888' => '8',
1387
              'k9' => 'W',
1388
              'k99' => 'X',
1389
              'k999' => 'Y',
1390
              'k9999' => 'Z',
1391
              'k99999' => '9'];
1392
    $symbol = ['k0' => '=',
1393
               'k1' => '<',
1394
               'k11' => '(',
1395
               'k111' => '[',
1396
               'k1111' => '{',
1397
               'k11111' => '1',
1398
               'k2' => '@',
1399
               'k22' => '$',
1400
               'k222' => '&',
1401
               'k2222' => '%',
1402
               'k22222' => '2',
1403
               'k3' => '>',
1404
               'k33' => ')',
1405
               'k333' => ']',
1406
               'k3333' => '}',
1407
               'k33333' => '3',
1408
               'k4' => '+',
1409
               'k44' => '-',
1410
               'k444' => '*',
1411
               'k4444' => '/',
1412
               'k44444' => '4',
1413
               'k5' => "'",
1414
               'k55' => '`',
1415
               'k555' => '5',
1416
               'k6' => '"',
1417
               'k66' => '6',
1418
               'k7' => '^',
1419
               'k77' => '7',
1420
               'k8' => "\\",
1421
               'k88' => '|',
1422
               'k888' => '8',
1423
               'k9' => '_',
1424
               'k99' => '~',
1425
               'k999' => '9'];
1426
    $text = '';
1427
    do {
1428
      $command = false;
1429
      $result = $this->get_data('beep');
1430
      foreach (explode('*', $result['result']) as $code) {
1431
        if ($command) {
1432
          switch ($code[0]) {
1433
            case '2':
1434
              $text = substr($text, 0, strlen($text) - 1);
1435
              break; // backspace
1436
            case '5':
1437
              $mode = 'LOWERCASE';
1438
              break;
1439
            case '6':
1440
              $mode = 'NUMERIC';
1441
              break;
1442
            case '7':
1443
              $mode = 'SYMBOL';
1444
              break;
1445
            case '8':
1446
              $mode = 'UPPERCASE';
1447
              break;
1448
            case '9':
1449
              $text = explode(' ', $text);
1450
              unset($text[count($text) - 1]);
1451
              $text = join(' ', $text);
1452
              break; // backspace a word
1453
          }
1454
          $code = substr($code, 1);
1455
          $command = false;
1456
        }
1457
        if ($code == '') {
1458
          $command = true;
1459
        } elseif ($mode == 'NUMERIC') {
1460
          $text .= $code;
1461
        } elseif ($mode == 'UPPERCASE' && isset($alpha['k' . $code])) {
1462
          $text .= $alpha['k' . $code];
1463
        } elseif ($mode == 'LOWERCASE' && isset($alpha['k' . $code])) {
1464
          $text .= strtolower($alpha['k' . $code]);
1465
        } elseif ($mode == 'SYMBOL' && isset($symbol['k' . $code])) {
1466
          $text .= $symbol['k' . $code];
1467
        }
1468
      }
1469
      $this->say_punctuation($text);
1470
    } while (substr($result['result'], -2) == '**');
1471
    return $text;
1472
  }
1473
1474
  /**
1475
   * Say Punctuation in a string.
1476
   *
1477
   * @param string $text
1478
   * @param string $escape_digits
1479
   * @param integer $frequency
1480
   * @return array, see evaluate for return information.
1481
   */
1482
  function say_punctuation(string $text, string $escape_digits = '', int $frequency = 8000): array {
1483
    $ret = "";
1484
    for ($i = 0; $i < strlen($text); $i++) {
1485
      switch ($text[$i]) {
1486
        case ' ':
1487
          $ret .= 'SPACE ';
1488
          break;
1489
        case ',':
1490
          $ret .= 'COMMA ';
1491
          break;
1492
        case '.':
1493
          $ret .= 'PERIOD ';
1494
          break;
1495
        case '?':
1496
          $ret .= 'QUESTION MARK ';
1497
          break;
1498
        case '!':
1499
          $ret .= 'EXPLANATION POINT ';
1500
          break;
1501
        case ':':
1502
          $ret .= 'COLON ';
1503
          break;
1504
        case ';':
1505
          $ret .= 'SEMICOLON ';
1506
          break;
1507
        case '#':
1508
          $ret .= 'POUND ';
1509
          break;
1510
        case '=':
1511
          $ret .= 'EQUALS ';
1512
          break;
1513
        case '<':
1514
          $ret .= 'LESS THAN ';
1515
          break;
1516
        case '(':
1517
          $ret .= 'LEFT PARENTHESIS ';
1518
          break;
1519
        case '[':
1520
          $ret .= 'LEFT BRACKET ';
1521
          break;
1522
        case '{':
1523
          $ret .= 'LEFT BRACE ';
1524
          break;
1525
        case '@':
1526
          $ret .= 'AT ';
1527
          break;
1528
        case '$':
1529
          $ret .= 'DOLLAR SIGN ';
1530
          break;
1531
        case '&':
1532
          $ret .= 'AMPERSAND ';
1533
          break;
1534
        case '%':
1535
          $ret .= 'PERCENT ';
1536
          break;
1537
        case '>':
1538
          $ret .= 'GREATER THAN ';
1539
          break;
1540
        case ')':
1541
          $ret .= 'RIGHT PARENTHESIS ';
1542
          break;
1543
        case ']':
1544
          $ret .= 'RIGHT BRACKET ';
1545
          break;
1546
        case '}':
1547
          $ret .= 'RIGHT BRACE ';
1548
          break;
1549
        case '+':
1550
          $ret .= 'PLUS ';
1551
          break;
1552
        case '-':
1553
          $ret .= 'MINUS ';
1554
          break;
1555
        case '*':
1556
          $ret .= 'ASTERISK ';
1557
          break;
1558
        case '/':
1559
          $ret .= 'SLASH ';
1560
          break;
1561
        case "'":
1562
          $ret .= 'SINGLE QUOTE ';
1563
          break;
1564
        case '`':
1565
          $ret .= 'BACK TICK ';
1566
          break;
1567
        case '"':
1568
          $ret .= 'QUOTE ';
1569
          break;
1570
        case '^':
1571
          $ret .= 'CAROT ';
1572
          break;
1573
        case "\\":
1574
          $ret .= 'BACK SLASH ';
1575
          break;
1576
        case '|':
1577
          $ret .= 'BAR ';
1578
          break;
1579
        case '_':
1580
          $ret .= 'UNDERSCORE ';
1581
          break;
1582
        case '~':
1583
          $ret .= 'TILDE ';
1584
          break;
1585
        default:
1586
          $ret .= $text[$i] . ' ';
1587
          break;
1588
      }
1589
    }
1590
    return $this->text2wav($ret, $escape_digits, $frequency);
1591
  }
1592
1593
  /**
1594
   * Create a new AGI_AsteriskManager.
1595
   */
1596
  function &new_AsteriskManager(): AGI_AsteriskManager {
1597
    $this->asm = new AGI_AsteriskManager(null, $this->config['asmanager']);
1598
    $this->asm->setPagi($this);
1599
    $this->config['asmanager'] = &$this->asm->config['asmanager'];
1600
    return $this->asm;
1601
  }
1602
1603
1604
  // *********************************************************************************************************
1605
  // **                             PRIVATE                                                                                             **
1606
  // *********************************************************************************************************
1607
1608
1609
  /**
1610
   * Evaluate an AGI command.
1611
   *
1612
   * @access private
1613
   * @param string $command
1614
   * @return array ('code'=>$code, 'result'=>$result, 'data'=>$data)
1615
   */
1616
  private function evaluate(string $command): array {
1617
    $broken = ['code' => 500,
1618
               'result' => -1,
1619
               'data' => ''];
1620
1621
    // write command
1622
    if (!@fwrite($this->out, trim($command) . "\n")) {
1623
      return $broken;
1624
    }
1625
    fflush($this->out);
1626
1627
    // Read result.  Occasionally, a command return a string followed by an extra new line.
1628
    // When this happens, our script will ignore the new line, but it will still be in the
1629
    // buffer.  So, if we get a blank line, it is probably the result of a previous
1630
    // command.  We read until we get a valid result or asterisk hangs up.  One offending
1631
    // command is SEND TEXT.
1632
    $count = 0;
1633
    do {
1634
      $str = trim(fgets($this->in, 4096));
1635
    } while ($str == '' && $count++ < 5);
1636
1637
    if ($count >= 5) {
1638
      //          $this->conlog("evaluate error on read for $command");
1639
      return $broken;
1640
    }
1641
1642
    // parse result
1643
    $ret['code'] = substr($str, 0, 3);
1644
    $str = trim(substr($str, 3));
1645
1646
    if ($str[0] == '-') { // we have a multiline response!
1647
      $count = 0;
1648
      $str = substr($str, 1) . "\n";
1649
      $line = fgets($this->in, 4096);
1650
      while (substr($line, 0, 3) != $ret['code'] && $count < 5) {
1651
        $str .= $line;
1652
        $line = fgets($this->in, 4096);
1653
        $count = (trim($line) == '') ? $count + 1 : 0;
1654
      }
1655
      if ($count >= 5) {
1656
        //            $this->conlog("evaluate error on multiline read for $command");
1657
        return $broken;
1658
      }
1659
    }
1660
1661
    $ret['result'] = null;
1662
    $ret['data'] = '';
1663
    if ($ret['code'] != AGI_Others::AGIRES_OK) { // some sort of error
1664
      $ret['data'] = $str;
1665
      $this->conlog(/** @scrutinizer ignore-type */ print_r($ret, true));
1666
    } else { // normal AGIRES_OK response
1667
      $parse = explode(' ', trim($str));
1668
      $in_token = false;
1669
      foreach ($parse as $token) {
1670
        if ($in_token) { // we previously hit a token starting with ')' but not ending in ')'
1671
          $ret['data'] .= ' ' . trim($token, '() ');
1672
          if ($token[strlen($token) - 1] == ')') {
1673
            $in_token = false;
1674
          }
1675
        } elseif ($token[0] == '(') {
1676
          if ($token[strlen($token) - 1] != ')') {
1677
            $in_token = true;
1678
          }
1679
          $ret['data'] .= ' ' . trim($token, '() ');
1680
        } elseif (strpos($token, '=')) {
1681
          $token = explode('=', $token);
1682
          $ret[$token[0]] = $token[1];
1683
        } elseif ($token != '') {
1684
          $ret['data'] .= ' ' . $token;
1685
        }
1686
      }
1687
      $ret['data'] = trim($ret['data']);
1688
    }
1689
1690
    // log some errors
1691
    if ($ret['result'] < 0) {
1692
      $this->conlog("$command returned {$ret['result']}");
1693
    }
1694
1695
    return $ret;
1696
  }
1697
1698
  /**
1699
   * Log to console if debug mode.
1700
   *
1701
   * @param string $str
1702
   * @param integer $vbl verbose level
1703
   * @example examples/ping.php Ping an IP address
1704
   *
1705
   */
1706
  function conlog(string $str, int $vbl = 1) {
1707
    static $busy = false;
1708
1709
    if ($this->config['phpagi']['debug'] != false) {
1710
      if (!$busy) { // no conlogs inside conlog!!!
1711
        $busy = true;
1712
        $this->verbose($str, $vbl);
1713
        $busy = false;
1714
      }
1715
    }
1716
  }
1717
1718
  /**
1719
   * Find an executable in the path.
1720
   *
1721
   * @access private
1722
   * @param string $cmd command to find
1723
   * @param string $checkpath path to check
1724
   * @return string the path to the command
1725
   */
1726
  private function which(string $cmd, $checkpath = null) {
1727
    if (is_null($checkpath)) {
1728
      $chpath = getenv('PATH');
1729
      if ($chpath === false) {
1730
        $chpath = '/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:' .
1731
          '/usr/X11R6/bin:/usr/local/apache/bin:/usr/local/mysql/bin';
1732
      }
1733
    } else {
1734
      $chpath = $checkpath;
1735
    }
1736
1737
    foreach (explode(':', $chpath) as $path) {
1738
      if (is_executable("$path/$cmd")) {
1739
        return "$path/$cmd";
1740
      }
1741
    }
1742
1743
    return false;
1744
  }
1745
1746
  /**
1747
   * Make a folder recursively.
1748
   *
1749
   * @access private
1750
   * @param string $folder
1751
   * @param integer $perms
1752
   * @return boolean
1753
   */
1754
  private function make_folder(string $folder, int $perms = 0755): bool {
1755
    $f = explode(DIRECTORY_SEPARATOR, $folder);
1756
    $base = '';
1757
    for ($i = 0; $i < count($f); $i++) {
1758
      $base .= $f[$i];
1759
      if ($f[$i] != '' && !file_exists($base)) {
1760
        if (mkdir($base, $perms) == false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
1761
          return (false);
1762
        }
1763
      }
1764
      $base .= DIRECTORY_SEPARATOR;
1765
    }
1766
    return (true);
1767
  }
1768
1769
  private function proceedDigits(string $buffer, string $escapeDigits): bool {
1770
    if ($escapeDigits != "" && $buffer != "") {
1771
      if (!strpos(chr(255) . $escapeDigits, $buffer[strlen($buffer) - 1])) {
1772
        return true;
1773
      }
1774
    }
1775
    return false;
1776
  }
1777
1778
  private function ordBuffer(string $buffer): int {
1779
    return ord($buffer[strlen($buffer) - 1]);
1780
  }
1781
1782
  private function appendBuffer(string &$buffer, array $res) {
1783
    if ($res['code'] == AGI_Others::AGIRES_OK && $res['result'] > 0) {
1784
      $buffer .= chr($res['result']);
1785
    }
1786
  }
1787
}
1788
1789