Files
fusionpbx/app/active_calls/resources/classes/event_message.php
frytimo 2b483ef0cb Fix event_message body always empty (#7660)
Message body was not checked. This will update the get and set methods to ensure if the body is present the body can be set and retrieved.
2025-12-10 14:10:05 -07:00

423 lines
12 KiB
PHP

<?php
/*
* FusionPBX
* Version: MPL 1.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is FusionPBX
*
* The Initial Developer of the Original Code is
* Mark J Crane <markjcrane@fusionpbx.com>
* Portions created by the Initial Developer are Copyright (C) 2008-2025
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Mark J Crane <markjcrane@fusionpbx.com>
* Tim Fry <tim@fusionpbx.com>
*/
/**
* Tracks switch events in an object instead of array
*
* @author Tim Fry <tim@fusionpbx.com>
*/
class event_message implements filterable_payload {
const BODY_ARRAY_KEY = '_body';
const EVENT_SWAP_API = 0x01;
const EVENT_USE_SUBCLASS = 0x02;
// Default keys in the event to capture
public static $keys = [];
/**
* Associative array to store the event with the key name always lowercase and the hyphen replaced with an underscore
* @var array
*/
private $event;
/**
* Body of the SIP MESSAGE used in SMS
* @var string
*/
private $body;
/**
* Only permitted keys on this list are allowed to be inserted in to the event_message object
* @var filter
*/
private $event_filter;
/**
* Creates an event message
* @param array $event_array
* @param filter $filter
*/
public function __construct(array $event_array, ?filter $filter = null) {
// Set the event to an empty array
$this->event = [];
// Clear the memory area for body and key_filter
$this->body = null;
$this->event_filter = $filter;
// Set the event array to match
foreach ($event_array as $name => $value) {
$this->__set($name, $value);
}
}
/**
* Sanitizes the key name and then stores the value in the event property as an associative array
* @param string $name
* @param string $value
* @return void
*/
public function __set(string $name, $value) {
self::sanitize_event_key($name);
// Use the filter chain to ensure the key is allowed
if ($this->event_filter === null || ($this->event_filter)($name, $value)) {
if ($name === self::BODY_ARRAY_KEY) {
$this->body = $value;
return;
}
$this->event[$name] = $value;
}
}
/**
* Sanitizes the key name and then returns the value stored in the event property
* @param string $name Name of the event key
* @return string Returns the stored value or an empty string
*/
public function __get(string $name) {
self::sanitize_event_key($name);
if ($name === 'name') $name = 'event_name';
if ($name === self::BODY_ARRAY_KEY) {
return $this->body;
}
return $this->event[$name] ?? '';
}
/**
* Return an array representation of this object.
*
* @return array
*/
public function __toArray(): array {
$array = [];
foreach ($this->event as $key => $value) {
$array[$key] = $value;
}
return $array;
}
/**
* Convert the current object into an array representation.
*
* @return array
*/
public function to_array(): array {
return $this->__toArray();
}
/**
* Apply a filter to the event collection.
*
* @param filter $filter The filter function to apply
*
* @return self This object for method chaining
*/
public function apply_filter(filter $filter) {
foreach ($this->event as $key => $value) {
$result = ($filter)($key, $value);
if ($result === null) {
$this->event = [];
} elseif (!$result) {
unset($this->event[$key]);
}
}
return $this;
}
/**
* Parse an active calls JSON string and return a list of event messages.
*
* This method expects a JSON string where each row represents an active call, and returns
* a list of event_message objects populated with the relevant details for each call.
*
* @param string $json_string The JSON string to parse.
*
* @return array A list of event_message objects.
*/
public static function parse_active_calls($json_string): array {
$calls = [];
$json_array = json_decode($json_string, true);
if (empty($json_array["rows"])) {
return $calls;
}
foreach ($json_array["rows"] as $call) {
$message = new event_message($call);
// adjust basic info to match an event setting the callstate to ringing
// so that a row can be created for it
$message['event_name'] = 'CHANNEL_CALLSTATE';
$message['answer_state'] = 'ringing';
$message['channel_call_state'] = 'ACTIVE';
$message['unique_id'] = $call['uuid'];
$message['call_direction'] = $call['direction'];
//set the codecs
$message['caller_channel_created_time'] = intval($call['created_epoch']) * 1000000;
$message['channel_read_codec_name'] = $call['read_codec'];
$message['channel_read_codec_rate'] = $call['read_rate'];
$message['channel_write_codec_name'] = $call['write_codec'];
$message['channel_write_codec_rate'] = $call['write_rate'];
//get the profile name
$message['caller_channel_name'] = $call['name'];
//domain or context
$message['caller_context'] = $call['context'];
$message['caller_caller_id_name'] = $call['initial_cid_name'];
$message['caller_caller_id_number'] = $call['initial_cid_num'];
$message['caller_destination_number'] = $call['initial_dest'];
$message['application'] = $call['application'] ?? '';
$message['secure'] = $call['secure'] ?? '';
$calls[] = $message;
}
return $calls;
}
/**
* Creates a websocket_message_event object from a json string
* @param type $json_string
* @return self|null
*/
public static function create_from_json($json_string) {
if (is_array($json_string)) {
print_r(debug_backtrace());
die();
}
$array = json_decode($json_string, true);
if ($array !== false) {
return new static($array);
}
return null;
}
/**
* Creates a new instance from a switch event.
*
* @param array|string $raw_event The raw event data.
* @param filter|null $filter Optional filter to be applied on the created object.
* @param int $flags Flags controlling the creation process (see EVENT_SWAP_API and EVENT_USE_SUBCLASS).
*
* @return self
*/
public static function create_from_switch_event($raw_event, filter $filter = null, $flags = 3): self {
// Set the options from the flags passed
$swap_api_name_with_event_name = ($flags & self::EVENT_SWAP_API) !== 0;
$swap_subclass_event_name_with_event_name = ($flags & self::EVENT_USE_SUBCLASS) !== 0;
// Get the payload and ignore the headers
if (is_array($raw_event) && isset($raw_event['$'])) {
$raw_event = $raw_event['$'];
}
//check if it is still an array
if (is_array($raw_event)) {
$raw_event = array_pop($raw_event);
}
$event_array = [];
foreach (explode("\n", $raw_event) as $line) {
$parts = explode(':', $line, 2);
$key = '';
$value = '';
if (count($parts) > 0) {
$key = $parts[0];
}
if (count($parts) > 1) {
$value = urldecode(trim($parts[1]));
}
if (!empty($key)) {
$event_array[$key] = $value;
}
}
//check for body
if (!empty($event_array['Content-Length'])) {
$event_array['_body'] = substr($raw_event, -1*$event_array['Content-Length']);
}
// Instead of using 'CUSTOM' for the Event-Name we use the actual API-Command when it is available instead
if ($swap_api_name_with_event_name && !empty($event_array['API-Command'])) {
// swap the values
[$event_array['Event-Name'], $event_array['API-Command']] = [$event_array['API-Command'], $event_array['Event-Name']];
}
// Promote the Event-Subclass name to the Event-Name
if ($swap_subclass_event_name_with_event_name && !empty($event_array['Event-Subclass'])) {
// swap the values
[$event_array['Event-Name'], $event_array['Event-Subclass']] = [$event_array['Event-Subclass'], $event_array['Event-Name']];
}
// Return the new object
return new static($event_array, $filter);
}
/**
* Return a Json representation for this object when the object is echoed or printed
* @return string
* @override websocket_message
*/
public function __toString(): string {
return json_encode($this->to_array());
}
/**
* Set or Get the body
* @param null|string $body
* @return self|string
*/
public function body(?string $body = null) {
// Check if we are setting the value for body
if (func_num_args() > 0) {
// Set the value
$this->body = $body;
// Return the object for chaining
return $this;
}
// A request was made to get the value from body
return $this->body;
}
/**
* Convert the event object to an array representation.
*
* This method iterates over the event properties and includes them in the returned array.
* If the body is not null, it will be included as a separate key-value pair in the resulting array.
*
* @return array
*/
public function event_to_array(): array {
$array = [];
foreach ($this->event as $key => $value) {
$array[$key] = $value;
}
if ($this->body !== null) {
$array[self::BODY_ARRAY_KEY] = $this->body;
}
return $array;
}
/**
* Return an iterator for this object.
*
* This method allows iteration over the event data as a Traversable object.
*
* @return \Traversable
*/
public function getIterator(): \Traversable {
yield from $this->event_to_array();
}
/**
* Check if a specific event key exists in this object.
*
* @param mixed $offset The key of the event to check for existence
*
* @return bool True if the event key exists, false otherwise
*/
public function offsetExists(mixed $offset): bool {
self::sanitize_event_key($offset);
return isset($this->event[$offset]);
}
/**
* Return the value associated with the given key from this event object.
*
* If the key is 'body', returns the event body. Otherwise, returns the value
* stored in the 'event' array for the given key.
*
* @param mixed $offset The key to retrieve the value for.
*
* @return mixed The value associated with the given key.
*/
public function offsetGet(mixed $offset): mixed {
self::sanitize_event_key($offset);
if ($offset === self::BODY_ARRAY_KEY) {
return $this->body;
}
return $this->event[$offset];
}
/**
* Set the value for a given offset in this object.
*
* @param mixed $offset The key or index to set the value for. If it is
* {@link self::BODY_ARRAY_KEY}, this method will replace the
* entire body of the event with the provided value.
* @param mixed $value The new value to be associated with the offset.
*
* @return void
*/
public function offsetSet(mixed $offset, mixed $value): void {
self::sanitize_event_key($offset);
if ($offset === self::BODY_ARRAY_KEY) {
$this->body = $value;
} else {
$this->event[$offset] = $value;
}
}
/**
* Unsets a property from the event array.
*
* This method first sanitizes the provided offset using the sanitize_event_key method to prevent potential security vulnerabilities.
* If the sanitized offset is equal to the BODY_ARRAY_KEY, it sets the body property of this object to null.
* Otherwise, it removes the specified key from the event array.
*
* @param mixed $offset The index or key to be unset from the event array.
*/
public function offsetUnset(mixed $offset): void {
self::sanitize_event_key($offset);
if ($offset === self::BODY_ARRAY_KEY) {
$this->body = null;
} else {
unset($this->event[$offset]);
}
}
/**
* Sanitizes key by replacing '-' with '_', converts to lowercase, and only allows digits 0-9 and letters a-z
* @param string $key
* @return string
*/
public static function sanitize_event_key(string &$key) /* : never */ {
$key = preg_replace('/[^a-z0-9_]/', '', str_replace('-', '_', strtolower($key)));
//rewrite 'name' to 'event_name'
if ($key === 'name') $key = 'event_name';
}
}