diff --git a/app/active_calls/active_calls.php b/app/active_calls/active_calls.php
index ff0532cc5..127f3fa1e 100644
--- a/app/active_calls/active_calls.php
+++ b/app/active_calls/active_calls.php
@@ -173,7 +173,9 @@ if (permission_exists('call_active_profile')) {
echo "
" . $text['label-profile'] . " | \n";
}
echo " " . $text['label-duration'] . " | \n";
-echo " " . $text['label-domain'] . " | \n";
+if (permission_exists('call_active_all')) {
+ echo " " . $text['label-domain'] . " | \n";
+}
echo " " . $text['label-cid-name'] . " | \n";
echo " " . $text['label-cid-number'] . " | \n";
echo " " . $text['label-destination'] . " | \n";
@@ -242,7 +244,7 @@ echo "\n";
?>
\n";
INACTIVE: 'black'
}
+ const arrow_color_key = {
+ [colors.RINGING]: 'blue',
+ [colors.CONNECTED]: 'green',
+ [colors.HANGUP]: 'red',
+ [colors.INACTIVE]: 'black',
+ blue: 'blue',
+ green: 'green',
+ red: 'red',
+ black: 'black'
+ };
+
const truncate_application_data_length = get('active_calls', 'truncate_application_data_length', 80); ?>;
const truncate_application_data = truncate_application_data_length > 0;
@@ -323,11 +336,11 @@ echo "\n";
}
let client = null;
- let reconnectAttempts = 0;
+ let reconnect_attempts = 0;
- function connectWebsocket() {
- const maxReconnectDelay = 30000; // 30 seconds
- const baseReconnectDelay = 1000; // 1 second
+ function connect_websocket() {
+ const max_reconnect_delay = 30000; // 30 seconds
+ const base_reconnect_delay = 1000; // 1 second
client = new ws_client(`wss://${window.location.hostname}/websockets/`, authToken);
@@ -339,7 +352,7 @@ echo "\n";
client.ws.addEventListener("open", async () => {
try {
console.log('Connected');
- reconnectAttempts = 0;
+ reconnect_attempts = 0;
const status = document.getElementById('calls_active_count');
status.style.backgroundColor = colors.INACTIVE;
} catch (err) {
@@ -358,7 +371,7 @@ echo "\n";
await client.request('authentication');
console.log('Authentication sent');
const status = document.getElementById('calls_active_count');
- bindEventHandlers(client);
+ bind_event_handlers(client);
console.log('Sent request for calls in progress');
client.request('active.calls', 'in.progress');
status.style.backgroundColor = colors.CONNECTED;
@@ -375,7 +388,7 @@ echo "\n";
console.warn("Websocket Disconnected");
// reconnect to web socket server
- reconnectAttempts++;
+ reconnect_attempts++;
// delay timer to reload page
const auto_reload_seconds = get('active_calls', 'auto_reload_seconds', 0); ?>;
@@ -387,10 +400,13 @@ echo "\n";
}
})
- // wire up “select all” checkbox
- document.getElementById("checkbox_all").addEventListener("change", e => {
- document.querySelectorAll("#calls_active_body input[type=checkbox]").forEach(cb => cb.checked = e.target.checked);
- });
+ // wire up "select all" checkbox only when hangup permission renders it
+ const checkbox_all = document.getElementById("checkbox_all");
+ if (checkbox_all) {
+ checkbox_all.addEventListener("change", e => {
+ document.querySelectorAll("#calls_active_body input[type=checkbox]").forEach(cb => cb.checked = e.target.checked);
+ });
+ }
// Show all listener
@@ -447,7 +463,7 @@ echo "\n";
/////////////////////
// Event Functions //
/////////////////////
- function bindEventHandlers(client) {
+ function bind_event_handlers(client) {
client.onEvent("CHANNEL_CALLSTATE", channel_callstate_event);
client.onEvent("CHANNEL_EXECUTE", channel_execute_event);
@@ -470,6 +486,7 @@ echo "\n";
//create a row for the call
if (row === null) {
new_call(call);
+ row = document.getElementById(uuid) || null;
}
const other_leg_rdnis = call.other_leg_rdnis ?? '';
const other_leg_unique_id = call.other_leg_unique_id ?? '';
@@ -488,7 +505,7 @@ echo "\n";
} else {
if (other_leg_unique_id !== '') {
const matched_call = document.getElementById(other_leg_unique_id);
- if (matched_call.dataset.forced_direction) {
+ if (matched_call && matched_call.dataset.forced_direction) {
replace_arrow_icon(uuid, matched_call.dataset.forced_direction);
}
} else {
@@ -577,7 +594,7 @@ echo "\n";
function playback_start_event(call) {
//console.log(call.event_name, call.unique_id, call);
const tbody = document.getElementById("calls_active_body")
- if (callsMap.has(call.unique_id)) {
+ if (calls_map.has(call.unique_id)) {
const uuid = call.unique_id;
const file = call.playback_file_path;
const file_basename = basename(file);
@@ -592,7 +609,7 @@ echo "\n";
function playback_stop_event(call) {
//console.log(call.event_name, call.unique_id, call);
const tbody = document.getElementById("calls_active_body")
- if (callsMap.has(call.unique_id)) {
+ if (calls_map.has(call.unique_id)) {
const uuid = call.unique_id;
//update application cell
@@ -603,9 +620,9 @@ echo "\n";
//update the application cell
function channel_application_event(call) {
//console.log(call.event_name, call.unique_id, call);
- const tbody = document.getElementById("calls_active_body");
- if (!callsMap.has(call.unique_id)) {
- update_call_element(`application_${uuid}`, call.application_name);
+ if (calls_map.has(call.unique_id)) {
+ const uuid = call.unique_id;
+ update_call_element(`application_${uuid}`, call.application_name ?? '');
}
}
@@ -656,18 +673,19 @@ echo "\n";
//get the table cell
const span = document.getElementById(`arrow_${uuid}`) ?? null;
if (!span) { return; }
- const icon = span.dataset.icon ?? 'local';
+ const icon = (span.dataset.icon && arrows[span.dataset.icon]) ? span.dataset.icon : 'local';
+ const normalized_color = arrow_color_key[color] ?? 'blue';
//nothing to do
- if (color === span.dataset.color) {
+ if (normalized_color === (arrow_color_key[span.dataset.color] ?? span.dataset.color)) {
return;
}
span.dataset.icon = icon;
- span.dataset.color = color;
+ span.dataset.color = normalized_color;
//copy the cached arrow
- const cached_arrow = arrows[icon][color];
+ const cached_arrow = arrows[icon]?.[normalized_color] ?? arrows.local.blue;
const arrow = cached_arrow.cloneNode(true);
//check for exiting arrow and add or replace
@@ -687,7 +705,8 @@ echo "\n";
//get the table cell
const span = document.getElementById(`arrow_${uuid}`) ?? null;
if (!span) { return; }
- const color = span.dataset.color ?? colors.RINGING;
+ const color = arrow_color_key[span.dataset.color] ?? 'blue';
+ const normalized_color = arrows[icon] ? icon : 'local';
if (span.dataset.icon === null) {
@@ -695,15 +714,15 @@ echo "\n";
}
//nothing to do
- if (icon === span.dataset.icon) {
+ if (normalized_color === span.dataset.icon) {
return;
}
- span.dataset.icon = icon;
+ span.dataset.icon = normalized_color;
span.dataset.color = color;
//copy the cached arrow
- const cached_arrow = arrows[icon][color];
+ const cached_arrow = arrows[normalized_color]?.[color] ?? arrows.local.blue;
const arrow = cached_arrow.cloneNode(true);
const span_arrow = span.firstChild ?? null;
@@ -717,13 +736,13 @@ echo "\n";
function new_call(call) {
//console.log(call);
const tbody = document.getElementById("calls_active_body");
- if (!callsMap.has(call.unique_id)) {
+ if (!calls_map.has(call.unique_id)) {
// create the row
const uuid = call.unique_id;
//set the profile
- const profile = call?.caller_channel_name.split('/')[1] ?? '';
+ const profile = call?.caller_channel_name?.split('/')[1] ?? '';
@@ -800,13 +819,17 @@ echo "\n";
// Hide/show domain column
const domain = document.getElementById('th_domain');
- document.getElementById(`caller_context_${call.unique_id}`).style.display = domain.style.display;
+ const caller_context = document.getElementById(`caller_context_${call.unique_id}`);
+ //console.debug('CONTEXT: ', domain, callerContext);
+ if (caller_context) {
+ caller_context.style.display = domain ? domain.style.display : 'none';
+ }
// start the timer
start_duration_timer(call.unique_id, call.caller_channel_created_time);
// add the uuid to the map
- callsMap.set(call.unique_id, row);
+ calls_map.set(call.unique_id, row);
const hangup = document.getElementById('btn_hangup').cloneNode(true);
@@ -851,13 +874,13 @@ echo "\n";
function update_call(call) {
const tbody = document.getElementById("calls_active_body")
- if (callsMap.has(call.unique_id)) {
+ if (calls_map.has(call.unique_id)) {
//set values
const uuid = call.unique_id;
const row = document.getElementById(uuid);
- const caller_channel_name = call?.caller_channel_name.split('/')[1] ?? '';
+ const caller_channel_name = call?.caller_channel_name?.split('/')[1] ?? '';
const caller_context = call.caller_context ?? '';
@@ -907,7 +930,7 @@ echo "\n";
}
function hangup_call(call) {
- const row = callsMap.get(call.unique_id);
+ const row = calls_map.get(call.unique_id);
if (row) {
const uuid = call.unique_id;
remove_button_by_id(`span_hangup_${uuid}`);
@@ -921,7 +944,7 @@ echo "\n";
if (get('active_calls','remove_completed_calls', true) ? 'true': 'false'; ?>) {
row.remove();
}
- callsMap.delete(uuid);
+ calls_map.delete(uuid);
stop_duration_timer_and_update_call_count(uuid);
updateCount();
}
@@ -977,21 +1000,21 @@ echo "\n";
}
function basename(path) {
- return path.replace(/^.*[\\\/]/, '')
+ return path ? path.replace(/^.*[\\\/]/, '') : '';
}
function updateCount() {
const calls_active_count = document.getElementById('calls_active_count');
- let visibleCount = 0;
- callsMap.forEach((row) => {
+ let visible_count = 0;
+ calls_map.forEach((row) => {
if (row.style.display !== 'none') {
- visibleCount++;
+ visible_count++;
}
});
- const totalCount = callsMap.size;
- calls_active_count.textContent = `${visibleCount}`;
+ const total_count = calls_map.size;
+ calls_active_count.textContent = `${visible_count}`;
}
function start_duration_timer(uuid, start_time) {
@@ -1018,12 +1041,12 @@ echo "\n";
}
render();
- const timerId = setInterval(render, 1000);
+ const timer_id = setInterval(render, 1000);
- timers[uuid] = timerId
+ timers[uuid] = timer_id
// Return stop function
- return () => clearInterval(timerId);
+ return () => clearInterval(timer_id);
}
function stop_duration_timer_and_update_call_count(uuid) {
@@ -1040,7 +1063,7 @@ echo "\n";
//////////////////////////
// Start the connection //
//////////////////////////
- connectWebsocket();
+ connect_websocket();
diff --git a/app/active_calls/app_config.php b/app/active_calls/app_config.php
index 6da738c5a..686645669 100644
--- a/app/active_calls/app_config.php
+++ b/app/active_calls/app_config.php
@@ -108,9 +108,11 @@
$y++;
$apps[$x]['permissions'][$y]['name'] = "call_active_application";
$apps[$x]['permissions'][$y]['groups'][] = "superadmin";
+ $apps[$x]['permissions'][$y]['groups'][] = "admin";
$y++;
$apps[$x]['permissions'][$y]['name'] = "call_active_codec";
$apps[$x]['permissions'][$y]['groups'][] = "superadmin";
+ $apps[$x]['permissions'][$y]['groups'][] = "admin";
$y++;
$apps[$x]['permissions'][$y]['name'] = "call_active_secure";
$apps[$x]['permissions'][$y]['groups'][] = "superadmin";
diff --git a/app/active_calls/resources/classes/active_calls_service.php b/app/active_calls/resources/classes/active_calls_service.php
index f84bd8a7b..e1a9cc54c 100644
--- a/app/active_calls/resources/classes/active_calls_service.php
+++ b/app/active_calls/resources/classes/active_calls_service.php
@@ -91,21 +91,26 @@ class active_calls_service extends service implements websocket_service_interfac
'content_type',
];
+ const PERMISSION_PREFIX = 'call_active_';
+
//
// Maps the event key to the permission name
//
const PERMISSION_MAP = [
- 'channel_read_codec_name' => 'call_active_codec',
- 'channel_read_codec_rate' => 'call_active_codec',
- 'channel_write_codec_name' => 'call_active_codec',
- 'channel_write_codec_rate' => 'call_active_codec',
- 'caller_channel_name' => 'call_active_profile',
- 'secure' => 'call_active_secure',
- 'application' => 'call_active_application',
- 'playback_file_path' => 'call_active_application',
- 'variable_current_application'=> 'call_active_application',
- 'channel_presence_id' => 'call_active_view',
- 'caller_context' => 'call_active_domain',
+ 'channel_read_codec_name' => 'call_active_codec',
+ 'channel_read_codec_rate' => 'call_active_codec',
+ 'channel_write_codec_name' => 'call_active_codec',
+ 'channel_write_codec_rate' => 'call_active_codec',
+ 'caller_channel_name' => 'call_active_profile',
+ 'secure' => 'call_active_secure',
+ 'application' => 'call_active_application',
+ 'application_data' => 'call_active_application',
+ 'playback_file_path' => 'call_active_application',
+ 'variable_current_application' => 'call_active_application',
+ 'call_direction' => 'call_active_direction',
+ 'variable_call_direction' => 'call_active_direction',
+ 'channel_presence_id' => 'call_active_view',
+ 'caller_context' => 'call_active_domain',
];
/**
@@ -181,6 +186,7 @@ class active_calls_service extends service implements websocket_service_interfac
public static function create_filter_chain_for(subscriber $subscriber): filter {
// Do not filter domain
if ($subscriber->has_permission('call_active_all') || $subscriber->is_service()) {
+ self::log("Subscriber $subscriber->id has permission to view all active calls", LOG_DEBUG);
return filter_chain::and_link([
new event_filter(self::SWITCH_EVENTS),
new permission_filter(self::PERMISSION_MAP, $subscriber->get_permissions()),
@@ -190,6 +196,7 @@ class active_calls_service extends service implements websocket_service_interfac
// Filter on single domain name
if ($subscriber->has_permission('call_active_domain')) {
+ self::log("Subscriber $subscriber->id has permission to view active calls for their domain", LOG_DEBUG);
return filter_chain::and_link([
new event_filter(self::SWITCH_EVENTS),
new permission_filter(self::PERMISSION_MAP, $subscriber->get_permissions()),
@@ -198,6 +205,7 @@ class active_calls_service extends service implements websocket_service_interfac
]);
}
+ self::log("Subscriber $subscriber->id does not have permission to view all active calls or active calls for their domain. Filtering on extensions.", LOG_DEBUG);
// Filter on extensions
return filter_chain::and_link([
new event_filter(self::SWITCH_EVENTS),
@@ -480,7 +488,7 @@ class active_calls_service extends service implements websocket_service_interfac
// Respond with bad command
if (empty($uuid)) {
- websocket_client::send(websocket_message::request_is_bad($request_id, SERVICE_NAME, 'hangup'));
+ websocket_client::send($this->ws_client->socket(), websocket_message::request_is_bad($request_id, SERVICE_NAME, 'hangup'));
}
$host = self::$switch_host ?? parent::$config->get('switch.event_socket.host', '127.0.0.1');
diff --git a/app/active_calls/resources/javascript/websocket_client.js b/app/active_calls/resources/javascript/websocket_client.js
index 3b5254a21..124bfdd07 100644
--- a/app/active_calls/resources/javascript/websocket_client.js
+++ b/app/active_calls/resources/javascript/websocket_client.js
@@ -102,13 +102,24 @@ class ws_client {
*/
_dispatchEvent(service, env) {
// if service==='event', topic carries the real event name:
- let event = (typeof env === 'string')
- ? JSON.parse(env)
- : env;
+ let event = null;
+ try {
+ event = (typeof env === 'string') ? JSON.parse(env) : env;
+ } catch (err) {
+ console.warn('Ignoring invalid event payload:', err);
+ return;
+ }
+
+ if (event === null || typeof event !== 'object') {
+ return;
+ }
// dispatch event handlers
if (service === 'active.calls') {
const topic = event.event_name;
+ if (!topic) {
+ return;
+ }
let handlers = this._eventHandlers.get(topic) || [];
if (handlers.length === 0) {
diff --git a/core/websockets/resources/classes/websocket_service.php b/core/websockets/resources/classes/websocket_service.php
index 7fbbab472..3505eccec 100644
--- a/core/websockets/resources/classes/websocket_service.php
+++ b/core/websockets/resources/classes/websocket_service.php
@@ -238,6 +238,7 @@ class websocket_service extends service {
$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)) {
+ $this->debug('Creating filter chain for subscriber ' . $subscriber->id . ' for service ' . $subscriber_service->service_name() . ' using class ' . $class_name);
try {
// Call the service class method to validate the subscriber
$filter = $class_name::create_filter_chain_for($subscriber);