Portions created by the Initial Developer are Copyright (C) 2016-2024 the Initial Developer. All Rights Reserved. Contributor(s): Mark J Crane */ /** * xml_cdr class provides methods for adding cdr records to the database * * @method boolean add */ if (!class_exists('xml_cdr')) { class xml_cdr { /** * define variables */ private $settings; private $database; public $array; public $fields; public $setting; public $domain_uuid; public $call_details; public $call_direction; public $status; public $billsec; private $username; private $password; private $json; public $recording_uuid; public $binary; /** * user summary */ public $quick_select; public $start_stamp_begin; public $start_stamp_end; public $include_internal; public $extensions; /** * delete method */ private $app_name; private $app_uuid; private $permission_prefix; private $list_page; private $table; private $uuid_prefix; /** * Used by read_files, xml_array, and save methods */ public $file; /** * Called when the object is created */ public function __construct() { //connect to the database $this->database = new database; //get the email queue settings $this->setting = new settings(); //assign private variables (for delete method) $this->app_name = 'xml_cdr'; $this->app_uuid = '4a085c51-7635-ff03-f67b-86e834422848'; $this->permission_prefix = 'xml_cdr_'; $this->list_page = 'xml_cdr.php'; $this->table = 'xml_cdr'; $this->uuid_prefix = 'xml_cdr_'; } /** * cdr process logging */ public function log($message) { //save the log if enabled is true if ($this->setting->get('log', 'enabled', false)) { //save the log to the php error log if ($this->setting->get('log', 'type') == 'error_log') { error_log($message); } //save the log to the syslog server if ($this->setting->get('log', 'type') == 'syslog') { openlog("XML CDR", LOG_PID | LOG_PERROR, LOG_LOCAL0); syslog(LOG_WARNING, $message); closelog(); } //save the log to the file system if ($this->setting->get('log', 'text') == 'file') { $fp = fopen($this->setting->get('server', 'temp').'/xml_cdr.log', 'a+'); if (!$fp) { return; } fwrite($fp, $message); fclose($fp); } } } /** * cdr fields in the database schema */ public function fields() { $this->fields[] = "xml_cdr_uuid"; $this->fields[] = "domain_uuid"; $this->fields[] = "provider_uuid"; $this->fields[] = "extension_uuid"; $this->fields[] = "sip_call_id"; $this->fields[] = "domain_name"; $this->fields[] = "accountcode"; $this->fields[] = "direction"; $this->fields[] = "default_language"; $this->fields[] = "context"; $this->fields[] = "call_flow"; $this->fields[] = "xml"; $this->fields[] = "json"; $this->fields[] = "missed_call"; $this->fields[] = "caller_id_name"; $this->fields[] = "caller_id_number"; $this->fields[] = "caller_destination"; $this->fields[] = "destination_number"; $this->fields[] = "source_number"; $this->fields[] = "start_epoch"; $this->fields[] = "start_stamp"; $this->fields[] = "answer_stamp"; $this->fields[] = "answer_epoch"; $this->fields[] = "end_epoch"; $this->fields[] = "end_stamp"; $this->fields[] = "duration"; $this->fields[] = "mduration"; $this->fields[] = "billsec"; $this->fields[] = "billmsec"; $this->fields[] = "hold_accum_seconds"; $this->fields[] = "bridge_uuid"; $this->fields[] = "read_codec"; $this->fields[] = "read_rate"; $this->fields[] = "write_codec"; $this->fields[] = "write_rate"; $this->fields[] = "remote_media_ip"; $this->fields[] = "network_addr"; $this->fields[] = "record_path"; $this->fields[] = "record_name"; $this->fields[] = "record_length"; $this->fields[] = "leg"; $this->fields[] = "originating_leg_uuid"; $this->fields[] = "pdd_ms"; $this->fields[] = "rtp_audio_in_mos"; $this->fields[] = "last_app"; $this->fields[] = "last_arg"; $this->fields[] = "voicemail_message"; $this->fields[] = "call_center_queue_uuid"; $this->fields[] = "cc_side"; $this->fields[] = "cc_member_uuid"; $this->fields[] = "cc_queue_joined_epoch"; $this->fields[] = "cc_queue"; $this->fields[] = "cc_member_session_uuid"; $this->fields[] = "cc_agent_uuid"; $this->fields[] = "cc_agent"; $this->fields[] = "cc_agent_type"; $this->fields[] = "cc_agent_bridged"; $this->fields[] = "cc_queue_answered_epoch"; $this->fields[] = "cc_queue_terminated_epoch"; $this->fields[] = "cc_queue_canceled_epoch"; $this->fields[] = "cc_cancel_reason"; $this->fields[] = "cc_cause"; $this->fields[] = "waitsec"; $this->fields[] = "conference_name"; $this->fields[] = "conference_uuid"; $this->fields[] = "conference_member_id"; $this->fields[] = "digits_dialed"; $this->fields[] = "pin_number"; $this->fields[] = "status"; $this->fields[] = "hangup_cause"; $this->fields[] = "hangup_cause_q850"; $this->fields[] = "sip_hangup_disposition"; if (!empty($this->setting->get('cdr', 'field'))) { foreach ($this->setting->get('cdr', 'field') as $field) { $field_name = end(explode(',', $field)); $this->fields[] = $field_name; } } $this->fields = array_unique($this->fields); } /** * save to the database */ public function save() { $this->fields(); $field_count = sizeof($this->fields); //$field_count = sizeof($this->fields); if (!empty($this->array)) { //set the directory if (!empty($this->setting->get('switch', 'log'))) { $xml_cdr_dir = $this->setting->get('switch', 'log').'/xml_cdr'; } //add the temporary permission $p = permissions::new(); $p->add("xml_cdr_add", "temp"); $p->add("xml_cdr_json_add", "temp"); $p->add("xml_cdr_flow_add", "temp"); $p->add("xml_cdr_log_add", "temp"); //save the call details record to the database $this->database->app_name = 'xml_cdr'; $this->database->app_uuid = '4a085c51-7635-ff03-f67b-86e834422848'; //$this->database->domain_uuid = $domain_uuid; $response = $this->database->save($this->array, false); if ($response['code'] == '200') { //saved to the database successfully delete the database file if (!empty($xml_cdr_dir)) { if (file_exists($xml_cdr_dir.'/'.$this->file)) { unlink($xml_cdr_dir.'/'.$this->file); } } } else { //move the file to a failed directory if (!empty($xml_cdr_dir)) { if (!file_exists($xml_cdr_dir.'/failed')) { if (!mkdir($xml_cdr_dir.'/failed', 0660, true)) { die('Failed to create '.$xml_cdr_dir.'/failed'); } } rename($xml_cdr_dir.'/'.$this->file, $xml_cdr_dir.'/failed/'.$this->file); } //send an error message echo 'failed file moved to '.$xml_cdr_dir.'/failed/'.$this->file; } //clear the array unset($this->array); //debug results $this->log(print_r($this->database->message, true)); //remove the temporary permission $p->delete("xml_cdr_add", "temp"); $p->delete("xml_cdr_json_add", "temp"); $p->delete("xml_cdr_flow_add", "temp"); $p->delete("xml_cdr_log_add", "temp"); unset($array); } } /** * process method converts the xml cdr and adds it to the database */ public function xml_array($key, $leg, $xml_string) { //xml string is empty if (empty($xml_string)) { return false; } //fix the xml by escaping the contents of if (defined('STDIN')) { $xml_string = preg_replace_callback("/<([^><]+)>(.*?[><].*?)<\/\g1>/", function ($matches) { return '<' . $matches[1] . '>' . str_replace(">", ">", str_replace("<", "<", $matches[2]) ) . ''; }, $xml_string ); } //remove invalid numeric xml tags $xml_string = preg_replace('/<\/?\d+>/', '', $xml_string); //disable xml entities if (PHP_VERSION_ID < 80000) { libxml_disable_entity_loader(true); } //load the string into an xml object $xml = simplexml_load_string($xml_string, 'SimpleXMLElement', LIBXML_NOCDATA); if ($xml === false) { //set the directory if (!empty($this->setting->get('switch', 'log'))) { $xml_cdr_dir = $this->setting->get('switch', 'log').'/xml_cdr'; } //failed to load the XML, move the XML file to the failed directory if (!empty($xml_cdr_dir)) { if (!file_exists($xml_cdr_dir.'/failed')) { if (!mkdir($xml_cdr_dir.'/failed', 0660, true)) { die('Failed to create '.$xml_cdr_dir.'/failed'); } } rename($xml_cdr_dir.'/'.$this->file, $xml_cdr_dir.'/failed/'.$this->file); } //return without saving the invalid xml return false; } //skip call detail records for calls blocked by call block if (isset($xml->variables->call_block) && !empty($this->setting->get('call_block', 'save_call_detail_record'))) { if ($xml->variables->call_block == 'true' && $this->setting->get('call_block', 'save_call_detail_record') == 'false') { //delete the xml cdr file if (!empty($this->setting->get('switch', 'log'))) { $xml_cdr_dir = $this->setting->get('switch', 'log').'/xml_cdr'; if (file_exists($xml_cdr_dir.'/'.$this->file)) { unlink($xml_cdr_dir.'/'.$this->file); } } //return without saving return false; } } //check for duplicate call uuid's $duplicate_uuid = false; $uuid = urldecode($xml->variables->uuid); if (empty($uuid)) { $uuid = urldecode($xml->variables->call_uuid); } if ($uuid != null && is_uuid($uuid)) { //check for duplicates $sql = "select count(xml_cdr_uuid) "; $sql .= "from v_xml_cdr "; $sql .= "where xml_cdr_uuid = :xml_cdr_uuid "; $parameters['xml_cdr_uuid'] = $uuid; $count = $this->database->select($sql, $parameters, 'column'); if ($count > 0) { //duplicate uuid detected $duplicate_uuid = true; //remove the file as the record already exists in the database if (!empty($this->setting->get('switch', 'log'))) { $xml_cdr_dir = $this->setting->get('switch', 'log').'/xml_cdr'; if (file_exists($xml_cdr_dir.'/'.$this->file)) { unlink($xml_cdr_dir.'/'.$this->file); } } //return without saving return false; } unset($sql, $parameters); } //set the call_direction if (isset($xml->variables->call_direction)) { $call_direction = urldecode($xml->variables->call_direction); } //set the accountcode if (isset($xml->variables->accountcode)) { $accountcode = urldecode($xml->variables->accountcode); } //process call detail record data if ($duplicate_uuid == false && is_uuid($uuid)) { //get the caller ID from call flow caller profile $i = 0; foreach ($xml->callflow as $row) { if ($i == 0) { $caller_id_name = urldecode($row->caller_profile->caller_id_name); $caller_id_number = urldecode($row->caller_profile->caller_id_number); } $i++; } unset($i); //get the caller ID from variables if (!isset($caller_id_number) && isset($xml->variables->caller_id_name)) { $caller_id_name = urldecode($xml->variables->caller_id_name); } if (!isset($caller_id_number) && isset($xml->variables->caller_id_number)) { $caller_id_number = urldecode($xml->variables->caller_id_number); } if (!isset($caller_id_number) && isset($xml->variables->sip_from_user)) { $caller_id_number = urldecode($xml->variables->sip_from_user); } //if the origination caller id name and number are set then use them if (isset($xml->variables->origination_caller_id_name)) { $caller_id_name = urldecode($xml->variables->origination_caller_id_name); } if (isset($xml->variables->origination_caller_id_number)) { $caller_id_number = urldecode($xml->variables->origination_caller_id_number); } //if the caller ID was updated then update the caller ID if (isset($xml->variables->effective_caller_id_name)) { $caller_id_name = urldecode($xml->variables->effective_caller_id_name); } if (isset($xml->variables->effective_caller_id_number)) { $caller_id_number = urldecode($xml->variables->effective_caller_id_number); } //if intercept is used then update use the last sent callee id name and number if (isset($xml->variables->last_app) && $xml->variables->last_app == 'intercept' && !empty($xml->variables->last_sent_callee_id_name)) { $caller_id_name = urldecode($xml->variables->last_sent_callee_id_name); } if (isset($xml->variables->last_app) && $xml->variables->last_app == 'intercept' && !empty($xml->variables->last_sent_callee_id_number)) { $caller_id_number = urldecode($xml->variables->last_sent_callee_id_number); } //if the sip_from_domain and domain_name are not the same then original call direction was inbound //when an inbound call is forward the call_direction is set to inbound and then updated to outbound //use sip_from_display and sip_from_user to get the original caller ID instead of the updated caller ID info from the forward if (isset($xml->variables->sip_from_domain) && urldecode($xml->variables->sip_from_domain) != urldecode($xml->variables->domain_name)) { if (isset($xml->variables->sip_from_display)) { $caller_id_name = urldecode($xml->variables->sip_from_display); } if (isset($xml->variables->sip_from_user)) { $caller_id_number = urldecode($xml->variables->sip_from_user); } } //get the values from the callflow. $i = 0; foreach ($xml->callflow as $row) { if ($i == 0) { $context = urldecode($row->caller_profile->context); $destination_number = urldecode($row->caller_profile->destination_number); $network_addr = urldecode($row->caller_profile->network_addr); } $i++; } unset($i); //remove the provider prefix if (isset($xml->variables->provider_prefix) && isset($destination_number)) { $provider_prefix = $xml->variables->provider_prefix; if ($provider_prefix == substr($destination_number, 0, strlen($provider_prefix))) { $destination_number = substr($destination_number, strlen($provider_prefix), strlen($destination_number)); } } //get the caller_destination if (isset($xml->variables->caller_destination) ) { $caller_destination = urldecode($xml->variables->caller_destination); } if (isset($xml->variables->sip_h_caller_destination) ) { $caller_destination = urldecode($xml->variables->sip_h_caller_destination); } if (!isset($caller_destination) && isset($xml->variables->dialed_user)) { $caller_destination = urldecode($xml->variables->dialed_user); } //set missed calls if (isset($call_direction) && $call_direction == 'inbound' && isset($xml->variables->hangup_cause) && $xml->variables->hangup_cause == 'ORIGINATOR_CANCEL') { $missed_call = 'true'; } if (isset($xml->variables->cc_side) && $xml->variables->cc_side == 'agent') { //call center $missed_call = 'false'; } if (isset($xml->variables->fax_success)) { //fax server $missed_call = 'false'; } if (isset($xml->variables->hangup_cause) && $xml->variables->hangup_cause == 'LOSE_RACE') { //ring group or multi destination bridge statement $missed_call = 'false'; } if (isset($xml->variables->hangup_cause) && $xml->variables->hangup_cause == 'NO_ANSWER' && isset($xml->variables->originating_leg_uuid)) { //ring group or multi destination bridge statement $missed_call = 'false'; } if (isset($xml->variables->cc_side) && $xml->variables->cc_side == 'member' && isset($xml->variables->cc_cause) && $xml->variables->cc_cause == 'cancel') { //call center $missed_call = 'true'; } if (isset($xml->variables->billsec) && $xml->variables->billsec > 0) { //answered call $missed_call = 'false'; } if (isset($xml->variables->destination_number) && substr($xml->variables->destination_number, 0, 3) == '*99') { //voicemail $missed_call = 'true'; } if (isset($xml->variables->voicemail_answer_stamp) && !empty($xml->variables->voicemail_answer_stamp)) { //voicemail $missed_call = 'true'; } if (isset($xml->variables->missed_call) && $xml->variables->missed_call == 'true') { //marked as missed $missed_call = 'true'; } //read the bridge statement variables if (isset($xml->variables->last_app)) { if (urldecode($xml->variables->last_app) == 'bridge') { //get the variables from inside the { and } brackets preg_match('/^\{([^}]+)\}/', urldecode($xml->variables->last_arg), $matches); //create a variables array from the comma delimitted string $bridge_variables = explode(",", $matches[1]); //set bridge variables as variables $x = 0; if (!empty($bridge_variables)) { foreach($bridge_variables as $variable) { $pairs = explode("=", $variable); $name = $pairs[0]; $$name = $pairs[1]; $x++; } } } } //get the last bridge_uuid from the call to preserve previous behavior foreach ($xml->variables->bridge_uuids as $bridge) { $last_bridge = urldecode($bridge); } //determine the call status $failed_array = array( "CALL_REJECTED", "CHAN_NOT_IMPLEMENTED", "DESTINATION_OUT_OF_ORDER", "EXCHANGE_ROUTING_ERROR", "INCOMPATIBLE_DESTINATION", "INVALID_NUMBER_FORMAT", "MANDATORY_IE_MISSING", "NETWORK_OUT_OF_ORDER", "NORMAL_TEMPORARY_FAILURE", "NORMAL_UNSPECIFIED", "NO_ROUTE_DESTINATION", "RECOVERY_ON_TIMER_EXPIRE", "REQUESTED_CHAN_UNAVAIL", "SUBSCRIBER_ABSENT", "SYSTEM_SHUTDOWN", "UNALLOCATED_NUMBER" ); if ($xml->variables->billsec > 0) { $status = 'answered'; } if ($xml->variables->hangup_cause == 'NO_ANSWER') { $status = 'no_answer'; } if (substr($destination_number, 0, 3) == '*99') { $status = 'voicemail'; } if (isset($xml->variables->voicemail_message_seconds) && $xml->variables->voicemail_message_seconds > 0) { $status = 'voicemail'; } if ($xml->variables->hangup_cause == 'ORIGINATOR_CANCEL') { $status = 'cancelled'; } if ($xml->variables->hangup_cause == 'USER_BUSY') { $status = 'busy'; } if (in_array($xml->variables->hangup_cause, $failed_array)) { $status = 'failed'; } if (!isset($status) && in_array($xml->variables->last_bridge_hangup_cause, $failed_array)) { $status = 'failed'; } if ($xml->variables->cc_side == 'agent' && $xml->variables->billsec == 0) { $status = 'no_answer'; } if (!isset($status) && $xml->variables->billsec == 0) { $status = 'no_answer'; } if ($missed_call == 'true') { $status = 'missed'; } //set the key $key = 'xml_cdr'; //get the domain values from the xml $domain_name = urldecode($xml->variables->domain_name); $domain_uuid = urldecode($xml->variables->domain_uuid); //misc $this->array[$key][0]['ring_group_uuid'] = urldecode($xml->variables->ring_group_uuid); $this->array[$key][0]['xml_cdr_uuid'] = $uuid; $this->array[$key][0]['destination_number'] = $destination_number; $this->array[$key][0]['sip_call_id'] = urldecode($xml->variables->sip_call_id); $this->array[$key][0]['source_number'] = urldecode($xml->variables->effective_caller_id_number); $this->array[$key][0]['network_addr'] = urldecode($xml->variables->sip_network_ip); $this->array[$key][0]['missed_call'] = $missed_call; $this->array[$key][0]['caller_id_name'] = $caller_id_name; $this->array[$key][0]['caller_id_number'] = $caller_id_number; $this->array[$key][0]['caller_destination'] = $caller_destination; $this->array[$key][0]['accountcode'] = urldecode($accountcode); $this->array[$key][0]['default_language'] = urldecode($xml->variables->default_language); $this->array[$key][0]['bridge_uuid'] = urldecode($xml->variables->bridge_uuid) ?: $last_bridge; //$this->array[$key][0]['digits_dialed'] = urldecode($xml->variables->digits_dialed); $this->array[$key][0]['sip_hangup_disposition'] = urldecode($xml->variables->sip_hangup_disposition); $this->array[$key][0]['pin_number'] = urldecode($xml->variables->pin_number); $this->array[$key][0]['status'] = $status; //time $start_epoch = urldecode($xml->variables->start_epoch); $this->array[$key][0]['start_epoch'] = $start_epoch; $this->array[$key][0]['start_stamp'] = is_numeric((int)$start_epoch) ? date('c', $start_epoch) : null; $answer_epoch = urldecode($xml->variables->answer_epoch); $this->array[$key][0]['answer_epoch'] = $answer_epoch; $this->array[$key][0]['answer_stamp'] = is_numeric((int)$answer_epoch) ? date('c', $answer_epoch) : null; $end_epoch = urldecode($xml->variables->end_epoch); $this->array[$key][0]['end_epoch'] = $end_epoch; $this->array[$key][0]['end_stamp'] = is_numeric((int)$end_epoch) ? date('c', $end_epoch) : null; $this->array[$key][0]['duration'] = urldecode($xml->variables->billsec); $this->array[$key][0]['mduration'] = urldecode($xml->variables->billmsec); $this->array[$key][0]['billsec'] = urldecode($xml->variables->billsec); $this->array[$key][0]['billmsec'] = urldecode($xml->variables->billmsec); $this->array[$key][0]['hold_accum_seconds'] = urldecode($xml->variables->hold_accum_seconds); //codecs $this->array[$key][0]['read_codec'] = urldecode($xml->variables->read_codec); $this->array[$key][0]['read_rate'] = urldecode($xml->variables->read_rate); $this->array[$key][0]['write_codec'] = urldecode($xml->variables->write_codec); $this->array[$key][0]['write_rate'] = urldecode($xml->variables->write_rate); $this->array[$key][0]['remote_media_ip'] = urldecode($xml->variables->remote_media_ip); $this->array[$key][0]['hangup_cause'] = urldecode($xml->variables->hangup_cause); $this->array[$key][0]['hangup_cause_q850'] = urldecode($xml->variables->hangup_cause_q850); //store the call direction $this->array[$key][0]['direction'] = urldecode($call_direction); //call center if ($xml->variables->cc_member_uuid == '_undef_') { $xml->variables->cc_member_uuid = ''; } if ($xml->variables->cc_member_session_uuid == '_undef_') { $xml->variables->cc_member_session_uuid = ''; } if ($xml->variables->cc_agent_uuid == '_undef_') { $xml->variables->cc_agent_uuid = ''; } if ($xml->variables->call_center_queue_uuid == '_undef_') { $xml->variables->call_center_queue_uuid = ''; } if ($xml->variables->cc_queue_joined_epoch == '_undef_') { $xml->variables->cc_queue_joined_epoch = ''; } $this->array[$key][0]['cc_side'] = urldecode($xml->variables->cc_side); if (!empty($xml->variables->cc_member_uuid) && is_uuid(urldecode($xml->variables->cc_member_uuid))) { $this->array[$key][0]['cc_member_uuid'] = urldecode($xml->variables->cc_member_uuid); } $this->array[$key][0]['cc_queue'] = urldecode($xml->variables->cc_queue); if (!empty($xml->variables->call_center_queue_uuid) && is_uuid(urldecode($xml->variables->call_center_queue_uuid))) { $call_center_queue_uuid = urldecode($xml->variables->call_center_queue_uuid); } if (empty($call_center_queue_uuid) && !empty($xml->variables->cc_queue)) { $sql = "select call_center_queue_uuid from v_call_center_queues "; $sql .= "where domain_uuid = :domain_uuid "; $sql .= "and queue_extension = :queue_extension "; $parameters['domain_uuid'] = $domain_uuid; $parameters['queue_extension'] = explode("@", $xml->variables->cc_queue)[0]; $call_center_queue_uuid = $this->database->select($sql, $parameters, 'column'); unset($parameters); } if (!empty($call_center_queue_uuid) && is_uuid($call_center_queue_uuid)) { $this->array[$key][0]['call_center_queue_uuid'] = $call_center_queue_uuid; } if (!empty($xml->variables->cc_member_session_uuid) && is_uuid(urldecode($xml->variables->cc_member_session_uuid))) { $this->array[$key][0]['cc_member_session_uuid'] = urldecode($xml->variables->cc_member_session_uuid); } if (!empty($xml->variables->cc_agent_uuid) && is_uuid(urldecode($xml->variables->cc_agent_uuid))) { $this->array[$key][0]['cc_agent_uuid'] = urldecode($xml->variables->cc_agent_uuid); } $this->array[$key][0]['cc_agent'] = urldecode($xml->variables->cc_agent); $this->array[$key][0]['cc_agent_type'] = urldecode($xml->variables->cc_agent_type); $this->array[$key][0]['cc_agent_bridged'] = urldecode($xml->variables->cc_agent_bridged); if (!empty($xml->variables->cc_queue_joined_epoch) && is_numeric((int)$xml->variables->cc_queue_joined_epoch)) { $this->array[$key][0]['cc_queue_joined_epoch'] = urldecode($xml->variables->cc_queue_joined_epoch); } if (!empty($xml->variables->cc_queue_answered_epoch) && is_numeric((int)$xml->variables->cc_queue_answered_epoch)) { $this->array[$key][0]['cc_queue_answered_epoch'] = urldecode($xml->variables->cc_queue_answered_epoch); } if (!empty($xml->variables->cc_queue_terminated_epoch) && is_numeric((int)trim($xml->variables->cc_queue_terminated_epoch))) { $this->array[$key][0]['cc_queue_terminated_epoch'] = urldecode($xml->variables->cc_queue_terminated_epoch); } if (!empty($xml->variables->cc_queue_canceled_epoch) && is_numeric((int)$xml->variables->cc_queue_canceled_epoch)) { $this->array[$key][0]['cc_queue_canceled_epoch'] = urldecode($xml->variables->cc_queue_canceled_epoch); } $this->array[$key][0]['cc_cancel_reason'] = urldecode($xml->variables->cc_cancel_reason); $this->array[$key][0]['cc_cause'] = urldecode($xml->variables->cc_cause); $this->array[$key][0]['waitsec'] = urldecode($xml->variables->waitsec); if (urldecode($xml->variables->cc_side) == 'agent') { $this->array[$key][0]['direction'] = 'inbound'; } //set the provider id if (isset($xml->variables->provider_uuid)) { $this->array[$key][0]['provider_uuid'] = urldecode($xml->variables->provider_uuid); } //app info $this->array[$key][0]['last_app'] = urldecode($xml->variables->last_app); $this->array[$key][0]['last_arg'] = urldecode($xml->variables->last_arg); //voicemail message success if (!empty($xml->variables->voicemail_answer_stamp) && $xml->variables->voicemail_message_seconds > 0){ $this->array[$key][0]['voicemail_message'] = "true"; } else { //if ($xml->variables->voicemail_action == "save") { $this->array[$key][0]['voicemail_message'] = "false"; } //conference $this->array[$key][0]['conference_name'] = urldecode($xml->variables->conference_name); $this->array[$key][0]['conference_uuid'] = urldecode($xml->variables->conference_uuid); $this->array[$key][0]['conference_member_id'] = urldecode($xml->variables->conference_member_id); //call quality $rtp_audio_in_mos = urldecode($xml->variables->rtp_audio_in_mos); if (!empty($rtp_audio_in_mos)) { $this->array[$key][0]['rtp_audio_in_mos'] = $rtp_audio_in_mos; } //store the call leg $this->array[$key][0]['leg'] = $leg; //store the originating leg uuid $this->array[$key][0]['originating_leg_uuid'] = urldecode($xml->variables->originating_leg_uuid); //store post dial delay, in milliseconds $this->array[$key][0]['pdd_ms'] = urldecode((int)$xml->variables->progress_mediamsec) + (int)urldecode($xml->variables->progressmsec); //get break down the date to year, month and day $start_stamp = urldecode($xml->variables->start_stamp); $start_time = strtotime($start_stamp); $start_year = date("Y", $start_time); $start_month = date("M", $start_time); $start_day = date("d", $start_time); //get the domain name if (empty($domain_name)) { $domain_name = urldecode($xml->variables->dialed_domain); } if (empty($domain_name)) { $domain_name = urldecode($xml->variables->sip_invite_domain); } if (empty($domain_name)) { $domain_name = urldecode($xml->variables->sip_req_host); } if (empty($domain_name)) { $presence_id = urldecode($xml->variables->presence_id); if (!empty($presence_id)) { $presence_array = explode($presence_id, '%40'); $domain_name = $presence_array[1]; } } //dynamic cdr fields if (!empty($this->setting->get('cdr', 'field'))) { foreach ($this->setting->get('cdr', 'field') as $field) { $fields = explode(",", $field); $field_name = end($fields); $this->fields[] = $field_name; if (!isset($this->array[$key][0][$field_name])) { if (count($fields) == 1) { $this->array[$key][0][$field_name] = urldecode($xml->variables->{$fields[0]}); } if (count($fields) == 2) { $this->array[$key][0][$field_name] = urldecode($xml->{$fields[0]}->{$fields[1]}); } if (count($fields) == 3) { $this->array[$key][0][$field_name] = urldecode($xml->{$fields[0]}->{$fields[1]}->{$fields[2]}); } if (count($fields) == 4) { $this->array[$key][0][$field_name] = urldecode($xml->{$fields[0]}->{$fields[1]}->{$fields[2]}->{$fields[3]}); } if (count($fields) == 5) { $this->array[$key][0][$field_name] = urldecode($xml->{$fields[0]}->{$fields[1]}->{$fields[2]}->{$fields[3]}->{$fields[4]}); } } } } //send the domain name to the cdr log //$this->log("\ndomain_name is `$domain_name`;\ndomain_uuid is '$domain_uuid'\n"); //get the domain_uuid with the domain_name if (empty($domain_uuid)) { $sql = "select domain_uuid from v_domains "; if (empty($domain_name) && $context != 'public' && $context != 'default') { $sql .= "where domain_name = :context "; $parameters['context'] = $context; } else { $sql .= "where domain_name = :domain_name "; $parameters['domain_name'] = $domain_name; } $domain_uuid = $this->database->select($sql, $parameters, 'column'); unset($parameters); } //set values in the database if (!empty($domain_uuid)) { $this->array[$key][0]['domain_uuid'] = $domain_uuid; } if (!empty($domain_name)) { $this->array[$key][0]['domain_name'] = $domain_name; } //get the recording details if (isset($xml->variables->record_path) && isset($xml->variables->record_name)) { $record_path = urldecode($xml->variables->record_path); $record_name = urldecode($xml->variables->record_name); if (isset($xml->variables->record_seconds)) { $record_length = urldecode($xml->variables->record_seconds); } else { $record_length = urldecode($xml->variables->duration); } } elseif (isset($xml->variables->cc_record_filename)) { $record_path = dirname(urldecode($xml->variables->cc_record_filename)); $record_name = basename(urldecode($xml->variables->cc_record_filename)); $record_length = urldecode($xml->variables->record_seconds); } elseif (!isset($record_path) && urldecode($xml->variables->last_app) == "record_session") { $record_path = dirname(urldecode($xml->variables->last_arg)); $record_name = basename(urldecode($xml->variables->last_arg)); $record_length = urldecode($xml->variables->record_seconds); } elseif (!empty($xml->variables->sofia_record_file)) { $record_path = dirname(urldecode($xml->variables->sofia_record_file)); $record_name = basename(urldecode($xml->variables->sofia_record_file)); $record_length = urldecode($xml->variables->record_seconds); } elseif (!empty($xml->variables->api_on_answer)) { $command = str_replace("\n", " ", urldecode($xml->variables->api_on_answer)); $parts = explode(" ", $command); if ($parts[0] == "uuid_record") { $recording = $parts[3]; $record_path = dirname($recording); $record_name = basename($recording); $record_length = urldecode($xml->variables->duration); } } elseif (!empty($xml->variables->conference_recording)) { $conference_recording = urldecode($xml->variables->conference_recording); $record_path = dirname($conference_recording); $record_name = basename($conference_recording); $record_length = urldecode($xml->variables->duration); } elseif (!empty($xml->variables->current_application_data)) { $commands = explode(",", urldecode($xml->variables->current_application_data)); foreach ($commands as $command) { $cmd = explode("=", $command); if ($cmd[0] == "api_on_answer") { $a = explode("]", $cmd[1]); $command = str_replace("'", "", $a[0]); $parts = explode(" ", $command); if ($parts[0] == "uuid_record") { $recording = $parts[3]; $record_path = dirname($recording); $record_name = basename($recording); $record_length = urldecode($xml->variables->duration); } } } } //check to see if file exists with the default file name and path if (empty($record_name)) { $path = $this->setting->get('switch', 'recordings').'/'.$domain_name.'/archive/'.$start_year.'/'.$start_month.'/'.$start_day; if (file_exists($path.'/'.$uuid.'.wav')) { $record_path = $path; $record_name = $uuid.'.wav'; $record_length = urldecode($xml->variables->duration); } elseif (file_exists($path.'/'.$uuid.'.mp3')) { $record_path = $path; $record_name = $uuid.'.mp3'; $record_length = urldecode($xml->variables->duration); } } //last check - check to see if file exists with the bridge_uuid for the file name and path if (empty($record_name)) { $bridge_uuid = urldecode($xml->variables->bridge_uuid) ?: $last_bridge; $path = $this->setting->get('switch', 'recordings').'/'.$domain_name.'/archive/'.$start_year.'/'.$start_month.'/'.$start_day; if (file_exists($path.'/'.$bridge_uuid.'.wav')) { $record_path = $path; $record_name = $bridge_uuid.'.wav'; $record_length = urldecode($xml->variables->duration); } elseif (file_exists($path.'/'.$bridge_uuid.'.mp3')) { $record_path = $path; $record_name = $bridge_uuid.'.mp3'; $record_length = urldecode($xml->variables->duration); } } //debug information //echo "line: ".__line__; //echo "record_path: ".$record_path."\n"; //echo "record_name: ".$record_name."\n"; //echo "record_length: ".$record_length."\n"; //exit; //add the call record path, name and length to the database if (isset($record_path) && isset($record_name) && file_exists($record_path.'/'.$record_name)) { $this->array[$key][0]['record_path'] = $record_path; $this->array[$key][0]['record_name'] = $record_name; if (isset($record_length)) { $this->array[$key][0]['record_length'] = $record_length; } else { $this->array[$key][0]['record_length'] = urldecode($xml->variables->duration); } } //save the xml object to json $this->json = json_encode($xml); //save to the database in xml format if ($this->setting->get('cdr', 'format') == "xml" && $this->setting->get('cdr', 'storage') == "db") { $this->array[$key][0]['xml'] = $xml_string; } //build the call detail array with json decode $this->call_details = json_decode($this->json, true); //get the extension_uuid and then add it to the database fields array if (isset($xml->variables->extension_uuid)) { $this->array[$key][0]['extension_uuid'] = urldecode($xml->variables->extension_uuid); } else { if (isset($domain_uuid) && isset($xml->variables->dialed_user)) { $sql = "select extension_uuid from v_extensions "; $sql .= "where domain_uuid = :domain_uuid "; $sql .= "and (extension = :dialed_user or number_alias = :dialed_user) "; $parameters['domain_uuid'] = $domain_uuid; $parameters['dialed_user'] = $xml->variables->dialed_user; $extension_uuid = $this->database->select($sql, $parameters, 'column'); $this->array[$key][0]['extension_uuid'] = $extension_uuid; unset($parameters); } if (isset($domain_uuid) && isset($xml->variables->referred_by_user)) { $sql = "select extension_uuid from v_extensions "; $sql .= "where domain_uuid = :domain_uuid "; $sql .= "and (extension = :referred_by_user or number_alias = :referred_by_user) "; $parameters['domain_uuid'] = $domain_uuid; $parameters['referred_by_user'] = $xml->variables->referred_by_user; $extension_uuid = $this->database->select($sql, $parameters, 'column'); $this->array[$key][0]['extension_uuid'] = $extension_uuid; unset($parameters); } if (isset($domain_uuid) && isset($xml->variables->last_sent_callee_id_number)) { $sql = "select extension_uuid from v_extensions "; $sql .= "where domain_uuid = :domain_uuid "; $sql .= "and (extension = :last_sent_callee_id_number or number_alias = :last_sent_callee_id_number) "; $parameters['domain_uuid'] = $domain_uuid; $parameters['last_sent_callee_id_number'] = $xml->variables->last_sent_callee_id_number; $extension_uuid = $this->database->select($sql, $parameters, 'column'); $this->array[$key][0]['extension_uuid'] = $extension_uuid; unset($parameters); } } //save the call flow json $key = 'xml_cdr_flow'; $this->array[$key][0]['xml_cdr_flow_uuid'] = uuid(); $this->array[$key][0]['xml_cdr_uuid'] = $uuid; $this->array[$key][0]['domain_uuid'] = $domain_uuid ?? ''; $this->array[$key][0]['call_flow'] = json_encode($this->call_flow()); //save to the database in json format if ($this->setting->get('cdr', 'format') == "json" && $this->setting->get('cdr', 'storage') == "db") { $key = 'xml_cdr_json'; $this->array[$key][0]['xml_cdr_json_uuid'] = uuid(); $this->array[$key][0]['xml_cdr_uuid'] = $uuid; $this->array[$key][0]['domain_uuid'] = $domain_uuid ?? ''; $this->array[$key][0]['json'] = $this->json; } //save the call log to the database if ($this->setting->get('cdr', 'call_log_enabled', false) && !empty($this->setting->get('switch', 'log')) && $this->setting->get('cdr', 'storage') == "db") { //get the log content $log_content = ''; $handle = @fopen($this->setting->get('switch', 'log').'/freeswitch.log', "r"); if ($handle) { while (!feof($handle)) { $line = stream_get_line($handle, 0, "\n"); if (substr($line, 0, 36 ) === $uuid) { $log_content .= substr($line, 37, strlen($line))."\n"; } } fclose($handle); } //save to the database if (!empty($log_content)) { $key = 'xml_cdr_logs'; $this->array[$key][0]['xml_cdr_log_uuid'] = uuid(); $this->array[$key][0]['xml_cdr_uuid'] = $uuid; $this->array[$key][0]['domain_uuid'] = $domain_uuid ?? ''; $this->array[$key][0]['log_date'] = 'now()'; $this->array[$key][0]['log_content'] = $log_content; } } //store xml cdr on the file system as a file if ($this->setting->get('cdr', 'storage') == "dir" && $error != "true") { if (!empty($uuid)) { $tmp_dir = $this->setting->get('switch', 'log').'/xml_cdr/archive/'.$start_year.'/'.$start_month.'/'.$start_day; if(!file_exists($tmp_dir)) { mkdir($tmp_dir, 0770, true); } if ($this->setting->get('cdr', 'format') == "xml") { $tmp_file = $uuid.'.xml'; $fh = fopen($tmp_dir.'/'.$tmp_file, 'w'); fwrite($fh, $xml_string); } else { $tmp_file = $uuid.'.json'; $fh = fopen($tmp_dir.'/'.$tmp_file, 'w'); fwrite($fh, json_encode($xml)); } fclose($fh); } } unset($error); //save data to the database $this->save(); //debug //GLOBAL $insert_time,$insert_count; //$insert_time+=microtime(true)-$time5_insert; //add this current query. //$insert_count++; } //if ($duplicate_uuid == false) } //function xml_array /** * Build a call flow array based on call details. * * This method constructs an array that represents the call flow, utilizing the provided call_details array. Reverses the array to put the events in chronological order and adds profile end times. * * @return array The call flow array. */ public function call_flow() { //save the call flow to the database if (isset($this->call_details['callflow'])) { //set the call flow array $call_flow_array = $this->call_details['callflow']; //normalize the array if (!isset($call_flow_array[0])) { $tmp = $call_flow_array; unset($call_flow_array); $call_flow_array[0] = $tmp; } //reverse the array to put events in chronological order $call_flow_array = array_reverse($call_flow_array); //add the profile end time to the call flow array $i = 0; foreach ($call_flow_array as $row) { //set the profile end time if (isset($call_flow_array[$i+1]["times"]["profile_created_time"])) { $call_flow_array[$i]["times"]["profile_end_time"] = $call_flow_array[$i+1]["times"]["profile_created_time"]; } else { $call_flow_array[$i]["times"]["profile_end_time"] = urldecode($this->call_details['variables']['end_uepoch']); } $i++; } //format the times in the call flow array and add the profile duration $i = 0; foreach ($call_flow_array as $row) { foreach ($row["times"] as $name => $value) { if ($value > 0) { $call_flow_array[$i]["times"]["profile_duration_seconds"] = round(((int) $call_flow_array[$i]["times"]["profile_end_time"])/1000000 - ((int) $call_flow_array[$i]["times"]["profile_created_time"])/1000000); $call_flow_array[$i]["times"]["profile_duration_formatted"] = gmdate("G:i:s", (int) $call_flow_array[$i]["times"]["profile_duration_seconds"]); } } $i++; } //add the call_flow to the array return $call_flow_array; } } /** * Build a call flow summary array based on call summary * * This method constructs an array that represents the call flow summary using the call flow array array. The call flow summary array contains a simplified view of the call flow. * * @return array The call flow summary array. */ public function call_flow_summary($call_flow_array) { //set the time zone if (!empty($this->setting->get('domain', 'time_zone'))) { $time_zone = $this->setting->get('domain', 'time_zone'); } else { $time_zone = date_default_timezone_get(); } //set the time zone for php date_default_timezone_set($time_zone); //get the destination select list $destination = new destinations; $destination_array = $destination->get('dialplan'); //add new rows when callee_id_number exists $new_rows = 0; foreach ($call_flow_array as $key => $row) { //for outbound calls update the times if the bridged_time to remove the call setup plus the ring time if ($this->call_direction === 'outbound') { if (isset($row["times"]["bridged_time"]) and $row["times"]["bridged_time"] > 0) { //change the end time for the current row $call_flow_array[$key]["times"]["profile_created_time"] = $row["times"]["bridged_time"]; } } //add a new row to the call summary if (!empty($row["caller_profile"]["destination_number"]) and !empty($row["caller_profile"]["callee_id_number"]) and $this->call_direction !== 'outbound' and $row["caller_profile"]["destination_number"] !== $row["caller_profile"]["callee_id_number"]) { //build the base of the new_row array $new_row["caller_profile"]["destination_number"] = $row["caller_profile"]["callee_id_number"]; $new_row["caller_profile"]["caller_id_name"] = $row["caller_profile"]["callee_id_name"]; $new_row["caller_profile"]["caller_id_number"] = $row["caller_profile"]["caller_id_number"]; $new_row['times']["profile_created_time"] = $row["times"]["profile_created_time"]; $new_row['times']["profile_end_time"] = $row["times"]["profile_end_time"]; //update the times if the transfer_time exists. The order of this is important add new row needs to be set before this code if (isset($row["times"]["transfer_time"]) and $row["times"]["transfer_time"] > 0) { //change the end time for the current row $call_flow_array[$key+$new_rows]["times"]["profile_end_time"] = $row["times"]["transfer_time"]; //change the created time for the new row $new_row['times']["profile_created_time"] = $row["times"]["transfer_time"]; } //update the times if the bridged_time exists. The order of this is important add new row needs to be set before this code, and transfer_time needs to be before bridge_time if (isset($row["times"]["bridged_time"]) and $row["times"]["bridged_time"] > 0) { //change the end time for the current row $call_flow_array[$key+$new_rows]["times"]["profile_end_time"] = $row["times"]["bridged_time"]; //change the created time for the new row $new_row['times']["profile_created_time"] = $row["times"]["bridged_time"]; } //increment the new row id $new_rows++; //insert the new row into the array without overwriting an existing row array_splice($call_flow_array, $key+$new_rows, 0, [$new_row]); //clean up unset($new_row); } } //format the times in the call flow array $i = 0; foreach ($call_flow_array as $key => $row) { foreach ($row["times"] as $name => $value) { if ($value > 0) { $call_flow_array[$i]["times"][$name.'stamp'] = date("Y-m-d H:i:s", round((float) $value / 1000000, 0)); } } $i++; } //build the call flow summary $x = 0; $skip_row = false; if (!empty($call_flow_array)) { foreach ($call_flow_array as $row) { //skip this row if ($skip_row) { $skip_row = false; continue; } //get the application array if (!empty($destination_array) && !empty($row["caller_profile"]["destination_number"])) { if ($this->call_direction == 'outbound') { $app = $this->find_app($destination_array, urldecode($row["caller_profile"]["username"])); } else { $app = $this->find_app($destination_array, urldecode($row["caller_profile"]["destination_number"])); } } //call centers if (!empty($app['application']) && $app['application'] == 'call_centers') { if (isset($row["caller_profile"]["transfer_source"])) { $app['status'] = 'answered'; //Out } else { $app['status'] = 'waited'; //In } } //call flows if (!empty($app['application']) && $app['application'] == 'call_flows') { $app['status'] = 'routed'; } //conferences if (!empty($app['application']) && $app['application'] == 'conferences') { $app['status'] = 'answered'; } //destinations if (!empty($app['application']) && $app['application'] == 'destinations') { $app['status'] = 'routed'; } //extensions if (!empty($app['application']) && $app['application'] == 'extensions') { if (!empty($row["times"]["profile_created_time"]) && !empty($row["times"]["profile_end_time"]) && (floor($row["times"]["profile_end_time"] / 1000000) - floor($row["times"]["profile_created_time"] / 1000000)) > 0) { $app['status'] = 'answered'; } else { $app['status'] = 'missed'; } } //ivr menus if (!empty($app['application']) && $app['application'] == 'ivr_menus') { $app['status'] = 'routed'; } //outbound routes if ($this->call_direction == 'outbound') { $status = 'missed'; if (!empty($row["times"]["answered_time"])) { $status = 'answered'; } if (!empty($row["caller_profile"]["username"])) { //add to the application array $app['application'] = 'extensions'; $app['source'] = $row["caller_profile"]["username"]; $app['status'] = $status; $app['name'] = ''; $app['label'] = 'extensions'; } elseif (empty($app['application'])) { $app['application'] = 'diaplans'; $app['uuid'] = ''; $app['status'] = $status; $app['name'] = 'Outbound'; $app['label'] = 'Outbound'; } } //ring groups if (!empty($app['application']) && $app['application'] == 'ring_groups') { $app['status'] = 'waited'; } //time conditions if (!empty($app['application']) && $app['application'] == 'time_conditions') { $app['status'] = 'routed'; } //valet park if ( !empty($row["caller_profile"]["destination_number"]) && ( substr($row["caller_profile"]["destination_number"], 0, 4) == 'park' || ( substr($row["caller_profile"]["destination_number"], 0, 3) == '*59' && strlen($row["caller_profile"]["destination_number"]) > 3 ) ) ) { //add items to the app array $app['application'] = 'dialplans'; $app['uuid'] = '46ae6d82-bb83-46a3-901d-33d0724347dd'; $app['name'] = 'Park'; $app['label'] = 'Park'; //set the call park status if (strpos($row["caller_profile"]["transfer_source"], 'park+') !== false) { //$app['status'] = 'In'; $app['status'] = 'parked'; //skip the next row $skip_row = true; } else { //$app['status'] = 'Out'; $app['status'] = 'unparked'; } } //conference if (!empty($app['application']) && $app['application'] == 'conferences') { $skip_row = true; } //voicemails if (!empty($app['application']) && $app['application'] == 'voicemails') { $app['status'] = 'voicemail'; } //debug - add the callee_id_number to the end of the status if (isset($_REQUEST['debug']) && $_REQUEST['debug'] == 'true' && !empty($row["caller_profile"]["destination_number"]) && !empty($row["caller_profile"]["callee_id_number"]) && $row["caller_profile"]["destination_number"] !== $row["caller_profile"]["callee_id_number"]) { $app['status'] .= ' ('.$row["caller_profile"]["callee_id_number"].')'; } //build the application urls $destination_url = "/app/".($app['application'] ?? '')."/".$destination->singular($app['application'] ?? '')."_edit.php?id=".($app["uuid"] ?? ''); $application_url = "/app/".($app['application'] ?? '')."/".($app['application'] ?? '').".php"; if (!empty($app['application']) && $app['application'] == 'call_centers') { $destination_url = "/app/".($app['application'] ?? '')."/".$destination->singular($app['application'] ?? '')."_queue_edit.php?id=".($app["uuid"] ?? ''); $application_url = "/app/".($app['application'] ?? '')."/".$destination->singular($app['application'] ?? '')."_queues.php"; } //add the application and destination details $language2 = new text; $text2 = $language2->get($this->setting->get('domain', 'language'), 'app/'.($app['application'] ?? '')); $call_flow_summary[$x]["application_name"] = ($app['application'] ?? ''); $call_flow_summary[$x]["application_label"] = trim($text2['title-'.($app['application'] ?? '')] ?? ''); $call_flow_summary[$x]["call_direction"] = $this->call_direction; $call_flow_summary[$x]["application_url"] = $application_url; if ($this->call_direction == 'outbound') { $call_flow_summary[$x]["source_uuid"] = ($app['uuid'] ?? ''); $call_flow_summary[$x]["source_number"] = $app['source']; $call_flow_summary[$x]["source_label"] = ($app['label'] ?? ''); $call_flow_summary[$x]["source_url"] = $destination_url; $call_flow_summary[$x]["source_name"] = $app['description'] ?? ''; //$call_flow_summary[$x]["source_description"] = $app['description'] ?? ''; $call_flow_summary[$x]["destination_uuid"] = ''; $call_flow_summary[$x]["destination_number"] = ''; $call_flow_summary[$x]["destination_label"] = ''; $call_flow_summary[$x]["destination_url"] = ''; $call_flow_summary[$x]["destination_description"] = ''; } else { $call_flow_summary[$x]["source_uuid"] = ''; $call_flow_summary[$x]["source_number"] = ''; $call_flow_summary[$x]["source_label"] = ''; $call_flow_summary[$x]["source_url"] = ''; $call_flow_summary[$x]["destination_name"] = ($app['description'] ?? ''); $call_flow_summary[$x]["destination_uuid"] = ($app['uuid'] ?? ''); $call_flow_summary[$x]["destination_label"] = ($app['label'] ?? ''); $call_flow_summary[$x]["destination_url"] = $destination_url; //$call_flow_summary[$x]["destination_description"] = $app['description'] ?? ''; } $call_flow_summary[$x]["destination_number"] = $row["caller_profile"]["destination_number"]; $call_flow_summary[$x]["destination_status"] = ($app['status'] ?? ''); $call_flow_summary[$x]["destination_description"] = $app['description'] ?? ''; //$call_flow_summary[$x]["application"] = $app; //set the start and epoch $profile_created_epoch = $row['times']['profile_created_time'] / 1000000; $profile_end_epoch = $row['times']['profile_end_time'] / 1000000; //add the call flow times $call_flow_summary[$x]["start_epoch"] = round($profile_created_epoch); $call_flow_summary[$x]["end_epoch"] = round($profile_end_epoch); $call_flow_summary[$x]["start_stamp"] = date("Y-m-d H:i:s", $profile_created_epoch); $call_flow_summary[$x]["end_stamp"] = date("Y-m-d H:i:s", $profile_end_epoch); $call_flow_summary[$x]["duration_seconds"] = round($profile_end_epoch - $profile_created_epoch); $call_flow_summary[$x]["duration_formatted"] = gmdate("G:i:s",(int) $call_flow_summary[$x]["duration_seconds"]); unset($app); $x++; } } unset($x); //set the last status to match the call detail record $call_flow_summary[count($call_flow_summary)-1]['destination_status'] = $this->status; //return the call flow summary array return $call_flow_summary; } //add a function to return the find_app public function find_app($destination_array, $detail_action) { //add the destinations to the destination array $sql = "select * from v_destinations "; $sql .= "where (domain_uuid = :domain_uuid or domain_uuid is null) "; $parameters['domain_uuid'] = $this->domain_uuid; $destinations = $this->database->select($sql, $parameters, 'all'); if (!empty($destinations)) { $i = 0; foreach($destinations as $row) { $destination_array['destinations'][$i]['application'] = 'destinations'; $destination_array['destinations'][$i]['destination_uuid'] = $row["destination_uuid"]; $destination_array['destinations'][$i]['uuid'] = $row["destination_uuid"]; $destination_array['destinations'][$i]['dialplan_uuid'] = $row["dialplan_uuid"]; $destination_array['destinations'][$i]['destination_type'] = $row["destination_type"]; $destination_array['destinations'][$i]['destination_prefix'] = $row["destination_prefix"]; $destination_array['destinations'][$i]['destination_number'] = $row["destination_number"]; $destination_array['destinations'][$i]['extension'] = $row["destination_prefix"] . $row["destination_number"]; $destination_array['destinations'][$i]['destination_trunk_prefix'] = $row["destination_trunk_prefix"]; $destination_array['destinations'][$i]['destination_area_code'] = $row["destination_area_code"]; $destination_array['destinations'][$i]['context'] = $row["destination_context"]; $destination_array['destinations'][$i]['label'] = $row["destination_description"]; $destination_array['destinations'][$i]['destination_enabled'] = $row["destination_enabled"]; $destination_array['destinations'][$i]['name'] = $row["destination_description"]; $destination_array['destinations'][$i]['description'] = $row["destination_description"]; //$destination_array[$i]['destination_caller_id_name'] = $row["destination_caller_id_name"]; //$destination_array[$i]['destination_caller_id_number'] = $row["destination_caller_id_number"]; $i++; } } unset($sql, $parameters, $row); $result = ''; if (!empty($destination_array)) { foreach($destination_array as $application => $row) { if (!empty($row)) { foreach ($row as $key => $value) { //find matching destinations if ($application == 'destinations') { if ('+'.($value['destination_prefix'] ?? '').$value['destination_number'] == $detail_action || ($value['destination_prefix'] ?? '').$value['destination_number'] == $detail_action || $value['destination_number'] == $detail_action || ($value['destination_trunk_prefix'] ?? '').$value['destination_number'] == $detail_action || '+'.($value['destination_prefix'] ?? '').($value['destination_area_code'] ?? '').$value['destination_number'] == $detail_action || ($value['destination_prefix'] ?? '').($value['destination_area_code'] ?? '').$value['destination_number'] == $detail_action || ($value['destination_area_code'] ?? '').$value['destination_number'] == $detail_action) { if (file_exists($_SERVER["PROJECT_ROOT"]."/app/".$application."/app_languages.php")) { $value['application'] = $application; return $value; } } } //find all other matching actions if (!empty($value['extension']) && $value['extension'] == $detail_action || preg_match('/^'.preg_quote($value['extension'] ?? '').'$/', $detail_action)) { if (file_exists($_SERVER["PROJECT_ROOT"]."/app/".$application."/app_languages.php")) { $value['application'] = $application; return $value; } } } } } } } /** * get xml from the filesystem and save it to the database */ public function read_files() { $xml_cdr_dir = $this->setting->get('switch', 'log').'/xml_cdr'; $dir_handle = opendir($xml_cdr_dir); $x = 0; while($file = readdir($dir_handle)) { if ($file != '.' && $file != '..') { //used to test a single file //$file = 'a_aa76e0af-461e-4d46-be23-433260307ede.cdr.xml'; //process the XML files if ( !is_dir($xml_cdr_dir . '/' . $file) ) { //get the leg of the call and the file prefix if (substr($file, 0, 2) == "a_") { $leg = "a"; $file_prefix = substr($file, 2, 1); } else { $leg = "b"; $file_prefix = substr($file, 0, 1); } //set the limit if (isset($_SERVER["argv"][1]) && is_numeric((int)$_SERVER["argv"][1])) { $limit = $_SERVER["argv"][1]; } else { $limit = 1; } //filter for specific files based on the file prefix if (isset($_SERVER["argv"][2])) { if (strpos($_SERVER["argv"][2], $file_prefix) !== FALSE) { $import = true; } else { $import = false; } } else { $import = true; } //import the call detail files are less than 3 mb - 3 million bytes if ($import && filesize($xml_cdr_dir.'/'.$file) <= 3000000) { //get the xml cdr string $call_details = file_get_contents($xml_cdr_dir.'/'.$file); //set the file $this->file = $file; //decode the xml string if (substr($call_details, 0, 1) == '%') { $call_details = urldecode($call_details); } //parse the xml and insert the data into the db $this->xml_array($x, $leg, $call_details); //increment the value $x++; } //move the files that are too large to the failed directory if ($import && filesize($xml_cdr_dir.'/'.$file) >= 3000000) { if (!empty($xml_cdr_dir)) { if (!file_exists($xml_cdr_dir.'/failed')) { if (!mkdir($xml_cdr_dir.'/failed', 0660, true)) { die('Failed to create '.$xml_cdr_dir.'/failed'); } } rename($xml_cdr_dir.'/'.$file, $xml_cdr_dir.'/failed/'.$file); } } //if limit exceeded exit the loop if ($limit == $x) { //echo "limit: $limit count: $x if\n"; break; } } } } //close the directory handle closedir($dir_handle); } //$this->read_files(); /** * read the call detail records from the http post */ public function post() { if (isset($_POST["cdr"])) { //debug method //$this->log($_POST["cdr"]); //authentication for xml cdr http post if (!defined('STDIN')) { if ($this->setting->get('cdr', 'http_enabled')) { //get the contents of xml_cdr.conf.xml $conf_xml_string = file_get_contents($this->setting->get('switch', 'conf').'/autoload_configs/xml_cdr.conf.xml'); //parse the xml to get the call detail record info try { //disable xml entities libxml_disable_entity_loader(true); //load the string into an xml object $conf_xml = simplexml_load_string($conf_xml_string, 'SimpleXMLElement', LIBXML_NOCDATA); } catch(Exception $e) { echo $e->getMessage(); } if (isset($conf_xml->settings->param)) { foreach ($conf_xml->settings->param as $row) { if ($row->attributes()->name == "cred") { $auth_array = explode(":", $row->attributes()->value); //echo "username: ".$auth_array[0]."
\n"; //echo "password: ".$auth_array[1]."
\n"; } if ($row->attributes()->name == "url") { //check name is equal to url } } } } } //if http enabled is set to false then deny access if (!defined('STDIN')) { if ($this->setting->get('cdr', 'http_enabled') == "false") { openlog('FusionPBX', LOG_NDELAY, LOG_AUTH); syslog(LOG_WARNING, '['.$_SERVER['REMOTE_ADDR'].'] XML CDR import default setting http_enabled is not enabled. Line: '.__line__); closelog(); echo "access denied\n"; return; } } //check for the correct username and password if (!defined('STDIN')) { if ($this->setting->get('cdr', 'http_enabled', true)) { if ($auth_array[0] == $_SERVER["PHP_AUTH_USER"] && $auth_array[1] == $_SERVER["PHP_AUTH_PW"]) { //echo "access granted\n"; $this->username = $auth_array[0]; $this->password = $auth_array[1]; } else { openlog('FusionPBX', LOG_NDELAY, LOG_AUTH); syslog(LOG_WARNING, '['.$_SERVER['REMOTE_ADDR'].'] XML CDR import username or password failed. Line: '.__line__); closelog(); echo "access denied\n"; return; } } } //loop through all attribues //foreach($xml->settings->param[1]->attributes() as $a => $b) { // echo $a,'="',$b,"\"\n"; //} //get the http post variable $xml_string = trim($_POST["cdr"]); //get the leg of the call if (substr($_REQUEST['uuid'], 0, 2) == "a_") { $leg = "a"; } else { $leg = "b"; } //log the xml cdr $this->log("HTTP POST\n"); //parse the xml and insert the data into the database $this->xml_array(0, $leg, $xml_string); } } //$this->post(); /** * user summary returns an array */ public function user_summary() { //set the time zone if (!empty($this->setting->get('domain', 'time_zone'))) { $time_zone = $this->setting->get('domain', 'time_zone'); } else { $time_zone = date_default_timezone_get(); } //set the time zone for php date_default_timezone_set($time_zone); //build the date range if ((!empty($this->start_stamp_begin) && strlen($this->start_stamp_begin) > 0) || !empty($this->start_stamp_end)) { unset($this->quick_select); if (strlen($this->start_stamp_begin) > 0 && !empty($this->start_stamp_end)) { $sql_date_range = " and start_stamp between :start_stamp_begin::timestamptz and :start_stamp_end::timestamptz \n"; $parameters['start_stamp_begin'] = $this->start_stamp_begin.':00.000 '.$time_zone; $parameters['start_stamp_end'] = $this->start_stamp_end.':59.999 '.$time_zone; } else { if (!empty($this->start_stamp_begin)) { $sql_date_range = "and start_stamp >= :start_stamp_begin::timestamptz \n"; $parameters['start_stamp_begin'] = $this->start_stamp_begin.':00.000 '.$time_zone; } if (!empty($this->start_stamp_end)) { $sql_date_range .= "and start_stamp <= :start_stamp_end::timestamptz \n"; $parameters['start_stamp_end'] = $this->start_stamp_end.':59.999 '.$time_zone; } } } else { switch ($this->quick_select) { case 1: $sql_date_range = "and start_stamp >= '".date('Y-m-d H:i:s.000', strtotime("-1 week"))." ".$time_zone."'::timestamptz \n"; break; //last 7 days case 2: $sql_date_range = "and start_stamp >= '".date('Y-m-d H:i:s.000', strtotime("-1 hour"))." ".$time_zone."'::timestamptz \n"; break; //last hour case 3: $sql_date_range = "and start_stamp >= '".date('Y-m-d')." "."00:00:00.000 ".$time_zone."'::timestamptz \n"; break; //today case 4: $sql_date_range = "and start_stamp between '".date('Y-m-d',strtotime("-1 day"))." "."00:00:00.000 ".$time_zone."'::timestamptz and '".date('Y-m-d',strtotime("-1 day"))." "."23:59:59.999 ".$time_zone."'::timestamptz \n"; break; //yesterday case 5: $sql_date_range = "and start_stamp >= '".date('Y-m-d',strtotime("this week"))." "."00:00:00.000 ".$time_zone."' \n"; break; //this week case 6: $sql_date_range = "and start_stamp >= '".date('Y-m-')."01 "."00:00:00.000 ".$time_zone."'::timestamptz \n"; break; //this month case 7: $sql_date_range = "and start_stamp >= '".date('Y-')."01-01 "."00:00:00.000 ".$time_zone."'::timestamptz \n"; break; //this year } } //calculate the summary data $sql = "select \n"; $sql .= "e.domain_uuid, \n"; $sql .= "d.domain_name, \n"; $sql .= "e.extension, \n"; $sql .= "e.number_alias, \n"; //answered $sql .= "count(*) \n"; $sql .= "filter ( \n"; $sql .= " where c.extension_uuid = e.extension_uuid \n"; $sql .= " and status = 'answered' \n"; if (!$this->include_internal) { $sql .= "and (direction = 'inbound' or direction = 'outbound') \n"; } $sql .= ") \n"; $sql .= "as answered, \n"; //missed $sql .= "count(*) \n"; $sql .= "filter ( \n"; $sql .= " where c.extension_uuid = e.extension_uuid \n"; $sql .= " and status = 'missed' \n"; $sql .= " and (cc_side is null or cc_side != 'agent') \n"; if (!$this->include_internal) { $sql .= "and (direction = 'inbound' or direction = 'outbound') \n"; } $sql .= ") \n"; $sql .= "as missed, \n"; //voicemail $sql .= "count(*) \n"; $sql .= "filter ( \n"; $sql .= " where c.extension_uuid = e.extension_uuid \n"; $sql .= " and status = 'voicemail' \n"; if (!$this->include_internal) { $sql .= "and (direction = 'inbound' or direction = 'outbound') \n"; } $sql .= ") \n"; $sql .= "as voicemail, \n"; //no answer $sql .= "count(*) \n"; $sql .= "filter ( \n"; $sql .= " where c.extension_uuid = e.extension_uuid \n"; $sql .= " and status = 'no_answer'\n"; $sql .= " and (cc_side IS NOT NULL or cc_side ='agent')"; if ($this->include_internal) { $sql .= " and (direction = 'inbound' or direction = 'local') \n"; } else { $sql .= "and direction = 'inbound' \n"; } $sql .= ") \n"; $sql .= "as no_answer, \n"; //busy $sql .= "count(*) \n"; $sql .= "filter ( \n"; $sql .= " where c.extension_uuid = e.extension_uuid \n"; $sql .= " and status = 'busy'\n"; if ($this->include_internal) { $sql .= " and (direction = 'inbound' or direction = 'local') \n"; } else { $sql .= " and direction = 'inbound' \n"; } $sql .= ") \n"; $sql .= "as busy, \n"; //aloc $sql .= "sum(c.billsec) \n"; $sql .= "filter ( \n"; $sql .= " where c.extension_uuid = e.extension_uuid \n"; if (!$this->include_internal) { $sql .= " and (direction = 'inbound' or direction = 'outbound') \n"; } $sql .= " ) / \n"; $sql .= "count(*) \n"; $sql .= "filter ( \n"; $sql .= " where c.extension_uuid = e.extension_uuid \n"; if (!$this->include_internal) { $sql .= " and (direction = 'inbound' or direction = 'outbound') \n"; } $sql .= ") \n"; $sql .= "as aloc, \n"; //inbound calls $sql .= "count(*) \n"; $sql .= "filter ( \n"; $sql .= " where c.extension_uuid = e.extension_uuid \n"; if (!permission_exists('xml_cdr_enterprise_leg')) { $sql .= " and originating_leg_uuid is null \n"; } elseif (!permission_exists('xml_cdr_lose_race')) { $sql .= " and hangup_cause <> 'LOSE_RACE' \n"; } $sql .= " and (cc_side is null or cc_side != 'agent') \n"; if ($this->include_internal) { $sql .= " and (direction = 'inbound' or direction = 'local') \n"; } else { $sql .= " and direction = 'inbound' \n"; } $sql .= ") \n"; $sql .= "as inbound_calls, \n"; //inbound duration $sql .= "sum(c.billsec) \n"; $sql .= "filter ( \n"; $sql .= " where c.extension_uuid = e.extension_uuid \n"; if ($this->include_internal) { $sql .= " and (direction = 'inbound' or direction = 'local')) \n"; } else { $sql .= " and direction = 'inbound') \n"; } $sql .= "as inbound_duration, \n"; //outbound duration $sql .= "count(*) \n"; $sql .= "filter ( \n"; $sql .= " where c.extension_uuid = e.extension_uuid \n"; $sql .= " and c.direction = 'outbound' \n"; $sql .= ") \n"; $sql .= "as outbound_calls, \n"; $sql .= "sum(c.billsec) \n"; $sql .= "filter ( \n"; $sql .= " where c.extension_uuid = e.extension_uuid \n"; $sql .= " and c.direction = 'outbound' \n"; $sql .= ") \n"; $sql .= "as outbound_duration, \n"; $sql .= "e.description \n"; $sql .= "from v_extensions as e, v_domains as d, \n"; $sql .= "( select \n"; $sql .= " domain_uuid, \n"; $sql .= " extension_uuid, \n"; $sql .= " caller_id_number, \n"; $sql .= " destination_number, \n"; $sql .= " missed_call, \n"; $sql .= " answer_stamp, \n"; $sql .= " bridge_uuid, \n"; $sql .= " direction, \n"; $sql .= " start_stamp, \n"; $sql .= " hangup_cause, \n"; $sql .= " originating_leg_uuid, \n"; $sql .= " billsec, \n"; $sql .= " cc_side, \n"; $sql .= " sip_hangup_disposition, \n"; $sql .= " voicemail_message, \n"; $sql .= " status \n"; $sql .= " from v_xml_cdr \n"; if (!(!empty($_GET['show']) && $_GET['show'] === 'all' && permission_exists('xml_cdr_extension_summary_all'))) { $sql .= " where domain_uuid = :domain_uuid \n"; } else { $sql .= " where true \n"; } $sql .= "and leg = 'a' "; $sql .= "and extension_uuid is not null "; $sql .= $sql_date_range; $sql .= ") as c \n"; $sql .= "where \n"; $sql .= "d.domain_uuid = e.domain_uuid \n"; if (!(!empty($_GET['show']) && $_GET['show'] === 'all' && permission_exists('xml_cdr_extension_summary_all'))) { $sql .= "and e.domain_uuid = :domain_uuid \n"; } $sql .= "group by e.extension, e.domain_uuid, d.domain_uuid, e.number_alias, e.description \n"; $sql .= "order by extension asc \n"; if (!(!empty($_GET['show']) && $_GET['show'] === 'all' && permission_exists('xml_cdr_extension_summary_all'))) { $parameters['domain_uuid'] = $this->domain_uuid; } $summary = $this->database->select($sql, $parameters, 'all'); unset($parameters); //return the array return $summary; } /** * download the recordings */ public function download() { if (permission_exists('xml_cdr_view')) { //get call recording from database if (is_uuid($this->recording_uuid)) { $sql = "select record_name, record_path from v_xml_cdr "; $sql .= "where xml_cdr_uuid = :xml_cdr_uuid "; $parameters['xml_cdr_uuid'] = $this->recording_uuid; $row = $this->database->select($sql, $parameters, 'row'); if (!empty($row) && is_array($row)) { $record_name = $row['record_name']; $record_path = $row['record_path']; } unset ($sql, $parameters, $row); } //build full path $record_file = $record_path.'/'.$record_name; //download the file if ($record_file != '/' && file_exists($record_file)) { ob_clean(); $fd = fopen($record_file, "rb"); if ($this->binary) { header("Content-Type: application/force-download"); header("Content-Type: application/octet-stream"); header("Content-Type: application/download"); header("Content-Description: File Transfer"); } else { $file_ext = pathinfo($record_name, PATHINFO_EXTENSION); switch ($file_ext) { case "wav" : header("Content-Type: audio/x-wav"); break; case "mp3" : header("Content-Type: audio/mpeg"); break; case "ogg" : header("Content-Type: audio/ogg"); break; } } $record_name = preg_replace('#[^a-zA-Z0-9_\-\.]#', '', $record_name); header('Content-Disposition: attachment; filename="'.$record_name.'"'); header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1 header("Expires: Sat, 26 Jul 1997 05:00:00 GMT"); // Date in the past if ($this->binary) { header("Content-Length: ".filesize($record_file)); } ob_clean(); //content-range if (isset($_SERVER['HTTP_RANGE']) && !$this->binary) { $this->range_download($record_file); } fpassthru($fd); } } } //end download method /* * range download method (helps safari play audio sources) */ private function range_download($file) { $fp = @fopen($file, 'rb'); $size = filesize($file); // File size $length = $size; // Content length $start = 0; // Start byte $end = $size - 1; // End byte // Now that we've gotten so far without errors we send the accept range header /* At the moment we only support single ranges. * Multiple ranges requires some more work to ensure it works correctly * and comply with the spesifications: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2 * * Multirange support annouces itself with: * header('Accept-Ranges: bytes'); * * Multirange content must be sent with multipart/byteranges mediatype, * (mediatype = mimetype) * as well as a boundry header to indicate the various chunks of data. */ header("Accept-Ranges: 0-".$length); // header('Accept-Ranges: bytes'); // multipart/byteranges // http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2 if (isset($_SERVER['HTTP_RANGE'])) { $c_start = $start; $c_end = $end; // Extract the range string list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2); // Make sure the client hasn't sent us a multibyte range if (strpos($range, ',') !== false) { // (?) Shoud this be issued here, or should the first // range be used? Or should the header be ignored and // we output the whole content? header('HTTP/1.1 416 Requested Range Not Satisfiable'); header("Content-Range: bytes $start-$end/$size"); // (?) Echo some info to the client? exit; } // If the range starts with an '-' we start from the beginning // If not, we forward the file pointer // And make sure to get the end byte if specified if ($range[0] == '-') { // The n-number of the last bytes is requested $c_start = $size - substr($range, 1); } else { $range = explode('-', $range); $c_start = $range[0]; $c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size; } /* Check the range and make sure it's treated according to the specs. * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html */ // End bytes can not be larger than $end. $c_end = ($c_end > $end) ? $end : $c_end; // Validate the requested range and return an error if it's not correct. if ($c_start > $c_end || $c_start > $size - 1 || $c_end >= $size) { header('HTTP/1.1 416 Requested Range Not Satisfiable'); header("Content-Range: bytes $start-$end/$size"); // (?) Echo some info to the client? exit; } $start = $c_start; $end = $c_end; $length = $end - $start + 1; // Calculate new content length fseek($fp, $start); header('HTTP/1.1 206 Partial Content'); } // Notify the client the byte range we'll be outputting header("Content-Range: bytes $start-$end/$size"); header("Content-Length: $length"); // Start buffered download $buffer = 1024 * 8; while(!feof($fp) && ($p = ftell($fp)) <= $end) { if ($p + $buffer > $end) { // In case we're only outputtin a chunk, make sure we don't // read past the length $buffer = $end - $p + 1; } set_time_limit(0); // Reset time limit for big files echo fread($fp, $buffer); flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit. } fclose($fp); } /** * delete records */ public function delete($records) { if (!permission_exists($this->permission_prefix.'delete')) { return false; } //add multi-lingual support $language = new text; $text = $language->get(); //validate the token $token = new token; if (!$token->validate($_SERVER['PHP_SELF'])) { message::add($text['message-invalid_token'],'negative'); header('Location: '.$this->list_page); exit; } //delete multiple records if (!is_array($records) || @sizeof($records) == 0) { return; } $records_deleted = 0; //loop through records foreach($records as $x => $record) { if (empty($record['checked']) || $record['checked'] != 'true' || !is_uuid($record['uuid'])) { continue; } //get the call recordings $sql = "select xml_cdr_uuid, record_name, record_path from v_xml_cdr "; $sql .= "where xml_cdr_uuid = :xml_cdr_uuid "; $sql .= "and record_name is not null"; $parameters['xml_cdr_uuid'] = $record['uuid']; $row = $this->database->select($sql, $parameters, 'row'); unset($sql, $parameters); //delete the call recording (file) $call_recording_path = realpath($row['record_path']); $call_recording_name = $row['record_name']; if (file_exists($call_recording_path.'/'.$call_recording_name)) { @unlink($call_recording_path.'/'.$call_recording_name); } //build the delete array $array[$this->table][$x][$this->uuid_prefix.'uuid'] = $record['uuid']; //increment counter $records_deleted++; } if (!is_array($array) || @sizeof($array) == 0) { return; } //grant temporary permissions $p = permissions::new(); $p->add('call_recording_delete', 'temp'); //execute delete $this->database->app_name = $this->app_name; $this->database->app_uuid = $this->app_uuid; $this->database->delete($array); unset($array); //revoke temporary permissions $p->delete('call_recording_delete', 'temp'); //set message message::add($text['message-delete'].": ".$records_deleted); unset($records); } //method /** * Removes old entries for in the database xml_cdr, xml_cdr_flow, xml_cdr_json, xml_cdr_logs table * see {@link https://github.com/fusionpbx/fusionpbx-app-maintenance/} FusionPBX Maintenance App * @param settings $settings Settings object * @return void */ public static function database_maintenance(settings $settings): void { //set table name for query $table = 'xml_cdr'; //get a database connection $database = $settings->database(); //get a list of domains $domains = maintenance::get_domains($database); foreach ($domains as $domain_uuid => $domain_name) { //get domain settings $domain_settings = new settings(['database' => $database, 'domain_uuid' => $domain_uuid]); //get the retention days for xml cdr table using 'cdr' and 'database_retention_days' $xml_cdr_retention_days = $domain_settings->get('cdr', 'database_retention_days', ''); //get the retention days for xml cdr flow table if ($database->table_exists('xml_cdr_flow')) { $xml_cdr_flow_retention_days = $domain_settings->get('cdr', 'flow_database_retention_days', $xml_cdr_retention_days); } else { $xml_cdr_flow_retention_days = null; } //get the retention days for xml cdr json table if ($database->table_exists('xml_cdr_json')) { $xml_cdr_json_retention_days = $domain_settings->get('cdr', 'json_database_retention_days', $xml_cdr_retention_days); } else { $xml_cdr_json_retention_days = null; } //get the retention days for xml cdr logs table if ($database->table_exists('xml_cdr_logs')) { $xml_cdr_logs_retention_days = $domain_settings->get('cdr', 'logs_database_retention_days', $xml_cdr_retention_days); } else { $xml_cdr_logs_retention_days = null; } //ensure we have retention days if (!empty($xml_cdr_retention_days) && is_numeric((int)$xml_cdr_retention_days)) { //clear out old xml_cdr records $sql = "delete from v_{$table} WHERE insert_date < NOW() - INTERVAL '{$xml_cdr_retention_days} days'" . " and domain_uuid = '{$domain_uuid}'"; $database->execute($sql); $code = $database->message['code'] ?? 0; //record result if ($code == 200) { maintenance_service::log_write(self::class, "Successfully removed entries older than $xml_cdr_retention_days", $domain_uuid); } else { $message = $database->message['message'] ?? "An unknown error has occurred"; maintenance_service::log_write(self::class, "XML CDR " . "Unable to remove old database records. Error message: $message ($code)", $domain_uuid, maintenance_service::LOG_ERROR); } //clear out old xml_cdr_flow records if (!empty($xml_cdr_flow_retention_days)) { $sql = "delete from v_xml_cdr_flow WHERE insert_date < NOW() - INTERVAL '{$xml_cdr_flow_retention_days} days'" . " and domain_uuid = '{$domain_uuid}"; $database->execute($sql); $code = $database->message['code'] ?? 0; //record result if ($database->message['code'] == 200) { maintenance_service::log_write(self::class, "Successfully removed XML CDR FLOW entries from $domain_name", $domain_uuid); } else { $message = $database->message['message'] ?? "An unknown error has occurred"; maintenance_service::log_write(self::class, "XML CDR FLOW " . "Unable to remove old database records. Error message: $message ($code)", $domain_uuid, maintenance_service::LOG_ERROR); } } //clear out old xml_cdr_json records if (!empty($xml_cdr_json_retention_days)) { $sql = "DELETE FROM v_xml_cdr_json WHERE insert_date < NOW() - INTERVAL '{$xml_cdr_json_retention_days} days'" . " and domain_uuid = '{$domain_uuid}"; $database->execute($sql); $code = $database->message['code'] ?? 0; //record result if ($database->message['code'] == 200) { maintenance_service::log_write(self::class, "Successfully removed XML CDR JSON entries from $domain_name", $domain_uuid); } else { $message = $database->message['message'] ?? "An unknown error has occurred"; maintenance_service::log_write(self::class, "XML CDR JSON " . "Unable to remove old database records. Error message: $message ($code)", $domain_uuid, maintenance_service::LOG_ERROR); } } //clear out old xml_cdr_logs records if (!empty($xml_cdr_logs_retention_days)) { $sql = "DELETE FROM v_xml_cdr_logs WHERE insert_date < NOW() - INTERVAL '{$xml_cdr_logs_retention_days} days'" . " and domain_uuid = '{$domain_uuid}'"; $database->execute($sql); $code = $database->message['code'] ?? 0; //record result if ($database->message['code'] === 200) { maintenance_service::log_write(self::class, "Successfully removed XML CDR LOG entries from $domain_name", $domain_uuid); } else { $message = $database->message['message'] ?? "An unknown error has occurred"; maintenance_service::log_write(self::class, "XML CDR LOG " . "Unable to remove old database records. Error message: $message ($code)", $domain_uuid, maintenance_service::LOG_ERROR); } } } } //ensure logs are saved maintenance_service::log_flush(); } /** * Return CDR for the default settings category name instead of using the class name xml_cdr * @return string Returns 'CDR' for the name */ public static function database_maintenance_category(): string { return "cdr"; } } //class } ?>