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);