mibew/src/mibew/libs/chat.php

772 lines
23 KiB
PHP

<?php
/*
* This file is a part of Mibew Messenger.
*
* Copyright 2005-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use Mibew\Asset\Generator\UrlGeneratorInterface as AssetUrlGeneratorInterface;
use Mibew\EventDispatcher\EventDispatcher;
use Mibew\EventDispatcher\Events;
use Mibew\Settings;
use Mibew\Thread;
use Mibew\Style\ChatStyle;
use Mibew\Style\PageStyle;
use Mibew\Routing\Generator\SecureUrlGeneratorInterface as UrlGeneratorInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Convert messages to formatted text
*
* @param array $msg message object which most be formatted
*
* @return string formatted message
*/
function message_to_text($msg)
{
$message_time = date("H:i:s ", $msg['created']);
if ($msg['kind'] == Thread::KIND_USER || $msg['kind'] == Thread::KIND_AGENT) {
if ($msg['name']) {
return $message_time . $msg['name'] . ": " . $msg['message'] . "\n";
} else {
return $message_time . $msg['message'] . "\n";
}
} elseif ($msg['kind'] == Thread::KIND_INFO) {
return $message_time . $msg['message'] . "\n";
} else {
return $message_time . "[" . $msg['message'] . "]\n";
}
}
/**
* Sanitize message body and make it a safe HTML string.
*
* @param array $msg Message object
* @return array Message object with sanitized body.
*/
function sanitize_message($msg)
{
$message_body = $msg['message'];
// Messages entered by user or operator cannot contain any markup
if ($msg['kind'] == Thread::KIND_USER || $msg['kind'] == Thread::KIND_AGENT) {
$message_body = safe_htmlspecialchars($message_body);
}
$msg['message'] = sanitize_string($message_body, 'low', 'moderate');
return $msg;
}
/**
* Format username
*
* @param string $user_name client username
* @param string $addr ip address of client
* @param string $id id of client
*
* @return string formatted username with "usernamepattern" pattern
*/
function get_user_name($user_name, $addr, $id)
{
return str_replace(
"{addr}",
$addr,
str_replace(
"{id}",
$id,
str_replace("{name}", $user_name, Settings::get('usernamepattern'))
)
);
}
/**
* Prepare logo data
*
* @param array $group Group info
* @return array Array of logo data
*/
function setup_logo($group = null)
{
$data = array();
$top_level_group = (!$group) ? array() : get_top_level_group($group);
$group_name = empty($top_level_group['vctitle'])
? Settings::get('title')
: $top_level_group['vctitle'];
$logo = empty($top_level_group['vclogo'])
? Settings::get('logo')
: $top_level_group['vclogo'];
$mibew_host = empty($top_level_group['vchosturl'])
? Settings::get('hosturl')
: $top_level_group['vchosturl'];
$data['company'] = array(
'name' => $group_name,
'chatLogoURL' => $logo,
);
$data['mibewHost'] = $mibew_host;
return $data;
}
/**
* Prepare values common for chat, prechat survey form and leave message form.
* @return array
*/
function prepare_chat_app_data()
{
$data = array();
// Set refresh frequency
$data['frequency'] = Settings::get('updatefrequency_chat');
return $data;
}
/**
* Prepare data to display leave message form
*
* @param string $name User name
* @param string $email User email
* @param int $group_id Id of selected group
* @param string $info User info
* @param string $referrer URL of referrer page
* @return array Array of leave message form data
*/
function setup_leavemessage($name, $email, $group_id, $info, $referrer)
{
$data = prepare_chat_app_data();
// Create some empty arrays
$data['leaveMessage'] = array();
$group = group_by_id($group_id);
$group_name = '';
if ($group) {
$group_name = get_group_name($group);
}
$data['leaveMessage']['leaveMessageForm'] = array(
'name' => $name,
'email' => $email,
'groupId' => $group_id,
'groupName' => $group_name,
'info' => $info,
'referrer' => $referrer,
'showCaptcha' => (bool) (Settings::get("enablecaptcha") == "1" && can_show_captcha()),
);
$data['page.title'] = (empty($group_name) ? '' : $group_name . ': ')
. getlocal('Leave your message');
$data['leaveMessage']['page'] = array(
'title' => $data['page.title']
);
// Set privacy policy link (if needed)
if (Settings::get('enableprivacypolicy')
&& strcmp('', Settings::get('privacypolicy'))) {
$data['leaveMessage']['leaveMessageForm']['privacyPolicyUrl'] = Settings::get('privacypolicy');
}
if (Settings::get('enablegroups') == '1') {
$data['leaveMessage']['leaveMessageForm']['groups']
= prepare_groups_select($group_id);
}
$data['startFrom'] = 'leaveMessage';
return $data;
}
/**
* Prepare data to display pre-chat survey
*
* @param string $name User name
* @param string $email User email
* @param int $group_id Id of selected group
* @param string $info User info
* @param string $referrer URL of referrer page
* @return array Array of survey data
*/
function setup_survey($name, $email, $group_id, $info, $referrer)
{
$data = prepare_chat_app_data();
// Create some empty arrays
$data['survey'] = array();
$data['survey']['surveyForm'] = array(
'name' => $name,
'groupId' => $group_id,
'email' => $email,
'info' => $info,
'referrer' => $referrer,
'showEmail' => (bool) (Settings::get("surveyaskmail") == "1"),
'showMessage' => (bool) (Settings::get("surveyaskmessage") == "1"),
'canChangeName' => (bool) (Settings::get('usercanchangename') == "1"),
);
$data['page.title'] = getlocal('Live support');
$data['survey']['page'] = array(
'title' => $data['page.title']
);
// Set privacy policy link (if needed)
if (Settings::get('enableprivacypolicy')
&& strcmp('', Settings::get('privacypolicy'))) {
$data['survey']['surveyForm']['privacyPolicyUrl'] = Settings::get('privacypolicy');
}
if (Settings::get('enablegroups') == '1' && Settings::get('surveyaskgroup') == '1') {
$data['survey']['surveyForm']['groups']
= prepare_groups_select($group_id);
}
$data['startFrom'] = 'survey';
return $data;
}
/**
* Prepare groups list to build group select box.
*
* If $group_id specified groups list will consist of group with id equals to
* $group_id and its children.
*
* @param int $group_id Id of selected group
* @return array|boolean Array of groups info arrays or boolean false if there
* are no suitable groups.
* Group info array contain following keys:
* - 'id': int, group id;
* - 'name': string, group name;
* - 'description': string, group description;
* - 'online': boolean, indicates if group online;
* - 'selected': boolean, indicates if group selected by default.
*/
function prepare_groups_select($group_id)
{
$show_groups = ($group_id == 0) ? true : group_has_children($group_id);
if (!$show_groups) {
return false;
}
$all_groups = get_groups(false);
if (empty($all_groups)) {
return false;
}
$groups_list = array();
$selected_group_id = $group_id;
foreach ($all_groups as $group) {
$group_is_empty = (bool) ($group['inumofagents'] == 0);
$group_related_with_specified = empty($group_id)
|| $group['parent'] == $group_id
|| $group['groupid'] == $group_id;
if ($group_is_empty || !$group_related_with_specified) {
continue;
}
if (group_is_online($group) && !$selected_group_id) {
$selected_group_id = $group['groupid'];
}
$groups_list[] = array(
'id' => $group['groupid'],
'name' => get_group_name($group),
'description' => get_group_description($group),
'online' => group_is_online($group),
'selected' => (bool) ($group['groupid'] == $selected_group_id),
);
}
// One group must be selected by default
if (!empty($groups_list)) {
// Check if there is selected group
$selected_group_present = false;
foreach ($groups_list as $group) {
if ($group['selected']) {
$selected_group_present = true;
break;
}
}
// If there is no selected group select the first one
if (!$selected_group_present) {
$groups_list[0]['selected'] = true;
}
}
return $groups_list;
}
/**
* Prepare some data for chat for both user and operator
*
* @param Thread $thread thread object
* @return array Array of chat view data
*/
function setup_chatview(Thread $thread)
{
$data = prepare_chat_app_data();
// Get group info
if (!empty($thread->groupId)) {
$group = group_by_id($thread->groupId);
$group = get_top_level_group($group);
} else {
$group = array();
}
// Create some empty arrays
$data['chat'] = array(
'messageForm' => array(),
'links' => array(),
'windowsParams' => array(),
);
// Set thread params
$data['chat']['thread'] = array(
'id' => $thread->id,
'token' => $thread->lastToken,
'agentId' => $thread->agentId,
'userId' => $thread->userId,
);
$data['page.title'] = empty($group['vcchattitle'])
? Settings::get('chattitle')
: $group['vcchattitle'];
$data['chat']['page'] = array(
'title' => $data['page.title']
);
// Setup logo
$data = array_merge_recursive($data, setup_logo($group));
// Set enter key shortcut
if (Settings::get('sendmessagekey') == 'enter') {
$data['chat']['messageForm']['ignoreCtrl'] = true;
} else {
$data['chat']['messageForm']['ignoreCtrl'] = false;
}
// Load dialogs style options
$chat_style = new ChatStyle(ChatStyle::getCurrentStyle());
$style_config = $chat_style->getConfigurations();
$data['chat']['windowsParams']['mail']
= $style_config['mail']['window'];
// Load core style options
$page_style = new PageStyle(PageStyle::getCurrentStyle());
$style_config = $page_style->getConfigurations();
$data['chat']['windowsParams']['history']
= $style_config['history']['window'];
$data['chat']['windowsParams']['trackedPath']
= $style_config['tracked']['visitor_window'];
$data['startFrom'] = 'chat';
return $data;
}
/**
* Prepare some data for chat for user
*
* @param UrlGeneratorInterface $url_generator A URL generator object.
* @param AssetUrlGeneratorInterface $asset_url_generator An asset URL generator
* object.
* @param Request $request The current request.
* @param Thread $thread thread object that will be used
* @return array Array of chat view data
*/
function setup_chatview_for_user(
UrlGeneratorInterface $url_generator,
AssetUrlGeneratorInterface $asset_url_generator,
Request $request,
Thread $thread
) {
$data = setup_chatview($thread);
// Set user info
$data['chat']['user'] = array(
'name' => htmlspecialchars($thread->userName),
'canChangeName' => (bool) (Settings::get('usercanchangename') == "1"),
'defaultName' => (bool) (getlocal("Guest") != $thread->userName),
'canPost' => true,
'isAgent' => false,
'canSendEmail' => (bool) (Settings::get('usercansendemail') == "1"),
);
// Set link to send mail page
$data['chat']['links']['mail'] = $url_generator->generate(
'chat_user_mail',
array(
'thread_id' => $thread->id,
'token' => $thread->lastToken,
)
);
// Set SSL link
if (Settings::get('enablessl') == "1" && $request->isSecure()) {
$data['chat']['links']['ssl'] = $url_generator->generateSecure(
'chat_user',
array(
'thread_id' => $thread->id,
'token' => $thread->lastToken,
),
UrlGeneratorInterface::ABSOLUTE_URL
);
}
// Set chat link
$data['chat']['links']['chat'] = $url_generator->generate(
'chat_user',
array(
'thread_id' => $thread->id,
'token' => $thread->lastToken,
),
UrlGeneratorInterface::ABSOLUTE_URL
);
// Set default operator's avatar
$operator = operator_by_id($thread->agentId);
$data['chat']['avatar'] = '';
if (!empty($operator['vcavatar'])) {
$data['chat']['avatar'] = $asset_url_generator->generate($operator['vcavatar']);
}
return $data;
}
/**
* Prepare some data for chat for operator
*
* @param UrlGeneratorInterface $url_generator A URL generator object.
* @param Request $request The current request.
* @param Thread $thread thread object.
* @param array $operator Operator's data.
* @return array Array of chat view data
*/
function setup_chatview_for_operator(
UrlGeneratorInterface $url_generator,
Request $request,
Thread $thread,
$operator
) {
$data = setup_chatview($thread);
// Set operator info
$data['chat']['user'] = array(
'name' => htmlspecialchars(
get_user_name(
$thread->userName,
$thread->remote,
$thread->userId
)
),
'canPost' => (bool) ($thread->agentId == $operator['operatorid']),
'isAgent' => true,
);
// Set SSL link
if (Settings::get('enablessl') == "1" && !$request->isSecure()) {
$data['chat']['links']['ssl'] = $url_generator->generateSecure(
'chat_operator',
array(
'thread_id' => $thread->id,
'token' => $thread->lastToken,
),
UrlGeneratorInterface::ABSOLUTE_URL
);
}
// Set chat link
$data['chat']['links']['chat'] = $url_generator->generate(
'chat_operator',
array(
'thread_id' => $thread->id,
'token' => $thread->lastToken,
),
UrlGeneratorInterface::ABSOLUTE_URL
);
// Set history window params
$data['chat']['links']['history'] = $url_generator->generate(
'history_user',
array('user_id' => $thread->userId)
);
// Set tracking params
if (Settings::get('enabletracking')) {
$visitor = track_get_visitor_by_thread_id($thread->id);
if ($visitor) {
$data['chat']['links']['tracked'] = $url_generator->generate(
'history_user_track',
array('visitor' => $visitor['visitorid'])
);
}
}
// Check if agent can post messages
if ($thread->agentId == $operator['operatorid']) {
// Get predefined answers
$canned_messages = load_canned_messages($thread->locale, 0);
if ($thread->groupId) {
$canned_messages = array_merge(
load_canned_messages($thread->locale, $thread->groupId),
$canned_messages
);
};
$predefined_answers = array();
foreach ($canned_messages as $answer) {
$predefined_answers[] = array(
'short' => htmlspecialchars(
$answer['vctitle'] ? $answer['vctitle'] : cut_string($answer['vcvalue'], 97, '...')
),
'full' => $answer['vcvalue'],
);
}
$data['chat']['messageForm']['predefinedAnswers'] = $predefined_answers;
}
// Set link to user redirection page
$data['chat']['links']['redirect'] = $url_generator->generate(
'chat_operator_redirection_links',
array(
'thread_id' => $thread->id,
'token' => $thread->lastToken,
)
);
return $data;
}
/**
* @return array Return visitor info from active request. contains
* (user_id string, user_name string)
*/
function visitor_from_request()
{
$tmp_request = Request::createFromGlobals();
$default_name = getlocal("Guest");
$user_name = $default_name;
if (isset($_COOKIE[USERNAME_COOKIE_NAME])) {
$data = base64_decode(strtr($_COOKIE[USERNAME_COOKIE_NAME], '-_,', '+/='));
if (strlen($data) > 0) {
$user_name = $data;
}
}
if ($user_name == $default_name) {
$temp = $tmp_request->query->get('name');
$user_name = (isset($temp) && ($temp !== '')) ? $temp : $user_name;
}
if (isset($_COOKIE[USERID_COOKIE_NAME])) {
$user_id = $_COOKIE[USERID_COOKIE_NAME];
} else {
// Check whether user id already exists in absence of the appropriate cookie:
// some browsers could have weird behaviour
$temp = $tmp_request->query->get('user_id');
$user_id = (isset($temp)) ? $temp : uniqid('', true);
$cookie_properties = array( 'expires' => time() + 60 * 60 * 24 * 365 );
if (version_compare(phpversion(), '7.3.0', '<')) {
setcookie(USERID_COOKIE_NAME, $user_id, $cookie_properties['expires']);
} else {
if ($tmp_request->isSecure()) {
$cookie_properties['samesite'] = 'None';
$cookie_properties['secure'] = true;
}
setcookie(USERID_COOKIE_NAME, $user_id, $cookie_properties);
}
}
unset($tmp_request);
return array('id' => $user_id, 'name' => $user_name);
}
/**
* @return string Return remote host from active request
*/
function get_remote_host()
{
$ext_addr = isset($_SERVER['REMOTE_ADDR'])
? $_SERVER['REMOTE_ADDR']
: '';
$has_proxy = isset($_SERVER['HTTP_X_FORWARDED_FOR'])
&& $_SERVER['HTTP_X_FORWARDED_FOR'] != $ext_addr;
if ($has_proxy) {
$ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'], 2);
$ext_addr = (count($ips) > 1)
? $ips[0] . ' (' . $_SERVER['HTTP_X_FORWARDED_FOR'] . ')'
: $ips[0];
}
return isset($_SERVER['REMOTE_HOST']) ? $_SERVER['REMOTE_HOST'] : $ext_addr;
}
/**
* Start chat thread for user
*
* @param int $group_id Id of group related to thread
* @param array $requested_operator Array of requested operator info
* @param string $visitor_id Id of the visitor
* @param string $visitor_name Name of the visitor
* @param string $referrer Page user came from
* @param string $info User info
*
* @return Thread thread object
*/
function chat_start_for_user(
$group_id,
$requested_operator,
$visitor_id,
$visitor_name,
$referrer,
$info
) {
// Get user info
$remote_host = get_remote_host();
$user_browser = isset($_SERVER['HTTP_USER_AGENT'])
? $_SERVER['HTTP_USER_AGENT']
: '';
// Check connection limit
if (Thread::connectionLimitReached($remote_host)) {
die("number of connections from your IP is exceeded, try again later");
}
// Check if visitor was invited to chat
$is_invited = false;
if (Settings::get('enabletracking')) {
$invitation_state = invitation_state($_SESSION[SESSION_PREFIX . 'visitorid']);
if ($invitation_state['invited']) {
$is_invited = true;
}
}
// Get info about requested operator
$requested_operator_online = false;
if ($requested_operator) {
$requested_operator_online = is_operator_online(
$requested_operator['operatorid']
);
}
// Get thread object
if ($is_invited) {
// Get thread from invitation
$thread = invitation_accept($_SESSION[SESSION_PREFIX . 'visitorid']);
if (!$thread) {
die("Cannot start thread");
}
} else {
// Create thread
$thread = new Thread();
$thread->state = Thread::STATE_LOADING;
$thread->agentId = 0;
if ($requested_operator && $requested_operator_online) {
$thread->nextAgent = $requested_operator['operatorid'];
}
}
// Update thread fields
$thread->groupId = $group_id;
$thread->userName = $visitor_name;
$thread->remote = $remote_host;
$thread->referer = $referrer;
$thread->locale = get_current_locale();
$thread->userId = $visitor_id;
$thread->userAgent = $user_browser;
$thread->save();
$_SESSION[SESSION_PREFIX . 'threadid'] = $thread->id;
// Store own thread ids to restrict access for other people
if (!isset($_SESSION[SESSION_PREFIX . 'own_threads'])) {
$_SESSION[SESSION_PREFIX . 'own_threads'] = array();
}
$_SESSION[SESSION_PREFIX . 'own_threads'][] = $thread->id;
// Bind thread to the visitor
if (Settings::get('enabletracking')) {
track_visitor_bind_thread($visitor_id, $thread);
}
// Send several messages
if ($is_invited) {
$operator = operator_by_id($thread->agentId);
$operator_name = get_operator_name($operator);
$thread->postMessage(
Thread::KIND_FOR_AGENT,
getlocal(
'Visitor accepted invitation from operator {0}',
array(safe_htmlspecialchars($operator_name)),
get_current_locale(),
true
)
);
} else {
if ($referrer) {
$thread->postMessage(
Thread::KIND_FOR_AGENT,
getlocal('Visitor came from page {0}', array(safe_htmlspecialchars($referrer)), get_current_locale(), true)
);
}
if ($requested_operator && !$requested_operator_online) {
$thread->postMessage(
Thread::KIND_INFO,
getlocal(
'Thank you for contacting us. We are sorry, but requested operator <strong>{0}</strong> is offline. Another operator will be with you shortly.',
array(safe_htmlspecialchars(get_operator_name($requested_operator))),
get_current_locale(),
true
)
);
} else {
$thread->postMessage(
Thread::KIND_INFO,
getlocal('Thank you for contacting us. An operator will be with you shortly.', null, get_current_locale(), true)
);
}
}
// TODO: May be move sending this message somewhere else?
if ($info) {
$thread->postMessage(
Thread::KIND_FOR_AGENT,
getlocal('Info: {0}', array(safe_htmlspecialchars($info)), get_current_locale(), true)
);
}
// Let plugins know that user is ready to chat.
$dispatcher = EventDispatcher::getInstance();
$event_args = array('thread' => $thread);
$dispatcher->triggerEvent(Events::THREAD_USER_IS_READY, $event_args);
return $thread;
}