mirror of
https://github.com/itflow-org/itflow
synced 2026-11-11 07:01:16 +00:00
836 lines
44 KiB
PHP
836 lines
44 KiB
PHP
<?php
|
|
|
|
/*
|
|
* Client portal invites and sign-ups.
|
|
*
|
|
* An invite asks someone by email to join a client's portal - an agent sends
|
|
* it from the client's Contacts (the Invites tab), a primary contact from
|
|
* Contacts in the portal. Nobody is a contact until they accept: the link
|
|
* opens client/join.php, where they put in their name, title, phone and a
|
|
* password (or choose their Microsoft 365 account), and they are made a
|
|
* contact with portal access - or, when the client already has a contact
|
|
* with that address, that contact is given access.
|
|
*
|
|
* A sign-up is the same, asked for by the person. Create an account on the
|
|
* sign-in page (client/signup.php) takes an address at one of a client's
|
|
* Domains, for a client that lets people sign up (clients.client_portal_signup,
|
|
* with settings.config_client_portal_signup on), and emails a link to finish.
|
|
* The address is all that is checked - they join as a plain contact, who sees
|
|
* the tickets they raise, and the agents and the client's primary contact
|
|
* are told. What the page says never depends on the address, so it can't be
|
|
* used to find out who has an account.
|
|
*
|
|
* Both are portal_invites rows (invite_kind invite or signup). The link
|
|
* carries a token of which only the SHA-256 is kept, and it runs out - an
|
|
* invite after PORTAL_INVITE_LINK_DAYS, a sign-up after
|
|
* PORTAL_SIGNUP_LINK_HOURS. Using it spends it; sending it again makes a new
|
|
* token, so the old link stops working; cancelling stops it.
|
|
*/
|
|
|
|
const PORTAL_SIGNUP_LINK_HOURS = 24; // a link to finish signing up
|
|
const PORTAL_INVITE_MAX = 50; // addresses in one go
|
|
const PORTAL_SIGNUP_IP_LIMIT = 20; // sign-ups asked for from one address in 10 minutes - an office signing up together
|
|
|
|
/** Has the 3.4.6 update run? Until it has, nothing here is shown or done. */
|
|
function portalInvitesReady() {
|
|
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.4.6', '>=');
|
|
}
|
|
return $ready;
|
|
}
|
|
|
|
/*
|
|
* ---------- who an address is ----------
|
|
*/
|
|
|
|
/**
|
|
* The people in what was typed - addresses one to a line or separated by
|
|
* commas or semicolons, each with an optional name: jamie@acme.com, Jamie Lee
|
|
* <jamie@acme.com>, "Lee, Jamie" <jamie@acme.com>. ['people' => [[email,
|
|
* name]], 'bad' => [what wasn't an address]], each address once.
|
|
*/
|
|
function portalInviteParse($text) {
|
|
$text = str_replace(["\r\n", "\r"], "\n", (string) $text);
|
|
$entries = [];
|
|
$current = '';
|
|
$quoted = false;
|
|
$angled = false;
|
|
$length = strlen($text);
|
|
for ($i = 0; $i < $length; $i++) {
|
|
$char = $text[$i];
|
|
if ($char === '"' && !$angled) {
|
|
$quoted = !$quoted;
|
|
} elseif ($char === '<' && !$quoted) {
|
|
$angled = true;
|
|
} elseif ($char === '>' && !$quoted) {
|
|
$angled = false;
|
|
} elseif (($char === "\n" || $char === ',' || $char === ';') && !$quoted && !$angled) {
|
|
$entries[] = $current;
|
|
$current = '';
|
|
continue;
|
|
}
|
|
$current .= $char;
|
|
}
|
|
$entries[] = $current;
|
|
|
|
$people = [];
|
|
$bad = [];
|
|
foreach ($entries as $entry) {
|
|
$entry = trim($entry);
|
|
if ($entry === '') {
|
|
continue;
|
|
}
|
|
$name = '';
|
|
if (preg_match('/^(.*)<\s*([^<>\s]+)\s*>$/s', $entry, $m)) {
|
|
$name = trim(trim($m[1]), "\"' \t");
|
|
$email = $m[2];
|
|
} elseif (preg_match('/^(.*\s)?(\S+@\S+)$/s', $entry, $m)) {
|
|
$name = trim(trim((string) $m[1]), "\"' \t");
|
|
$email = $m[2];
|
|
} else {
|
|
$email = $entry;
|
|
}
|
|
$email = trim($email, " \t\"'");
|
|
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || mb_strlen($email) > 200) {
|
|
$bad[] = mb_substr($entry, 0, 100);
|
|
continue;
|
|
}
|
|
$key = strtolower($email);
|
|
if (!isset($people[$key])) {
|
|
$people[$key] = ['email' => $email, 'name' => mb_substr(preg_replace('/\s+/', ' ', $name), 0, 200)];
|
|
} elseif ($people[$key]['name'] === '' && $name !== '') {
|
|
$people[$key]['name'] = mb_substr($name, 0, 200);
|
|
}
|
|
}
|
|
return ['people' => array_values($people), 'bad' => $bad];
|
|
}
|
|
|
|
/**
|
|
* Where an address stands at a client: state new (nobody here has it),
|
|
* contact (a contact without portal access - or with it turned off; contact_id,
|
|
* name, user_id, access, primary), member (a contact who signs in already), elsewhere
|
|
* (another account signs in with it - an agent, or a contact at another
|
|
* client; type 1 or 2, and on for a portal account that can sign in).
|
|
*/
|
|
function portalInviteWho($email, $client_id) {
|
|
global $mysqli;
|
|
$email_sql = escapeSql(trim((string) $email));
|
|
$client_id = intval($client_id);
|
|
$contact = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT contact_id, contact_name, contact_primary, user_id, user_auth_method, user_status, user_archived_at
|
|
FROM contacts LEFT JOIN users ON user_id = contact_user_id AND user_type = 2
|
|
WHERE contact_client_id = $client_id AND contact_archived_at IS NULL AND contact_email = '$email_sql'
|
|
ORDER BY (user_id IS NOT NULL) DESC, contact_id ASC LIMIT 1"));
|
|
$access = $contact ? portalContactAccess($contact) : null;
|
|
if ($access && in_array($access['key'], ['local', 'azure'], true)) {
|
|
return ['state' => 'member', 'contact_id' => intval($contact['contact_id']), 'name' => (string) $contact['contact_name'], 'user_id' => intval($contact['user_id']), 'access' => $access['key']];
|
|
}
|
|
$own_user = intval($contact['user_id'] ?? 0);
|
|
$other = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT user_id, user_type, user_auth_method, user_status FROM users
|
|
WHERE user_email = '$email_sql' AND user_archived_at IS NULL AND user_id <> $own_user LIMIT 1"));
|
|
if ($other) {
|
|
$type = intval($other['user_type']);
|
|
return ['state' => 'elsewhere', 'user_id' => intval($other['user_id']), 'type' => $type,
|
|
'on' => $type === 2 && intval($other['user_status']) === 1 && in_array($other['user_auth_method'], ['local', 'azure'], true), 'access' => (string) $other['user_auth_method']];
|
|
}
|
|
if ($contact) {
|
|
return ['state' => 'contact', 'contact_id' => intval($contact['contact_id']), 'name' => (string) $contact['contact_name'], 'user_id' => $own_user, 'access' => $access['key'],
|
|
'primary' => intval($contact['contact_primary']) === 1];
|
|
}
|
|
return ['state' => 'new'];
|
|
}
|
|
|
|
/*
|
|
* ---------- invites ----------
|
|
*/
|
|
|
|
/** An invite or sign-up, with the client and who sent it - or null. */
|
|
function portalInviteRow($invite_id) {
|
|
global $mysqli;
|
|
$invite_id = intval($invite_id);
|
|
if (!$invite_id) {
|
|
return null;
|
|
}
|
|
return mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT portal_invites.*, client_name, client_archived_at, client_portal_signup,
|
|
agent.user_name AS invite_by_agent, agent.user_status AS invite_by_agent_status, agent.user_archived_at AS invite_by_agent_archived_at,
|
|
by_contact.contact_name AS invite_by_contact, by_contact.contact_email AS invite_by_contact_email
|
|
FROM portal_invites
|
|
JOIN clients ON client_id = invite_client_id
|
|
LEFT JOIN users agent ON agent.user_id = invite_created_by AND invite_created_by > 0
|
|
LEFT JOIN contacts by_contact ON by_contact.contact_id = invite_created_by_contact_id AND invite_created_by_contact_id > 0
|
|
WHERE invite_id = $invite_id LIMIT 1")) ?: null;
|
|
}
|
|
|
|
/** Who sent an invite, in words - an agent's name, a primary contact's, or '' for a sign-up. */
|
|
function portalInviteBy(array $row) {
|
|
if (($row['invite_kind'] ?? '') === 'signup') {
|
|
return '';
|
|
}
|
|
if (intval($row['invite_created_by_contact_id'] ?? 0)) {
|
|
return (string) ($row['invite_by_contact'] ?? '');
|
|
}
|
|
return (string) ($row['invite_by_agent'] ?? '');
|
|
}
|
|
|
|
/** Where an invite stands: pending, expired, accepted or cancelled. */
|
|
function portalInviteState(array $row) {
|
|
if (!empty($row['invite_accepted_at'])) {
|
|
return 'accepted';
|
|
}
|
|
if (!empty($row['invite_cancelled_at'])) {
|
|
return 'cancelled';
|
|
}
|
|
if (strtotime((string) $row['invite_expires_at']) <= time()) {
|
|
return 'expired';
|
|
}
|
|
return 'pending';
|
|
}
|
|
|
|
/** The link that opens the join page for an invite and its token. */
|
|
function portalInviteUrl($invite_id, $token) {
|
|
global $config_base_url;
|
|
return "https://$config_base_url/client/join.php?" . http_build_query(['invite' => intval($invite_id), 'token' => (string) $token]);
|
|
}
|
|
|
|
/** A new token for an invite or sign-up - kept as its SHA-256, running out again from now. The token, for the link. */
|
|
function portalInviteNewToken($invite_id) {
|
|
global $mysqli;
|
|
$invite_id = intval($invite_id);
|
|
$kind = (string) (mysqli_fetch_row(mysqli_query($mysqli, "SELECT invite_kind FROM portal_invites WHERE invite_id = $invite_id"))[0] ?? 'invite');
|
|
$token = bin2hex(random_bytes(24));
|
|
$hash = hash('sha256', $token);
|
|
$expires = $kind === 'signup' ? 'INTERVAL ' . PORTAL_SIGNUP_LINK_HOURS . ' HOUR' : 'INTERVAL ' . PORTAL_INVITE_LINK_DAYS . ' DAY';
|
|
mysqli_query($mysqli, "UPDATE portal_invites SET invite_token_hash = '$hash', invite_expires_at = NOW() + $expires WHERE invite_id = $invite_id");
|
|
return $token;
|
|
}
|
|
|
|
/**
|
|
* The invite or sign-up a link is for - its token right - with where it
|
|
* stands (portalInviteState(), or gone for a link that matches nothing or a
|
|
* client archived since): ['row' => row or null, 'state' => ...].
|
|
*/
|
|
function portalInviteByLink($invite_id, $token) {
|
|
$token = is_string($token) ? $token : '';
|
|
if (!preg_match('/^[a-f0-9]{48}$/', $token)) {
|
|
return ['row' => null, 'state' => 'gone'];
|
|
}
|
|
$row = portalInviteRow($invite_id);
|
|
if (!$row || !hash_equals((string) $row['invite_token_hash'], hash('sha256', $token))) {
|
|
return ['row' => null, 'state' => 'gone'];
|
|
}
|
|
if (!empty($row['client_archived_at'])) {
|
|
return ['row' => null, 'state' => 'gone'];
|
|
}
|
|
return ['row' => $row, 'state' => portalInviteState($row)];
|
|
}
|
|
|
|
/**
|
|
* Email an invite or a sign-up its link - $token from portalInviteNewToken().
|
|
* An invite says who sent it; a sign-up that someone - probably them - asked
|
|
* for it. Notes when it went. False when it can't go.
|
|
*/
|
|
function portalInviteMail($invite_id, $token) {
|
|
global $mysqli;
|
|
$row = portalInviteRow($invite_id);
|
|
if (!$row || !portalMailReady() || !filter_var($row['invite_email'], FILTER_VALIDATE_EMAIL)) {
|
|
return false;
|
|
}
|
|
$company_name = (string) (billingCompany()['company_name'] ?? '');
|
|
$client_name = (string) $row['client_name'];
|
|
$url = portalInviteUrl($row['invite_id'], $token);
|
|
$greet = trim((string) $row['invite_name']);
|
|
|
|
if ($row['invite_kind'] === 'signup') {
|
|
$subject = "Finish signing up for the $company_name client portal";
|
|
$email = [
|
|
'kind' => 'Client portal', 'tone' => 'primary', 'preview' => 'One more step to your client portal account',
|
|
'title' => $subject,
|
|
'intro' => ["Someone - probably you - asked to sign up for the $company_name client portal with this address, for $client_name.", 'Finish with the button below - you choose your password there.'],
|
|
'summary' => ['Signing up as' => (string) $row['invite_email']],
|
|
'button' => ['label' => 'Finish signing up', 'url' => $url],
|
|
'after' => ['The link works once, for the next ' . PORTAL_SIGNUP_LINK_HOURS . ' hours. If you did not ask for it, ignore this email - nothing is set up unless the link is used.'],
|
|
];
|
|
$footnote = 'You received this because someone asked to sign up for the client portal with this address.';
|
|
} else {
|
|
$by = portalInviteBy($row);
|
|
$who = $by !== '' ? $by : $company_name;
|
|
$subject = ($by !== '' ? "$by invited you" : 'You are invited') . " to the $company_name client portal";
|
|
$email = [
|
|
'kind' => 'Client portal', 'tone' => 'primary', 'preview' => "Join $client_name on the $company_name client portal",
|
|
'title' => "Join $client_name on the $company_name client portal",
|
|
'intro' => [($by !== '' ? "$by has" : 'We have') . " invited you to the $company_name client portal for $client_name - where you can raise and follow support tickets.", 'Accept with the button below - you put in your name and choose a password there. It takes a minute.'],
|
|
'summary' => ['Invited' => (string) $row['invite_email']],
|
|
'button' => ['label' => 'Accept the invite', 'url' => $url],
|
|
'after' => ['The invite works for ' . PORTAL_INVITE_LINK_DAYS . ' days. If you were not expecting it, you can ignore this email.'],
|
|
];
|
|
$footnote = "You received this because $who invited this address to the $company_name client portal.";
|
|
}
|
|
$sent = (bool) emailQueue([['email' => (string) $row['invite_email'], 'name' => $greet, 'greet' => $greet !== '' ? $greet : null, 'footnote' => $footnote]], $subject, $email);
|
|
if ($sent && !emailCapturing()) {
|
|
mysqli_query($mysqli, "UPDATE portal_invites SET invite_sent_at = NOW(), invite_sent_count = invite_sent_count + 1 WHERE invite_id = " . intval($row['invite_id']));
|
|
}
|
|
return $sent;
|
|
}
|
|
|
|
/**
|
|
* Invite people to a client's portal - $people from portalInviteParse(), each
|
|
* given the roles in $roles (billing, technical). $by: user_id (an agent) or
|
|
* contact_id (a primary contact in the portal). An address already invited is
|
|
* sent its invite again, with a new link; one that signs in already, here or
|
|
* to another account, is left out with the reason. ['invited' => [emails],
|
|
* 'resent' => [emails], 'skipped' => [email => why]].
|
|
*/
|
|
function portalInviteSend($client_id, array $people, array $roles, array $by) {
|
|
global $mysqli;
|
|
$client_id = intval($client_id);
|
|
$billing = !empty($roles['billing']) ? 1 : 0;
|
|
$technical = !empty($roles['technical']) ? 1 : 0;
|
|
$by_user = intval($by['user_id'] ?? 0);
|
|
$by_contact = intval($by['contact_id'] ?? 0);
|
|
$done = ['invited' => [], 'resent' => [], 'skipped' => []];
|
|
|
|
foreach (array_slice($people, 0, PORTAL_INVITE_MAX) as $person) {
|
|
$email = trim((string) $person['email']);
|
|
$name = trim((string) ($person['name'] ?? ''));
|
|
$who = portalInviteWho($email, $client_id);
|
|
if ($who['state'] === 'member') {
|
|
$done['skipped'][$email] = intval($who['contact_id']) === $by_contact ? "that's you" : 'already signs in';
|
|
continue;
|
|
}
|
|
if ($who['state'] === 'elsewhere') {
|
|
$done['skipped'][$email] = $who['type'] === 1 ? 'an agent signs in with it' : 'it signs in to another client portal account';
|
|
continue;
|
|
}
|
|
// In the portal, as with giving access there, another primary contact is ours to set up
|
|
if ($by_contact && $who['state'] === 'contact' && $who['primary']) {
|
|
$done['skipped'][$email] = 'a primary contact - ask us';
|
|
continue;
|
|
}
|
|
if ($name === '' && $who['state'] === 'contact') {
|
|
$name = (string) $who['name'];
|
|
}
|
|
$email_sql = escapeSql($email);
|
|
$name_sql = $name !== '' ? "'" . escapeSql($name) . "'" : 'NULL';
|
|
$open = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT invite_id FROM portal_invites
|
|
WHERE invite_client_id = $client_id AND invite_kind = 'invite' AND invite_email = '$email_sql' AND invite_accepted_at IS NULL AND invite_cancelled_at IS NULL
|
|
ORDER BY invite_id DESC LIMIT 1"))[0] ?? 0);
|
|
if ($open) {
|
|
mysqli_query($mysqli, "UPDATE portal_invites SET invite_name = COALESCE($name_sql, invite_name), invite_billing = $billing, invite_technical = $technical,
|
|
invite_created_by = $by_user, invite_created_by_contact_id = $by_contact WHERE invite_id = $open");
|
|
$invite_id = $open;
|
|
} else {
|
|
mysqli_query($mysqli, "INSERT INTO portal_invites SET invite_kind = 'invite', invite_email = '$email_sql', invite_name = $name_sql, invite_billing = $billing,
|
|
invite_technical = $technical, invite_expires_at = NOW(), invite_created_by = $by_user, invite_created_by_contact_id = $by_contact, invite_client_id = $client_id");
|
|
$invite_id = intval(mysqli_insert_id($mysqli));
|
|
}
|
|
if (!portalInviteMail($invite_id, portalInviteNewToken($invite_id))) {
|
|
if (!$open) {
|
|
mysqli_query($mysqli, "DELETE FROM portal_invites WHERE invite_id = $invite_id");
|
|
}
|
|
$done['skipped'][$email] = 'the email could not be sent';
|
|
continue;
|
|
}
|
|
$done[$open ? 'resent' : 'invited'][] = $email;
|
|
}
|
|
return $done;
|
|
}
|
|
|
|
/** Send an invite that is waiting again, with a new link (the old one stops working). False when it isn't waiting or can't go. */
|
|
function portalInviteResend($invite_id, $client_id) {
|
|
$row = portalInviteRow($invite_id);
|
|
if (!$row || intval($row['invite_client_id']) !== intval($client_id) || $row['invite_kind'] !== 'invite'
|
|
|| !in_array(portalInviteState($row), ['pending', 'expired'], true)) {
|
|
return false;
|
|
}
|
|
return portalInviteMail($row['invite_id'], portalInviteNewToken($row['invite_id']));
|
|
}
|
|
|
|
/** Cancel an invite that is waiting - its link stops working. False when it isn't waiting. */
|
|
function portalInviteCancel($invite_id, $client_id) {
|
|
global $mysqli;
|
|
mysqli_query($mysqli, "UPDATE portal_invites SET invite_cancelled_at = NOW()
|
|
WHERE invite_id = " . intval($invite_id) . " AND invite_client_id = " . intval($client_id) . " AND invite_kind = 'invite'
|
|
AND invite_accepted_at IS NULL AND invite_cancelled_at IS NULL");
|
|
return mysqli_affected_rows($mysqli) === 1;
|
|
}
|
|
|
|
/** A client's invites waiting to be accepted - in date or run out - newest first, with who sent them. */
|
|
function portalInvitesWaiting($client_id) {
|
|
global $mysqli;
|
|
$rows = [];
|
|
$sql = mysqli_query($mysqli, "SELECT invite_id, invite_email, invite_name, invite_billing, invite_technical, invite_expires_at, invite_sent_at, invite_sent_count,
|
|
invite_created_at, invite_created_by, invite_created_by_contact_id, invite_kind, invite_accepted_at, invite_cancelled_at,
|
|
agent.user_name AS invite_by_agent, by_contact.contact_name AS invite_by_contact
|
|
FROM portal_invites
|
|
LEFT JOIN users agent ON agent.user_id = invite_created_by AND invite_created_by > 0
|
|
LEFT JOIN contacts by_contact ON by_contact.contact_id = invite_created_by_contact_id AND invite_created_by_contact_id > 0
|
|
WHERE invite_client_id = " . intval($client_id) . " AND invite_kind = 'invite' AND invite_accepted_at IS NULL AND invite_cancelled_at IS NULL
|
|
ORDER BY invite_created_at DESC, invite_id DESC");
|
|
while ($row = mysqli_fetch_assoc($sql)) {
|
|
$rows[] = $row;
|
|
}
|
|
return $rows;
|
|
}
|
|
|
|
/** How an invite stands, as a badge: Waiting (and how long it has left) or Ran out. */
|
|
function portalInviteStateLabel(array $row) {
|
|
if (portalInviteState($row) === 'expired') {
|
|
return ['label' => 'Ran out', 'title' => 'The link ran out on ' . date('M j, Y', strtotime($row['invite_expires_at'])) . ' - send it again for a new one', 'tone' => 'warning'];
|
|
}
|
|
$left = strtotime((string) $row['invite_expires_at']) - time();
|
|
$days = intval(floor($left / 86400));
|
|
return ['label' => 'Waiting', 'title' => 'The link works for ' . ($days >= 1 ? $days . ' more day' . ($days === 1 ? '' : 's') : 'less than a day'), 'tone' => 'info'];
|
|
}
|
|
|
|
/** How a contact joined the portal, when it was by an invite or signing up - ['kind', 'by', 'at'] or null. */
|
|
function portalContactJoined($contact_id) {
|
|
global $mysqli;
|
|
if (!portalInvitesReady() || !intval($contact_id)) {
|
|
return null;
|
|
}
|
|
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT invite_kind, invite_accepted_at, invite_created_by, invite_created_by_contact_id,
|
|
agent.user_name AS invite_by_agent, by_contact.contact_name AS invite_by_contact
|
|
FROM portal_invites
|
|
LEFT JOIN users agent ON agent.user_id = invite_created_by AND invite_created_by > 0
|
|
LEFT JOIN contacts by_contact ON by_contact.contact_id = invite_created_by_contact_id AND invite_created_by_contact_id > 0
|
|
WHERE invite_contact_id = " . intval($contact_id) . " AND invite_accepted_at IS NOT NULL
|
|
ORDER BY invite_accepted_at DESC LIMIT 1"));
|
|
if (!$row) {
|
|
return null;
|
|
}
|
|
return ['kind' => $row['invite_kind'], 'by' => portalInviteBy($row), 'at' => $row['invite_accepted_at']];
|
|
}
|
|
|
|
/*
|
|
* ---------- accepting ----------
|
|
*/
|
|
|
|
/**
|
|
* Accept an invite or finish a sign-up - $invite from portalInviteByLink()
|
|
* (still pending), $in what they put in: name, title, phone, method (local or
|
|
* azure), password and confirm. Makes the contact (or uses the client's
|
|
* contact with that address) and their portal account, spends the link and
|
|
* any other waiting for the address, and tells whoever should know. The
|
|
* contact's id, or 0 with $error (plain text).
|
|
*/
|
|
function portalInviteAccept(array $invite, array $in, &$error = '') {
|
|
global $mysqli;
|
|
$error = '';
|
|
$invite_id = intval($invite['invite_id']);
|
|
$client_id = intval($invite['invite_client_id']);
|
|
$email = trim((string) $invite['invite_email']);
|
|
$signup = $invite['invite_kind'] === 'signup';
|
|
|
|
$name = trim(preg_replace('/\s+/', ' ', mb_substr((string) ($in['name'] ?? ''), 0, 200)));
|
|
$title = trim(mb_substr((string) ($in['title'] ?? ''), 0, 200));
|
|
$phone = substr(preg_replace('/[^0-9]/', '', (string) ($in['phone'] ?? '')), 0, 200);
|
|
$method = (string) ($in['method'] ?? 'local');
|
|
if (!isset(portalSignInMethods()[$method])) {
|
|
$method = 'local';
|
|
}
|
|
$password = (string) ($in['password'] ?? '');
|
|
|
|
if ($name === '') {
|
|
$error = 'Put in your name.';
|
|
return 0;
|
|
}
|
|
if ($method === 'local') {
|
|
if (($problem = userPasswordProblem($password)) !== '') {
|
|
$error = $problem;
|
|
return 0;
|
|
}
|
|
if ($password !== (string) ($in['confirm'] ?? '')) {
|
|
$error = 'The two passwords are not the same - type it again in both.';
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
$who = portalInviteWho($email, $client_id);
|
|
if ($who['state'] === 'member' || ($who['state'] === 'elsewhere' && $who['type'] === 2 && $who['on'])) {
|
|
$error = 'This address already has a client portal account - sign in with it, or choose Forgot password on the sign-in page.';
|
|
return 0;
|
|
}
|
|
if ($who['state'] === 'elsewhere') {
|
|
$error = 'This address signs in to another account here, so it can\'t be used for the client portal too - ask us about it.';
|
|
return 0;
|
|
}
|
|
if ($signup && ($who['state'] === 'contact' && $who['access'] === 'off')) {
|
|
$error = 'This address can\'t sign up - ask your company\'s primary contact or us to give you access.';
|
|
return 0;
|
|
}
|
|
if ($signup && portalSignupClientFor($email) !== $client_id) {
|
|
$error = 'Signing up has been turned off for ' . $invite['client_name'] . ' - ask your company\'s primary contact or us to invite you.';
|
|
return 0;
|
|
}
|
|
|
|
// Spend the link - one press, or one of two at once, gets through
|
|
mysqli_query($mysqli, "UPDATE portal_invites SET invite_accepted_at = NOW()
|
|
WHERE invite_id = $invite_id AND invite_accepted_at IS NULL AND invite_cancelled_at IS NULL AND invite_expires_at > NOW()
|
|
AND invite_token_hash = '" . escapeSql((string) $invite['invite_token_hash']) . "'");
|
|
if (mysqli_affected_rows($mysqli) !== 1) {
|
|
$error = 'That link has been used already.';
|
|
return 0;
|
|
}
|
|
|
|
$name_sql = escapeSql($name);
|
|
$title_sql = escapeSql($title);
|
|
$phone_sql = escapeSql($phone);
|
|
$email_sql = escapeSql($email);
|
|
$billing = $signup ? 0 : intval($invite['invite_billing']);
|
|
$technical = $signup ? 0 : intval($invite['invite_technical']);
|
|
|
|
// The contact - the client's own with this address, given what was left empty, or a new one
|
|
$new_contact = $who['state'] !== 'contact';
|
|
if ($new_contact) {
|
|
mysqli_query($mysqli, "INSERT INTO contacts SET contact_name = '$name_sql', contact_title = '$title_sql', contact_email = '$email_sql', contact_phone = '$phone_sql',
|
|
contact_billing = $billing, contact_technical = $technical, contact_client_id = $client_id, contact_user_id = 0");
|
|
$contact_id = intval(mysqli_insert_id($mysqli));
|
|
} else {
|
|
$contact_id = intval($who['contact_id']);
|
|
mysqli_query($mysqli, "UPDATE contacts SET contact_name = '$name_sql',
|
|
contact_title = IF(COALESCE(contact_title, '') = '', '$title_sql', contact_title),
|
|
contact_phone = IF(COALESCE(contact_phone, '') = '', '$phone_sql', contact_phone),
|
|
contact_billing = GREATEST(COALESCE(contact_billing, 0), $billing), contact_technical = GREATEST(COALESCE(contact_technical, 0), $technical)
|
|
WHERE contact_id = $contact_id AND contact_client_id = $client_id");
|
|
}
|
|
|
|
// Their portal account - a password, or their Microsoft 365 account
|
|
$hash = escapeSql(password_hash($method === 'local' ? $password : randomString(32), PASSWORD_DEFAULT));
|
|
$user_id = intval($who['user_id'] ?? 0);
|
|
if ($user_id) {
|
|
mysqli_query($mysqli, "UPDATE users SET user_name = '$name_sql', user_email = '$email_sql', user_password = '$hash', user_auth_method = '$method',
|
|
user_status = 1, user_archived_at = NULL, user_password_reset_token = NULL, user_password_reset_token_expire = NULL WHERE user_id = $user_id AND user_type = 2");
|
|
} else {
|
|
mysqli_query($mysqli, "INSERT INTO users SET user_name = '$name_sql', user_email = '$email_sql', user_password = '$hash', user_auth_method = '$method', user_type = 2, user_status = 1");
|
|
$user_id = intval(mysqli_insert_id($mysqli));
|
|
mysqli_query($mysqli, "UPDATE contacts SET contact_user_id = $user_id WHERE contact_id = $contact_id");
|
|
}
|
|
|
|
// The link is theirs now; any other waiting for the address here is done with
|
|
mysqli_query($mysqli, "UPDATE portal_invites SET invite_contact_id = $contact_id WHERE invite_id = $invite_id");
|
|
mysqli_query($mysqli, "UPDATE portal_invites SET invite_cancelled_at = NOW()
|
|
WHERE invite_client_id = $client_id AND invite_email = '$email_sql' AND invite_id <> $invite_id AND invite_accepted_at IS NULL AND invite_cancelled_at IS NULL");
|
|
|
|
// Who should know
|
|
$client_name = (string) $invite['client_name'];
|
|
$by = portalInviteBy($invite);
|
|
$agent_link = "/agent/contact.php?client_id=$client_id&contact_id=$contact_id";
|
|
$how = $signup ? 'signed up in the client portal' : 'accepted ' . ($by !== '' ? "$by's" : 'an') . ' invite to the client portal';
|
|
logAudit("Contact", $new_contact ? "Create" : "Edit", escapeSql("$name ($email) $how for $client_name" . ($new_contact ? '' : ' - an existing contact, now with portal access')), $client_id, $contact_id);
|
|
if ($new_contact) {
|
|
triggerCustomAction('contact_create', $contact_id);
|
|
}
|
|
if ($signup || intval($invite['invite_created_by_contact_id'])) {
|
|
appNotify("Contact", escapeSql("$name $how for $client_name"), $agent_link, $client_id, $contact_id);
|
|
} elseif (intval($invite['invite_created_by']) && intval($invite['invite_by_agent_status'] ?? 0) === 1 && empty($invite['invite_by_agent_archived_at'])) {
|
|
mysqli_query($mysqli, "INSERT INTO notifications SET notification_type = 'Contact', notification = '" . escapeSql(mb_substr("$name accepted your invite to the client portal for $client_name", 0, 1000)) . "',
|
|
notification_action = '" . escapeSql($agent_link) . "', notification_client_id = $client_id, notification_entity_id = $contact_id, notification_user_id = " . intval($invite['invite_created_by']));
|
|
}
|
|
if ($signup) {
|
|
portalMailSignupJoined($contact_id);
|
|
} elseif (intval($invite['invite_created_by_contact_id'])) {
|
|
portalMailInviteAccepted($invite_id);
|
|
}
|
|
return $contact_id;
|
|
}
|
|
|
|
/** Tell a client's primary contacts who can sign in that someone signed up. False when nobody could be told. */
|
|
function portalMailSignupJoined($contact_id) {
|
|
global $mysqli, $config_base_url;
|
|
$contact_id = intval($contact_id);
|
|
$joined = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT contact_name, contact_email, contact_client_id, client_name FROM contacts
|
|
JOIN clients ON client_id = contact_client_id WHERE contact_id = $contact_id LIMIT 1"));
|
|
if (!$joined || !portalMailReady()) {
|
|
return false;
|
|
}
|
|
$client_id = intval($joined['contact_client_id']);
|
|
$company_name = (string) (billingCompany()['company_name'] ?? '');
|
|
$to = [];
|
|
$sql = mysqli_query($mysqli, "SELECT contact_name, contact_email FROM contacts JOIN users ON user_id = contact_user_id
|
|
WHERE contact_client_id = $client_id AND contact_primary = 1 AND contact_archived_at IS NULL AND contact_id <> $contact_id
|
|
AND user_type = 2 AND user_status = 1 AND user_archived_at IS NULL AND user_auth_method IN ('local', 'azure')");
|
|
while ($row = mysqli_fetch_assoc($sql)) {
|
|
$to[] = ['email' => $row['contact_email'], 'name' => $row['contact_name'],
|
|
'footnote' => 'You received this because you are the primary contact for ' . $joined['client_name'] . ' in the client portal.'];
|
|
}
|
|
if (!$to) {
|
|
return false;
|
|
}
|
|
$name = (string) $joined['contact_name'];
|
|
return (bool) emailQueue($to, "$name joined the $company_name client portal", [
|
|
'kind' => 'Client portal', 'tone' => 'primary', 'preview' => "$name signed up with their work email",
|
|
'title' => "$name joined the client portal",
|
|
'intro' => ["$name signed up for the $company_name client portal for " . $joined['client_name'] . ' with their work email address. They see the tickets they raise.',
|
|
'If they should not have access, turn it off from their page under Contacts.'],
|
|
'summary' => ['Who' => $name, 'Email' => (string) $joined['contact_email']],
|
|
'button' => ['label' => 'See them', 'url' => "https://$config_base_url/client/contact.php?id=$contact_id"],
|
|
]);
|
|
}
|
|
|
|
/** Tell the primary contact who sent an invite that it was accepted. False when they can't be told. */
|
|
function portalMailInviteAccepted($invite_id) {
|
|
global $mysqli, $config_base_url;
|
|
$row = portalInviteRow($invite_id);
|
|
if (!$row || !intval($row['invite_contact_id']) || !intval($row['invite_created_by_contact_id']) || !portalMailReady()) {
|
|
return false;
|
|
}
|
|
$by = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT contact_name, contact_email FROM contacts JOIN users ON user_id = contact_user_id
|
|
WHERE contact_id = " . intval($row['invite_created_by_contact_id']) . " AND contact_client_id = " . intval($row['invite_client_id']) . " AND contact_archived_at IS NULL
|
|
AND user_type = 2 AND user_status = 1 AND user_archived_at IS NULL AND user_auth_method IN ('local', 'azure') LIMIT 1"));
|
|
if (!$by) {
|
|
return false;
|
|
}
|
|
$joined = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT contact_name, contact_email FROM contacts WHERE contact_id = " . intval($row['invite_contact_id'])));
|
|
$name = (string) ($joined['contact_name'] ?? $row['invite_email']);
|
|
$company_name = (string) (billingCompany()['company_name'] ?? '');
|
|
return (bool) emailQueue([['email' => $by['contact_email'], 'name' => $by['contact_name'],
|
|
'footnote' => 'You received this because you invited ' . $row['invite_email'] . ' to the client portal.']], "$name accepted your invite", [
|
|
'kind' => 'Client portal', 'tone' => 'success', 'preview' => "$name can sign in to the client portal now",
|
|
'title' => "$name accepted your invite",
|
|
'intro' => ["$name has joined the $company_name client portal for " . $row['client_name'] . ' and can sign in now.'],
|
|
'summary' => ['Who' => $name, 'Email' => (string) ($joined['contact_email'] ?? $row['invite_email'])],
|
|
'button' => ['label' => 'See them', 'url' => "https://$config_base_url/client/contact.php?id=" . intval($row['invite_contact_id'])],
|
|
]);
|
|
}
|
|
|
|
/*
|
|
* ---------- signing up ----------
|
|
*/
|
|
|
|
/** Is signing up on for the portal as a whole (Admin > Settings > Client portal)? */
|
|
function portalSignupOn() {
|
|
global $mysqli;
|
|
static $on = null;
|
|
if ($on === null) {
|
|
$on = false;
|
|
if (portalInvitesReady()) {
|
|
$on = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT config_client_portal_signup FROM settings WHERE company_id = 1"))[0] ?? 0) === 1;
|
|
}
|
|
}
|
|
return $on;
|
|
}
|
|
|
|
/** A client's Domains, lowercased - the addresses people there sign up with. */
|
|
function portalSignupDomains($client_id) {
|
|
global $mysqli;
|
|
$domains = [];
|
|
$sql = mysqli_query($mysqli, "SELECT domain_name FROM domains WHERE domain_client_id = " . intval($client_id) . " AND domain_archived_at IS NULL ORDER BY domain_name ASC");
|
|
while ($row = mysqli_fetch_row($sql)) {
|
|
$domain = strtolower(trim((string) $row[0]));
|
|
if ($domain !== '' && !in_array($domain, $domains, true)) {
|
|
$domains[] = $domain;
|
|
}
|
|
}
|
|
return $domains;
|
|
}
|
|
|
|
/** The clients that let people sign up, not archived - client_id => [name, domains]. Before 3.4.6, none. */
|
|
function portalSignupClients() {
|
|
global $mysqli;
|
|
$clients = [];
|
|
if (!portalInvitesReady()) {
|
|
return $clients;
|
|
}
|
|
$sql = mysqli_query($mysqli, "SELECT client_id, client_name FROM clients WHERE client_portal_signup = 1 AND client_archived_at IS NULL ORDER BY client_name ASC");
|
|
while ($row = mysqli_fetch_assoc($sql)) {
|
|
$clients[intval($row['client_id'])] = ['name' => (string) $row['client_name'], 'domains' => portalSignupDomains($row['client_id'])];
|
|
}
|
|
return $clients;
|
|
}
|
|
|
|
/** Can anyone sign up now - the portal and signing up on, mail working, and a client that lets people with a domain to do it? */
|
|
function portalSignupOpen() {
|
|
global $config_client_portal_enable;
|
|
if (intval($config_client_portal_enable) !== 1 || !portalSignupOn() || !portalMailReady()) {
|
|
return false;
|
|
}
|
|
foreach (portalSignupClients() as $client) {
|
|
if ($client['domains']) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* The client an address signs up to - the one client that lets people sign up
|
|
* and has its domain among its Domains - or 0: none does, or more than one
|
|
* (then nobody can, until it is put right). $why says which.
|
|
*/
|
|
function portalSignupClientFor($email, &$why = '') {
|
|
global $mysqli, $config_client_portal_enable;
|
|
$why = '';
|
|
$email = trim((string) $email);
|
|
if (intval($config_client_portal_enable) !== 1 || !portalSignupOn()) {
|
|
$why = 'signing up is off';
|
|
return 0;
|
|
}
|
|
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
$why = 'not an email address';
|
|
return 0;
|
|
}
|
|
$domain = strtolower(substr(strrchr($email, '@'), 1));
|
|
$domain_sql = escapeSql($domain);
|
|
$clients = [];
|
|
$sql = mysqli_query($mysqli, "SELECT DISTINCT client_id FROM domains JOIN clients ON client_id = domain_client_id
|
|
WHERE LOWER(domain_name) = '$domain_sql' AND domain_archived_at IS NULL AND client_archived_at IS NULL AND client_portal_signup = 1");
|
|
while ($row = mysqli_fetch_row($sql)) {
|
|
$clients[] = intval($row[0]);
|
|
}
|
|
if (count($clients) > 1) {
|
|
$why = "more than one client that lets people sign up has $domain among its domains";
|
|
return 0;
|
|
}
|
|
if (!$clients) {
|
|
$why = "no client that lets people sign up has $domain among its domains";
|
|
return 0;
|
|
}
|
|
return $clients[0];
|
|
}
|
|
|
|
/**
|
|
* Someone asks to sign up with $email (on client/signup.php). Whatever the
|
|
* address, the page says the same; what happens is: a link to finish, for an
|
|
* address at a client that lets people sign up; a note that they have an
|
|
* account already, with a link to set a new password, for one that signs in
|
|
* here; nothing otherwise. At most one email per address every
|
|
* PORTAL_RESET_REQUEST_GAP_MINUTES. Each ask is logged (Client Login). What
|
|
* happened, for the tests and the log: sent, exists, refused or quiet.
|
|
*/
|
|
function portalSignupRequest($email, $ip, $user_agent) {
|
|
global $mysqli;
|
|
$email = trim(mb_substr((string) $email, 0, 200));
|
|
$email_sql = escapeSql($email);
|
|
$ip_sql = escapeSql((string) $ip);
|
|
$user_agent_sql = escapeSql(mb_substr((string) $user_agent, 0, 255));
|
|
$log = function ($action, $description, $user_id = 0, $client_id = 0) use ($mysqli, $ip_sql, $user_agent_sql) {
|
|
mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Client Login', log_action = '" . escapeSql($action) . "', log_description = '" . escapeSql(mb_substr($description, 0, 1000)) . "',
|
|
log_ip = '$ip_sql', log_user_agent = '$user_agent_sql', log_client_id = " . intval($client_id) . ", log_user_id = " . intval($user_id));
|
|
logForwardQueue('audit', mysqli_insert_id($mysqli));
|
|
};
|
|
|
|
// From one address - an office behind one, say - PORTAL_SIGNUP_IP_LIMIT asks in ten minutes; after that, nothing is sent
|
|
$asked = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM logs WHERE log_type = 'Client Login' AND log_action = 'Sign Up Requested'
|
|
AND log_ip = '$ip_sql' AND log_created_at > NOW() - INTERVAL 10 MINUTE"))[0]);
|
|
$client_id = portalSignupClientFor($email, $why);
|
|
$log('Sign Up Requested', 'Asked to sign up to the client portal as ' . $email, 0, $client_id);
|
|
if ($asked >= PORTAL_SIGNUP_IP_LIMIT) {
|
|
$log('Sign Up Refused', "Did not answer a sign-up for $email - too many from this address", 0, $client_id);
|
|
return 'refused';
|
|
}
|
|
|
|
// Already signs in - with a password, a link to set a new one; with Microsoft, how to sign in
|
|
$who = $client_id ? portalInviteWho($email, $client_id) : null;
|
|
$account = null;
|
|
if ($who && $who['state'] === 'member') {
|
|
$account = ['user_id' => $who['user_id'], 'method' => $who['access']];
|
|
} elseif (!$client_id && filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
// Signing up isn't open to the address, but it may sign in already - say so, as Forgot password would
|
|
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT user_id, user_auth_method, contact_client_id FROM users
|
|
JOIN contacts ON contact_user_id = user_id JOIN clients ON client_id = contact_client_id
|
|
WHERE user_email = '$email_sql' AND user_type = 2 AND user_status = 1 AND user_archived_at IS NULL AND user_auth_method IN ('local', 'azure')
|
|
AND contact_archived_at IS NULL AND client_archived_at IS NULL LIMIT 1"));
|
|
if ($row && portalSignupOn()) {
|
|
$account = ['user_id' => intval($row['user_id']), 'method' => (string) $row['user_auth_method']];
|
|
$client_id = intval($row['contact_client_id']);
|
|
}
|
|
} elseif ($who && $who['state'] === 'elsewhere' && $who['type'] === 2 && $who['on']) {
|
|
$account = ['user_id' => $who['user_id'], 'method' => $who['access']];
|
|
}
|
|
if ($account) {
|
|
$recent = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM logs WHERE log_type = 'Client Login' AND log_action IN ('Sign Up Sent', 'Reset Sent')
|
|
AND log_user_id = " . intval($account['user_id']) . " AND log_created_at > NOW() - INTERVAL " . PORTAL_RESET_REQUEST_GAP_MINUTES . " MINUTE"))[0]);
|
|
if ($recent) {
|
|
return 'quiet';
|
|
}
|
|
portalMailSignupExists($account['user_id']);
|
|
$log('Sign Up Sent', "Told $email they have a client portal account already", $account['user_id'], $client_id);
|
|
return 'exists';
|
|
}
|
|
|
|
if (!$client_id) {
|
|
$log('Sign Up Refused', "Did not sign up $email - $why");
|
|
return 'refused';
|
|
}
|
|
if ($who['state'] === 'elsewhere') {
|
|
$log('Sign Up Refused', "Did not sign up $email - " . ($who['type'] === 1 ? 'an agent signs in with it' : 'its portal account is turned off'), 0, $client_id);
|
|
return 'refused';
|
|
}
|
|
if ($who['state'] === 'contact' && $who['access'] === 'off') {
|
|
$log('Sign Up Refused', "Did not sign up $email - their portal access was turned off", 0, $client_id);
|
|
return 'refused';
|
|
}
|
|
|
|
// One link at a time for an address, and not another within a few minutes
|
|
$open = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT invite_id, invite_sent_at > NOW() - INTERVAL " . PORTAL_RESET_REQUEST_GAP_MINUTES . " MINUTE AS just_sent FROM portal_invites
|
|
WHERE invite_client_id = $client_id AND invite_kind = 'signup' AND invite_email = '$email_sql' AND invite_accepted_at IS NULL AND invite_cancelled_at IS NULL
|
|
ORDER BY invite_id DESC LIMIT 1"));
|
|
if ($open && intval($open['just_sent'])) {
|
|
return 'quiet';
|
|
}
|
|
if ($open) {
|
|
$invite_id = intval($open['invite_id']);
|
|
} else {
|
|
mysqli_query($mysqli, "INSERT INTO portal_invites SET invite_kind = 'signup', invite_email = '$email_sql', invite_expires_at = NOW(), invite_client_id = $client_id");
|
|
$invite_id = intval(mysqli_insert_id($mysqli));
|
|
}
|
|
if (!portalInviteMail($invite_id, portalInviteNewToken($invite_id))) {
|
|
$log('Sign Up Refused', "Could not email $email a link to finish signing up", 0, $client_id);
|
|
return 'refused';
|
|
}
|
|
$log('Sign Up Sent', "Emailed $email a link to finish signing up", 0, $client_id);
|
|
return 'sent';
|
|
}
|
|
|
|
/** Tell someone who tried to sign up that they have an account - with a link to set a new password, or for Microsoft how to sign in. */
|
|
function portalMailSignupExists($user_id) {
|
|
global $mysqli, $config_base_url;
|
|
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT user_email, user_auth_method, contact_name FROM users JOIN contacts ON contact_user_id = user_id
|
|
WHERE user_id = " . intval($user_id) . " AND user_type = 2 LIMIT 1"));
|
|
if (!$row || !portalMailReady()) {
|
|
return false;
|
|
}
|
|
$company_name = (string) (billingCompany()['company_name'] ?? '');
|
|
$local = $row['user_auth_method'] === 'local';
|
|
return (bool) emailQueue([['email' => $row['user_email'], 'name' => $row['contact_name'],
|
|
'footnote' => 'You received this because someone tried to sign up for the client portal with this address.']], "You already have a $company_name client portal account", [
|
|
'kind' => 'Client portal', 'tone' => 'primary', 'preview' => 'Sign in with the account you have',
|
|
'title' => "You already have a $company_name client portal account",
|
|
'intro' => ["Someone - probably you - tried to sign up for the $company_name client portal with this address, but it has an account already.",
|
|
$local ? 'Forgot your password? Choose a new one with the button below.' : 'Sign in with your Microsoft 365 account - choose Login with Microsoft on the sign-in page.'],
|
|
'summary' => ['Account' => (string) $row['user_email']],
|
|
'button' => $local ? ['label' => 'Set a new password', 'url' => portalPasswordLink($user_id, PORTAL_RESET_LINK_MINUTES)] : ['label' => 'Sign in', 'url' => "https://$config_base_url/login.php"],
|
|
'after' => [$local ? 'The link works once, for the next ' . PORTAL_RESET_LINK_MINUTES . ' minutes. If you did not ask, ignore this email - your account stays as it is.' : 'If you did not ask, ignore this email - your account stays as it is.'],
|
|
]);
|
|
}
|
|
|
|
/** Set whether people at a client can sign up. True when it changed. */
|
|
function portalSignupSet($client_id, $on) {
|
|
global $mysqli;
|
|
mysqli_query($mysqli, "UPDATE clients SET client_portal_signup = " . ($on ? 1 : 0) . " WHERE client_id = " . intval($client_id));
|
|
return mysqli_affected_rows($mysqli) === 1;
|
|
}
|
|
|
|
/*
|
|
* ---------- nightly ----------
|
|
*/
|
|
|
|
/** Clear out the links nobody used - sign-ups a week after they ran out, cancelled invites after 90 days. How many went. */
|
|
function portalInvitesNightly() {
|
|
global $mysqli;
|
|
if (!portalInvitesReady()) {
|
|
return 0;
|
|
}
|
|
mysqli_query($mysqli, "DELETE FROM portal_invites WHERE invite_accepted_at IS NULL AND (
|
|
(invite_kind = 'signup' AND invite_expires_at < NOW() - INTERVAL 7 DAY)
|
|
OR (invite_cancelled_at IS NOT NULL AND invite_cancelled_at < NOW() - INTERVAL 90 DAY))");
|
|
return max(0, intval(mysqli_affected_rows($mysqli)));
|
|
}
|