" . escapeHtml($name) . " created"); redirect(); } if (isset($_POST['edit_calendar'])) { validateCSRFToken(); enforceAdminPermission(); $calendar_id = intval($_POST['calendar_id'] ?? 0); $name = trim(strip_tags((string) ($_POST['name'] ?? ''))); if ($name === '') { flashAlert('A calendar needs a name.', 'error'); redirect(); } $color = calendarPostedColor($_POST['color'] ?? ''); mysqli_query($mysqli, "UPDATE calendars SET calendar_name = '" . escapeSql(mb_substr($name, 0, 200)) . "', calendar_color = '$color' WHERE calendar_id = $calendar_id"); logAudit("Calendar", "Edit", "$session_name edited calendar " . escapeSql($name), 0, $calendar_id); flashAlert("Calendar " . escapeHtml($name) . " saved"); redirect(); } if (isset($_GET['delete_calendar'])) { validateCSRFToken(); enforceAdminPermission(); $calendar_id = intval($_GET['delete_calendar']); $calendar_name = (string) getFieldById('calendars', $calendar_id, 'calendar_name'); // Its events go with it, and whoever was invited to one is told it is off $sql = mysqli_query($mysqli, "SELECT event_id FROM calendar_events WHERE event_calendar_id = $calendar_id AND event_archived_at IS NULL"); $cancelled = 0; while ($row = mysqli_fetch_row($sql)) { $event = calendarEventRow($row[0]); $invited = calendarInvitedAttendees($row[0]); if ($event && $invited) { $cancelled += calendarSendInvites($event, 'cancel', $invited); } } mysqli_query($mysqli, "DELETE calendar_event_attendees FROM calendar_event_attendees JOIN calendar_events ON event_id = attendee_event_id WHERE event_calendar_id = $calendar_id"); mysqli_query($mysqli, "DELETE FROM calendar_events WHERE event_calendar_id = $calendar_id"); mysqli_query($mysqli, "DELETE FROM calendars WHERE calendar_id = $calendar_id"); logAudit("Calendar", "Delete", "$session_name deleted calendar " . escapeSql($calendar_name) . " and its events"); flashAlert("Calendar " . escapeHtml($calendar_name) . " and its events deleted" . calendarSentText($cancelled, 'cancellation'), 'error'); redirect(); } if (isset($_POST['share_calendar'])) { validateCSRFToken(); // Publishing a calendar mints an unauthenticated public URL, so keep it to // admins - calendars are not owned by a module. enforceAdminPermission(); $calendar_id = intval($_POST['calendar_id']); $busy_only = isset($_POST['busy_only']) ? 1 : 0; $calendar_name = (string) getFieldById('calendars', $calendar_id, 'calendar_name'); $existing_key = getFieldById('calendars', $calendar_id, 'calendar_feed_key'); if (empty($existing_key)) { // 32 URL-safe base64 chars (~192 bits). Stored in cleartext, like // invoice_url_key, so the link stays re-copyable for a second device. $feed_key = escapeSql(randomString(32)); mysqli_query($mysqli, "UPDATE calendars SET calendar_feed_key = '$feed_key', calendar_feed_busy_only = $busy_only, calendar_feed_created_at = NOW() WHERE calendar_id = $calendar_id"); logAudit("Calendar", "Share", "$session_name published calendar " . escapeSql($calendar_name) . " as a read-only feed", 0, $calendar_id); flashAlert("Calendar " . escapeHtml($calendar_name) . " published - copy the subscription link from Share"); } else { mysqli_query($mysqli, "UPDATE calendars SET calendar_feed_busy_only = $busy_only WHERE calendar_id = $calendar_id"); logAudit("Calendar", "Edit", "$session_name updated feed settings for calendar " . escapeSql($calendar_name), 0, $calendar_id); flashAlert("Sharing for " . escapeHtml($calendar_name) . " saved"); } redirect(); } if (isset($_GET['regenerate_calendar_feed'])) { validateCSRFToken(); enforceAdminPermission(); $calendar_id = intval($_GET['regenerate_calendar_feed']); $calendar_name = (string) getFieldById('calendars', $calendar_id, 'calendar_name'); $feed_key = escapeSql(randomString(32)); mysqli_query($mysqli, "UPDATE calendars SET calendar_feed_key = '$feed_key', calendar_feed_created_at = NOW(), calendar_feed_accessed_at = NULL WHERE calendar_id = $calendar_id"); logAudit("Calendar", "Share", "$session_name regenerated the feed link for calendar " . escapeSql($calendar_name), 0, $calendar_id); flashAlert("New link for " . escapeHtml($calendar_name) . " - the old one has stopped working, so subscribers must subscribe again", 'warning'); redirect(); } if (isset($_GET['unshare_calendar'])) { validateCSRFToken(); enforceAdminPermission(); $calendar_id = intval($_GET['unshare_calendar']); $calendar_name = (string) getFieldById('calendars', $calendar_id, 'calendar_name'); mysqli_query($mysqli, "UPDATE calendars SET calendar_feed_key = NULL, calendar_feed_busy_only = 0, calendar_feed_created_at = NULL, calendar_feed_accessed_at = NULL WHERE calendar_id = $calendar_id"); logAudit("Calendar", "Share", "$session_name stopped sharing calendar " . escapeSql($calendar_name), 0, $calendar_id); flashAlert("Calendar " . escapeHtml($calendar_name) . " is no longer shared", 'error'); redirect(); } /* * ---------- events ---------- */ if (isset($_POST['add_event'])) { validateCSRFToken(); $client_id = intval($_POST['client_id'] ?? 0); if ($client_id) { enforceClientAccess(); enforceCalendarEvent(['event_client_id' => $client_id], 'edit'); } $set = calendarEventFormSet($_POST, $client_id, $error); if ($set === '') { flashAlert($error, 'error'); redirect(); } mysqli_query($mysqli, "INSERT INTO calendar_events SET $set, event_created_by = $session_user_id"); $event_id = mysqli_insert_id($mysqli); [$agents, $contacts, $emails] = calendarPostedAttendees(); calendarSetAttendees($event_id, $client_id, $agents, $contacts, $emails); $event = calendarEventRow($event_id); $sent = !empty($_POST['send_invites']) ? calendarSendInvites($event, 'invite') : 0; logAudit("Calendar Event", "Create", "$session_name added the event " . escapeSql($event['event_title']) . " to calendar " . escapeSql($event['calendar_name']), $client_id, $event_id); flashAlert("Event " . escapeHtml($event['event_title']) . " added" . calendarSentText($sent)); redirect(); } if (isset($_POST['edit_event'])) { validateCSRFToken(); $old = calendarEventForRequest($_POST['event_id'] ?? 0, 'edit'); $event_id = intval($old['event_id']); // It can move to another client - which needs access too $client_id = intval($_POST['client_id'] ?? 0); if ($client_id) { enforceClientAccess(); enforceCalendarEvent(['event_client_id' => $client_id], 'edit'); } $set = calendarEventFormSet($_POST, $client_id, $error); if ($set === '') { flashAlert($error, 'error'); redirect(); } mysqli_query($mysqli, "UPDATE calendar_events SET $set WHERE event_id = $event_id"); // A change a calendar that already has it needs to know about moves it on a version $event = calendarEventRow($event_id); $changed = false; foreach (['event_title', 'event_start', 'event_end', 'event_all_day', 'event_location', 'event_location_id', 'event_description', 'event_repeat', 'event_repeat_interval', 'event_repeat_until'] as $field) { if ((string) $old[$field] !== (string) $event[$field]) { $changed = true; } } if ($changed) { mysqli_query($mysqli, "UPDATE calendar_events SET event_sequence = event_sequence + 1 WHERE event_id = $event_id"); $event['event_sequence'] = intval($event['event_sequence']) + 1; } [$agents, $contacts, $emails] = calendarPostedAttendees(); $attendees = calendarSetAttendees($event_id, $client_id, $agents, $contacts, $emails); $sent = 0; $cancelled = 0; if (!empty($_POST['send_invites'])) { $invite = []; $update = []; foreach (calendarEventAttendees($event_id) as $attendee) { if (!in_array(intval($attendee['attendee_id']), $attendees['new'], true) && !empty($attendee['attendee_invited_at'])) { $update[] = $attendee; } else { $invite[] = $attendee; } } // Newcomers and anyone not yet invited get the invitation; those who have // it get the update when something about it changed $sent += calendarSendInvites($event, 'invite', $invite); if ($changed) { $sent += calendarSendInvites($event, 'update', $update); } // Whoever was taken off and had it in their calendar is told it is off for them $cancelled = calendarSendInvites($event, 'cancel', array_values(array_filter($attendees['removed'], function ($attendee) { return !empty($attendee['attendee_invited_at']); }))); } logAudit("Calendar Event", "Edit", "$session_name edited the event " . escapeSql($event['event_title']), $client_id, $event_id); flashAlert("Event " . escapeHtml($event['event_title']) . " saved" . calendarSentText($sent + $cancelled, '')); redirect(); } if (isset($_POST['send_event_invites'])) { validateCSRFToken(); $event = calendarEventForRequest($_POST['event_id'] ?? 0, 'edit'); $event_id = intval($event['event_id']); if (!calendarCanEmail()) { flashAlert('Email is not set up, so there is nothing to send the invitation with.', 'error'); redirect(); } // Those who have it get it again as an update; the rest are invited $invited = calendarInvitedAttendees($event_id); $not_invited = array_values(array_filter(calendarEventAttendees($event_id), function ($attendee) { return empty($attendee['attendee_invited_at']); })); $sent = calendarSendInvites($event, 'invite', $not_invited) + calendarSendInvites($event, 'update', $invited); if (!$sent) { flashAlert('Nobody on this event has an email address to send it to.', 'warning'); redirect(); } flashAlert("Invitation for " . escapeHtml($event['event_title']) . " emailed to $sent " . ($sent === 1 ? 'person' : 'people')); redirect(); } if (isset($_GET['skip_event_occurrence'])) { validateCSRFToken(); $event = calendarEventForRequest($_GET['skip_event_occurrence'], 'edit'); $event_id = intval($event['event_id']); $date = (string) ($_GET['date'] ?? ''); if (!calendarEventRepeats($event) || !calendarIsDate($date)) { flashAlert('That is not one of its occurrences.', 'error'); redirect(); } $skip = calendarSkippedDates($event); $skip[] = $date; $skip = array_values(array_unique($skip)); sort($skip); mysqli_query($mysqli, "UPDATE calendar_events SET event_repeat_skip = '" . escapeSql(implode(',', $skip)) . "', event_sequence = event_sequence + 1 WHERE event_id = $event_id"); // The people who have the series in their calendar get it without that day $event = calendarEventRow($event_id); $sent = calendarSendInvites($event, 'update', calendarInvitedAttendees($event_id)); $when = date('D, M j', strtotime($date)); logAudit("Calendar Event", "Edit", "$session_name skipped $when of the event " . escapeSql($event['event_title']), intval($event['event_client_id']), $event_id); flashAlert("" . escapeHtml($event['event_title']) . " skipped on $when" . calendarSentText($sent, 'update')); redirect(); } if (isset($_GET['delete_event'])) { validateCSRFToken(); $event = calendarEventForRequest($_GET['delete_event'], 'delete'); $event_id = intval($event['event_id']); // The people who have it in their calendar are told it is off $cancelled = calendarSendInvites($event, 'cancel', calendarInvitedAttendees($event_id)); mysqli_query($mysqli, "DELETE FROM calendar_event_attendees WHERE attendee_event_id = $event_id"); mysqli_query($mysqli, "DELETE FROM calendar_events WHERE event_id = $event_id"); logAudit("Calendar Event", "Delete", "$session_name deleted the event " . escapeSql($event['event_title']), intval($event['event_client_id']), $event_id); flashAlert("Event " . escapeHtml($event['event_title']) . " deleted" . calendarSentText($cancelled, 'cancellation'), 'error'); redirect(); } if (isset($_GET['export_event_ics'])) { $event = calendarEventForRequest($_GET['export_event_ics']); $ics = icsCalendar(['name' => $event['event_title']], [calendarEventIcsItem($event)], $config_timezone); $filename = trim(preg_replace('/[^A-Za-z0-9_-]+/', '_', $event['event_title']), '_') ?: 'event'; header('Content-Type: text/calendar; charset=utf-8'); header("Content-Disposition: attachment; filename=\"$filename.ics\""); echo $ics; exit; } /* * ---------- my schedule, as a subscription ---------- * Each agent's own: their scheduled tickets, the events they are on and the * projects they manage, for the calendar on their phone, Nextcloud or Google. */ if (isset($_POST['calendar_feed_save'])) { validateCSRFToken(); $scope = ($_POST['scope'] ?? 'mine') === 'everyone' ? 'everyone' : 'mine'; $tickets = !empty($_POST['feed_tickets']) ? 1 : 0; $events = !empty($_POST['feed_events']) ? 1 : 0; $projects = !empty($_POST['feed_projects']) ? 1 : 0; $busy_only = !empty($_POST['busy_only']) ? 1 : 0; $fields = "feed_scope = '$scope', feed_tickets = $tickets, feed_events = $events, feed_projects = $projects, feed_busy_only = $busy_only"; if (calendarUserFeed($session_user_id)) { mysqli_query($mysqli, "UPDATE user_calendar_feeds SET $fields WHERE feed_user_id = $session_user_id"); flashAlert('Your calendar link is saved - subscribers pick up the change on their next refresh'); } else { $feed_key = escapeSql(randomString(40)); mysqli_query($mysqli, "INSERT INTO user_calendar_feeds SET feed_user_id = $session_user_id, feed_key = '$feed_key', $fields"); logAudit("Calendar", "Share", "$session_name created a subscription link to their schedule"); flashAlert('Your calendar link is ready - copy it from My calendar link'); } redirect(); } if (isset($_GET['calendar_feed_regenerate'])) { validateCSRFToken(); $feed_key = escapeSql(randomString(40)); mysqli_query($mysqli, "UPDATE user_calendar_feeds SET feed_key = '$feed_key', feed_created_at = NOW(), feed_accessed_at = NULL WHERE feed_user_id = $session_user_id"); logAudit("Calendar", "Share", "$session_name regenerated the subscription link to their schedule"); flashAlert('New calendar link made - the old one has stopped working, so subscribe again with the new one', 'warning'); redirect(); } if (isset($_GET['calendar_feed_stop'])) { validateCSRFToken(); mysqli_query($mysqli, "DELETE FROM user_calendar_feeds WHERE feed_user_id = $session_user_id"); logAudit("Calendar", "Share", "$session_name stopped the subscription link to their schedule"); flashAlert('Your calendar link has stopped working', 'error'); redirect(); }