Files
itflow/functions/ticket_mention.php

246 lines
12 KiB
PHP

<?php
/*
* Mentions in a ticket's internal notes - @ and an agent's name, picked from
* the agents who can open the ticket (js/app.js itflowMentionAutocompleter).
* Each agent a note mentions is told in ITFlow and by email, the note goes
* with them on the ticket ("Mentions you"), and the tickets list's Mentioned
* me finds the open tickets they were mentioned on.
*
* In the note, a mention is a span the editor can't type into:
* <span class="itflow-mention itflow-mention-u5" data-mention="5" contenteditable="false">@Jordan Lee</span>
* Saving puts each one right (ticketMentionsFromHtml()): the agent's name as
* it is now, or the span gone - leaving its text - for anyone who can't open
* the ticket, and for every mention in a public reply, which tells nobody.
* The page's purifier keeps the classes, so a mention of you stands out.
*
* Who was mentioned is kept in ticket_reply_mentions, with the note: an edit
* that adds someone tells them, one that takes someone out takes them off,
* and an archived note's mentions no longer count.
*/
/** Has the 3.5.3 update run? Until it has, a mention is plain text and nobody is told. */
function ticketMentionsReady() {
global $mysqli;
static $ready = null;
if ($ready === null) {
if (defined('CURRENT_DATABASE_VERSION')) {
$version = CURRENT_DATABASE_VERSION;
} else {
$result = mysqli_query($mysqli, "SELECT config_current_database_version FROM settings WHERE company_id = 1");
$version = $result && ($row = mysqli_fetch_row($result)) ? (string) $row[0] : '0';
}
$ready = version_compare($version, '3.5.3', '>=');
}
return $ready;
}
/**
* The agents who can open a ticket of this client - active, with Support (or
* an admin role), and let at the client by their client access - by name:
* user_id => ['id', 'name', 'role', 'avatar' (its URL, or ''), 'initials'].
* $client_id 0 is a ticket with no client, which any of them can open.
*/
function ticketMentionAgents($client_id) {
global $mysqli;
static $cache = [];
$client_id = intval($client_id);
if (isset($cache[$client_id])) {
return $cache[$client_id];
}
$support_id = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT module_id FROM modules WHERE module_name = 'module_support' LIMIT 1"))[0] ?? 0);
$sql = mysqli_query($mysqli, "SELECT user_id, user_name, user_avatar, role_name, role_is_admin,
COALESCE((SELECT user_role_permission_level FROM user_role_permissions WHERE user_role_permissions.user_role_id = users.user_role_id AND module_id = $support_id LIMIT 1), 0) AS support_level
FROM users LEFT JOIN user_roles ON role_id = user_role_id
WHERE user_type = 1 AND user_status = 1 AND user_archived_at IS NULL
ORDER BY user_name ASC");
$agents = [];
while ($row = mysqli_fetch_assoc($sql)) {
if (intval($row['role_is_admin']) !== 1 && intval($row['support_level']) < 1) {
continue;
}
$user_id = intval($row['user_id']);
$agents[$user_id] = [
'id' => $user_id,
'name' => (string) $row['user_name'],
'role' => (string) ($row['role_name'] ?? ''),
'admin' => intval($row['role_is_admin']) === 1,
'avatar' => trim((string) $row['user_avatar']) !== '' ? "/uploads/users/$user_id/" . rawurlencode($row['user_avatar']) : '',
'initials' => personInitials($row['user_name']),
];
}
// Client access: a deny keeps them out; an allow list keeps them to it. Admins see every client.
if ($client_id && $agents) {
$allow = $deny = [];
$sql = mysqli_query($mysqli, "SELECT user_id, client_id, permission_type FROM user_client_permissions WHERE user_id IN (" . implode(',', array_keys($agents)) . ")");
while ($row = mysqli_fetch_assoc($sql)) {
if ($row['permission_type'] === 'deny') {
$deny[intval($row['user_id'])][] = intval($row['client_id']);
} else {
$allow[intval($row['user_id'])][] = intval($row['client_id']);
}
}
foreach ($agents as $user_id => $agent) {
if ($agent['admin']) {
continue;
}
if (in_array($client_id, $deny[$user_id] ?? [], true) || (!empty($allow[$user_id]) && !in_array($client_id, $allow[$user_id], true))) {
unset($agents[$user_id]);
}
}
}
return $cache[$client_id] = $agents;
}
/** Who the @ picker offers on a ticket of this client - everyone who can open it but you: [['id', 'name', 'role', 'avatar', 'initials'], ...]. None before the update. */
function ticketMentionOffer($client_id) {
global $session_user_id;
$offer = [];
if (!ticketMentionsReady()) {
return $offer;
}
foreach (ticketMentionAgents($client_id) as $user_id => $agent) {
if ($user_id === intval($session_user_id)) {
continue;
}
$offer[] = ['id' => $user_id, 'name' => $agent['name'], 'role' => $agent['role'], 'avatar' => $agent['avatar'], 'initials' => $agent['initials']];
}
return $offer;
}
/** The offer as a textarea's data-mention-agents attribute (js/app.js), or '' when there is nobody to offer. */
function ticketMentionAgentsAttr($client_id) {
$offer = ticketMentionOffer($client_id);
return $offer ? ' data-mention-agents="' . escapeHtml(json_encode($offer)) . '"' : '';
}
/** A mention as it is saved - the agent's name as it is now. */
function ticketMentionSpan($user_id, $name) {
$user_id = intval($user_id);
return '<span class="itflow-mention itflow-mention-u' . $user_id . '" data-mention="' . $user_id . '" contenteditable="false">@' . escapeHtml($name) . '</span>';
}
/**
* A note's mentions put right before it is saved - [$html, user ids]. In an
* internal note ($internal) each mention of an agent who can open a ticket of
* $client_id is made again with their name as it is now, and counted; any
* other - someone who can't, or every one in a public reply - is left as the
* text it showed, @ and all, and tells nobody. $html is the note as posted
* (not SQL escaped).
*/
function ticketMentionsFromHtml($html, $client_id, $internal = true) {
$html = (string) $html;
if (!ticketMentionsReady() || stripos($html, 'data-mention') === false) {
return [$html, []];
}
$agents = $internal ? ticketMentionAgents($client_id) : [];
$ids = [];
$html = preg_replace_callback('#<span\b(?=[^>]*\bdata-mention\s*=\s*["\']?(\d+))[^>]*>(.*?)</span>#is', function ($m) use ($agents, &$ids) {
$user_id = intval($m[1]);
if (!isset($agents[$user_id])) {
return strip_tags($m[2]);
}
$ids[$user_id] = $user_id;
return ticketMentionSpan($user_id, $agents[$user_id]['name']);
}, $html);
return [$html, array_values($ids)];
}
/** Who a note mentions now - [user ids]; ones no longer there taken off, new ones added. The ones added - to tell. */
function ticketMentionsSync($reply_id, array $user_ids) {
global $mysqli, $session_user_id;
$reply_id = intval($reply_id);
if (!$reply_id || !ticketMentionsReady()) {
return [];
}
$user_ids = array_values(array_unique(array_filter(array_map('intval', $user_ids))));
$had = [];
$sql = mysqli_query($mysqli, "SELECT mention_user_id FROM ticket_reply_mentions WHERE mention_reply_id = $reply_id");
while ($row = mysqli_fetch_row($sql)) {
$had[] = intval($row[0]);
}
$gone = array_diff($had, $user_ids);
if ($gone) {
mysqli_query($mysqli, "DELETE FROM ticket_reply_mentions WHERE mention_reply_id = $reply_id AND mention_user_id IN (" . implode(',', $gone) . ")");
}
$new = array_values(array_diff($user_ids, $had));
foreach ($new as $user_id) {
mysqli_query($mysqli, "INSERT IGNORE INTO ticket_reply_mentions SET mention_reply_id = $reply_id, mention_user_id = $user_id, mention_by = " . intval($session_user_id));
}
return $new;
}
/** The ids of the agents a ticket's notes mention, by note - reply_id => [user ids]. Archived notes left out. */
function ticketMentionsForTicket($ticket_id) {
global $mysqli;
$mentions = [];
if (!ticketMentionsReady()) {
return $mentions;
}
$sql = mysqli_query($mysqli, "SELECT mention_reply_id, mention_user_id FROM ticket_reply_mentions
JOIN ticket_replies ON ticket_reply_id = mention_reply_id
WHERE ticket_reply_ticket_id = " . intval($ticket_id) . " AND ticket_reply_archived_at IS NULL");
while ($row = mysqli_fetch_row($sql)) {
$mentions[intval($row[0])][] = intval($row[1]);
}
return $mentions;
}
/** SQL for "a note on this ticket mentions $user_id" - tickets.ticket_id in the query. */
function ticketMentionedSql($user_id) {
return "EXISTS (SELECT 1 FROM ticket_reply_mentions JOIN ticket_replies ON ticket_reply_id = mention_reply_id
WHERE ticket_reply_ticket_id = tickets.ticket_id AND mention_user_id = " . intval($user_id) . " AND ticket_reply_archived_at IS NULL)";
}
/** Where a note is - the ticket, scrolled to it. For the agent side; $absolute for an email. */
function ticketMentionUrl(array $row, $reply_id, $absolute = false) {
return ($absolute ? ticketAgentUrl($row) : '/agent/' . ticketUrl($row['ticket_id'], $row['ticket_client_id'] ?? 0)) . '#reply-' . intval($reply_id);
}
/**
* Tell the agents a note mentions - in ITFlow, and by email from the ticket
* mailbox when mail is set up - but not whoever wrote it. $row is the ticket
* (ticketRow() or the handler's), $html the note as saved. Who was told -
* user_id => name, raw.
*/
function ticketMentionsTell(array $row, $reply_id, $html, array $user_ids) {
global $mysqli, $session_user_id, $session_name;
$told = [];
$agents = ticketMentionAgents($row['ticket_client_id'] ?? 0);
$by = stripslashes((string) $session_name);
$reference = ticketReference($row);
$client_id = intval($row['ticket_client_id'] ?? 0);
$action = escapeSql(ticketMentionUrl($row, $reply_id));
foreach (array_unique(array_map('intval', $user_ids)) as $user_id) {
if ($user_id === intval($session_user_id) || !isset($agents[$user_id])) {
continue;
}
$text = escapeSql("$by mentioned you in an internal note on ticket $reference - " . $row['ticket_subject']);
mysqli_query($mysqli, "INSERT INTO notifications SET notification_type = 'Ticket', notification = '$text', notification_action = '$action',
notification_client_id = $client_id, notification_entity_id = " . intval($row['ticket_id']) . ", notification_user_id = $user_id");
ticketMailMentioned($row['ticket_id'], $reply_id, $html, $user_id, $by);
$told[$user_id] = $agents[$user_id]['name'];
}
return $told;
}
/** Email an agent a note mentions them - the note, and the ticket. $by raw. How many went (0 or 1). */
function ticketMailMentioned($ticket_id, $reply_id, $html, $user_id, $by) {
global $config_app_name;
$row = ticketRow($ticket_id);
$agent = $row ? ticketMailAgent($user_id) : null;
if (!$agent) {
return 0;
}
$agent['footnote'] = 'You get this because ' . ($by !== '' ? $by : 'someone') . ' mentioned you in an internal note on ' . ticketReference($row) . '.';
return ticketMailAgents([$agent], ($config_app_name ?: 'ITFlow') . " - Mentioned on ticket [" . ticketReference($row) . "] " . $row['ticket_subject'],
ticketAgentEmailContent($row, 'mentioned', ['by' => $by, 'message' => (string) $html, 'reply_id' => intval($reply_id)]));
}
/** "Jordan Lee", "Jordan Lee and Sam Rivera", "Jordan Lee, Sam Rivera and Priya Shah". */
function ticketMentionNames(array $names) {
$names = array_values($names);
$last = array_pop($names);
return $names ? implode(', ', $names) . ' and ' . $last : (string) $last;
}