From a36e71ff2a57754fa135a5cbb93c6720b0ca9e9e Mon Sep 17 00:00:00 2001 From: frytimo Date: Fri, 4 Jul 2025 00:52:55 -0300 Subject: [PATCH] Active calls dashboard widget (#7411) * Add context filter * Add dashboard widget * Add config for widget * Add domain filter to service * Fix websocket_service to get the class name * Add new method get_service_name --- .../classes/active_calls_service.php | 11 + .../classes/caller_context_filter.php | 56 +++ .../resources/dashboard/active_calls.php | 473 ++++++++++++++++++ .../resources/dashboard/config.php | 38 ++ .../resources/classes/subscriber.php | 19 + .../resources/classes/websocket_service.php | 20 +- 6 files changed, 609 insertions(+), 8 deletions(-) create mode 100644 app/active_calls/resources/classes/caller_context_filter.php create mode 100644 app/active_calls/resources/dashboard/active_calls.php create mode 100644 app/active_calls/resources/dashboard/config.php diff --git a/app/active_calls/resources/classes/active_calls_service.php b/app/active_calls/resources/classes/active_calls_service.php index c176dba7f7..e4e06eaf88 100644 --- a/app/active_calls/resources/classes/active_calls_service.php +++ b/app/active_calls/resources/classes/active_calls_service.php @@ -176,10 +176,21 @@ class active_calls_service extends service implements websocket_service_interfac * @return filter */ public static function create_filter_chain_for(subscriber $subscriber): filter { + // Do not filter domain + if ($subscriber->has_permission('call_active_all') || $subscriber->is_service()) { + return filter_chain::and_link([ + new event_filter(self::SWITCH_EVENTS), + new permission_filter(self::PERMISSION_MAP, $subscriber->get_permissions()), + new event_key_filter(self::EVENT_KEYS), + ]); + } + + // Filter on single domain name return filter_chain::and_link([ new event_filter(self::SWITCH_EVENTS), new permission_filter(self::PERMISSION_MAP, $subscriber->get_permissions()), new event_key_filter(self::EVENT_KEYS), + new caller_context_filter([$subscriber->get_domain_name()]), ]); } diff --git a/app/active_calls/resources/classes/caller_context_filter.php b/app/active_calls/resources/classes/caller_context_filter.php new file mode 100644 index 0000000000..1a7af3f0cf --- /dev/null +++ b/app/active_calls/resources/classes/caller_context_filter.php @@ -0,0 +1,56 @@ + + * Portions created by the Initial Developer are Copyright (C) 2008-2025 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Mark J Crane + * Tim Fry + */ + +/** + * Description of caller_context_filter + * + * @author Tim Fry + */ +class caller_context_filter implements filter { + + private $domains; + + public function __construct(array $domain_names) { + foreach ($domain_names as $name) { + $this->domains[$name] = true; + } + } + + public function __invoke(string $key, $value): ?bool { + // return true when not on the event key caller_context to validate + if ($key !== 'caller_context') { + return true; + } + // Instruct the filter chain to drop the payload + if (!isset($this->domains[$value])) { + return null; + } + return true; + } + +} diff --git a/app/active_calls/resources/dashboard/active_calls.php b/app/active_calls/resources/dashboard/active_calls.php new file mode 100644 index 0000000000..578dc9c039 --- /dev/null +++ b/app/active_calls/resources/dashboard/active_calls.php @@ -0,0 +1,473 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +//set project root +$project_root = dirname(__DIR__, 4); + +//includes files +require_once "$project_root/resources/require.php"; + +//check permisions +require_once "$project_root/resources/check_auth.php"; +if (!permission_exists('call_active_view')) { + return; +} + +//set the row style +$c = 0; +$row_style["0"] = "row_style0"; +$row_style["1"] = "row_style1"; + +//connect to the database +if (!isset($database)) { + $database = database::new(); +} + +//set the dashboard icon to a solid color phone +$dashboard_icon = 'fa-solid fa-phone'; + +//add multi-lingual support +$text = (new text)->get($_SESSION['domain']['language']['code'], 'app/active_calls'); + +//show the widget +echo "
\n"; + +//create a token +$token = (new token())->create($_SERVER['PHP_SELF']); +echo " \n"; +echo " \n"; + +//subscribe to service +subscriber::save_token($token, ['active.calls']); + +//define row styles +$c = 0; +$row_style["0"] = "row_style0"; +$row_style["1"] = "row_style1"; + +//icon and count +echo "
\n"; + echo "".$text['title']."\n"; + echo "
\n"; + echo "
\n"; + echo "\n"; + echo "0\n"; + echo "
\n"; + echo "
\n"; +echo "
\n"; + +//active call details +echo "
\n"; +if ($dashboard_details_state != 'disabled') { + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "
".$text['label-cid-number']."".$text['label-destination']."".$text['label-status']."
\n"; +} +echo "
\n"; + +//include arrows when not changed +$version = md5(file_get_contents($project_root, '/app/active_calls/resources/javascript/arrow.js')); +echo "\n"; + +?> + + +\n"; ?> diff --git a/app/active_calls/resources/dashboard/config.php b/app/active_calls/resources/dashboard/config.php new file mode 100644 index 0000000000..1472a0c877 --- /dev/null +++ b/app/active_calls/resources/dashboard/config.php @@ -0,0 +1,38 @@ +authenticated; } + /** + * Allows overriding the token authentication + * @param bool $authenticated + * @return self + */ public function set_authenticated(bool $authenticated): self { + $this->authenticated = $authenticated; return $this; } @@ -452,6 +458,14 @@ class subscriber { return $this->service; } + /** + * Alias of service_name without the parameters + * @return string + */ + public function get_service_name(): string { + return $this->service_name; + } + /** * Get or set the service_name * @param string|null $service_name @@ -495,6 +509,11 @@ class subscriber { return isset($this->services[$service_name]); } + /** + * Subscribe to a service by ensuring this subscriber has the appropriate permissions + * @param string $service_name + * @return self + */ public function subscribe(string $service_name): self { $this->services[$service_name] = true; return $this; diff --git a/core/websockets/resources/classes/websocket_service.php b/core/websockets/resources/classes/websocket_service.php index d475939810..c1fd3d288e 100644 --- a/core/websockets/resources/classes/websocket_service.php +++ b/core/websockets/resources/classes/websocket_service.php @@ -193,24 +193,29 @@ class websocket_service extends service { // Authenticate their token if ($subscriber->authenticate_token($message->token)) { $subscriber->send(websocket_message::request_authenticated($message->request_id, $message->service)); + // Check for service authenticated if ($subscriber->is_service()) { $this->info("Service $subscriber->id authenticated"); $this->services[$subscriber->service_name()] = $subscriber; } else { + // Subscriber authenticated $this->info("Client $subscriber->id authenticated"); - $this->info("Setting permissions on $subscriber->id"); $subscriptions = $subscriber->subscribed_to(); - foreach ($subscriber->subscribed_to() as $subscribed_to) { + foreach ($subscriptions as $subscribed_to) { if (isset($this->services[$subscribed_to])) { - $service = $this->services[$subscribed_to]; - if (is_a($service, 'websocket_service_interface', true)) { - $class = $service->get_service_name(); - $filter = $class::create_filter_chain_for($subscriber); + $subscriber_service = $this->services[$subscribed_to]; + $class_name = $subscriber_service->service_class(); + // Make sure we can call the 'create_filter_chain_for' method + if (is_a($class_name, 'websocket_service_interface', true)) { + // Call the service class method to validate the subscriber + $filter = $class_name::create_filter_chain_for($subscriber); if ($filter !== null) { + // Log the filter has been set for the subscriber + $this->info("Set filter for " . $subscriber->id()); $subscriber->set_filter($filter); } } - $this->info("Set permissions for $subscriber->id for service " . $service->service_name()); + $this->info("Set permissions for $subscriber->id for service " . $subscriber_service->service_name()); } } } @@ -256,7 +261,6 @@ class websocket_service extends service { try { // Notify of the message we are broadcasting $this->debug("Broadcasting message '" . $message->payload['event_name'] . "' for service '" . $message->service_name . "' to subscriber $subscriber->id"); - $message->apply_filter($subscriber->get_filter()); $subscriber->send_message($message); } catch (subscriber_token_expired_exception $ste) { $this->info("Subscriber $ste->id token expired");