commit 74e1f0054a14e7189492b57fcd0169f4b3a89978
Author: jonas
Date: Sat Oct 21 04:55:29 2023 +0200
first commit
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2f1e51e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+## ICS Parser and Renderer ##
+
+The index.php takes a public ICS via GET parameter and renders it either as a feed view with n-amount of future events or as week/month overview
\ No newline at end of file
diff --git a/calendar-month.php b/calendar-month.php
new file mode 100644
index 0000000..c73b716
--- /dev/null
+++ b/calendar-month.php
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/calendar-week.html b/calendar-week.html
new file mode 100644
index 0000000..7853837
--- /dev/null
+++ b/calendar-week.html
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..0f426a4
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,5 @@
+{
+ "require": {
+ "johngrogg/ics-parser": "^3"
+ }
+}
\ No newline at end of file
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 0000000..b8c1114
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,83 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "f1be773dbf3c5fddbb5187c8c187ceb4",
+ "packages": [
+ {
+ "name": "johngrogg/ics-parser",
+ "version": "v3.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/u01jmg3/ics-parser.git",
+ "reference": "eeb51c4c0c06e6df3266f85ea774ca314536aba4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/u01jmg3/ics-parser/zipball/eeb51c4c0c06e6df3266f85ea774ca314536aba4",
+ "reference": "eeb51c4c0c06e6df3266f85ea774ca314536aba4",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": ">=5.6.40"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5|^9|^10"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "ICal": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jonathan Goode",
+ "role": "Developer/Owner"
+ },
+ {
+ "name": "John Grogg",
+ "email": "john.grogg@gmail.com",
+ "role": "Developer/Prior Owner"
+ }
+ ],
+ "description": "ICS Parser",
+ "homepage": "https://github.com/u01jmg3/ics-parser",
+ "keywords": [
+ "iCalendar",
+ "ical",
+ "ical-parser",
+ "ics",
+ "ics-parser",
+ "ifb"
+ ],
+ "support": {
+ "issues": "https://github.com/u01jmg3/ics-parser/issues",
+ "source": "https://github.com/u01jmg3/ics-parser/tree/v3.3.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sponsors/u01jmg3",
+ "type": "github"
+ }
+ ],
+ "time": "2023-10-10T09:58:49+00:00"
+ }
+ ],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": [],
+ "platform-dev": [],
+ "plugin-api-version": "2.6.0"
+}
diff --git a/css/calendar-week.css b/css/calendar-week.css
new file mode 100644
index 0000000..ab46f4d
--- /dev/null
+++ b/css/calendar-week.css
@@ -0,0 +1,27 @@
+.calendar-header.hours {
+ grid-template-columns: 120px;
+}
+
+.calendar-weekday-column {
+ background: white;
+ position: relative;
+ gap: 1px;
+
+ display: grid;
+}
+
+.test-entry {
+ position: initial;
+ background: yellow;
+
+ padding: 0px 5px;
+ top: calc(100% / 23 * 11 + 1px);
+ height: calc(100% / 23 * 3 - 1px);
+}
+
+.test-entry.two {
+ background: red;
+
+ top: calc(100% / 23 * 1.1 + 1px);
+ height: calc(100% / 23 * 3 - 1px);
+}
\ No newline at end of file
diff --git a/css/calendar.css b/css/calendar.css
new file mode 100644
index 0000000..0d4f85d
--- /dev/null
+++ b/css/calendar.css
@@ -0,0 +1,188 @@
+.calendar-parent {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+
+ background: #555;
+ border: 1px solid #555;
+}
+
+/* Grid setup */
+.calendar-header,
+.calendar-body {
+ display: grid;
+ grid-template-columns: 120px repeat(7, 1fr);
+ grid-auto-rows: 1fr;
+ width: 100%;
+ gap: 1px;
+}
+
+/* Body of the calendar */
+.calendar-body {
+ height: 100%;
+ min-height: 0;
+}
+
+/* Common layout of all grid elements */
+.calendar-header-value,
+.calendar-entry {
+ padding: 5px;
+ text-align: center;
+}
+
+/* Header elements within grid */
+.calendar-header-value {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ background: #000;
+ color: white;
+ font-weight: bold;
+}
+
+/* First header, which shows the current month */
+.calendar-header-value.first {
+ display: flex;
+ /*justify-content: space-between;*/
+ justify-content: center;
+ align-items: center;
+
+ font-size: larger;
+
+ -webkit-user-select: none; /* Safari */
+ user-select: none;
+}
+
+/* Links in first header to change month */
+.calendar-header-value > a {
+ margin: 0px 5px;
+ text-decoration: none;
+ color: #7a7ac7;
+}
+
+/* Month day entry */
+.calendar-entry {
+ display: inline-flex;
+ flex-direction: column;
+ overflow: hidden;
+ min-width: 0;
+}
+
+/* Day entry for previous or next month */
+.calendar-entry.other-month {
+ background: #222 !important;
+ color: #bbb;
+}
+.calendar-entry.other-month .day-event-entry {
+ background: #444;
+ border-color: #333;
+}
+
+/* Two different backgrounds for odd and even rows*/
+.calendar-entry.even-row {
+ background: #ccc;
+}
+.calendar-entry.odd-row {
+ background: #aaa;
+}
+/* Yet again different backgrounds for weekend days */
+.calendar-entry.weekend {
+ background: #777;
+}
+
+/* Header of month day entry */
+.day-header {
+ display: inline-block;
+ width: 100%;
+ text-align: center;
+ font-weight: bold;
+ padding-bottom: 4px;
+}
+
+/* Modification of day-header that highlights today */
+.day-header.today > span {
+ padding: 0px 5px;
+ border-radius: 3px;
+ background: black;
+ color: white;
+}
+
+/* Scrollbox containing the day events element */
+.day-events-scrollbox {
+ flex: 1;
+ overflow: hidden;
+ position: relative;
+ border-radius: 3px;
+}
+
+/* Element containing all event elements for a day */
+.day-events {
+ display: flex;
+ height: 100%;
+
+ flex-direction: column;
+ overflow: scroll;
+ scroll-behavior: smooth;
+
+ gap: 2px;
+
+ scrollbar-width: none;
+}
+
+/* Hide day-events scrollbars */
+.day-events::-webkit-scrollbar {
+ display: none;
+}
+
+
+/* Shadow for the scrollbox */
+.day-events-shadow {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ width: 100%;
+ height: 100%;
+
+ pointer-events: none;
+}
+
+/* Top, bottom and top-bottom shadows for day-events-scrollbox */
+.box-shadow-bottom {
+ box-shadow: inset 0px -11px 8px -10px #000;
+}
+.box-shadow-top {
+ box-shadow: inset 0px 11px 8px -10px #000;
+}
+.box-shadow-topbottom {
+ box-shadow:
+ inset 0px -11px 8px -10px #000,
+ inset 0px 11px 8px -10px #000;
+}
+
+/* Event entry for a day */
+.day-event-entry {
+ background: yellow;
+ border: 1px solid darkorange;
+ border-radius: 3px;
+ padding: 2px 4px;
+ margin: 0px 2px;
+
+ display: inline-flex;
+}
+
+/* Event timespan */
+.day-event-entry > .time {
+ font-weight: bold;
+ margin-right: 3px;
+ white-space: nowrap;
+}
+
+/* Event Description */
+.day-event-entry > .description {
+ display: inline-block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
\ No newline at end of file
diff --git a/css/dialog.css b/css/dialog.css
new file mode 100644
index 0000000..d2eca16
--- /dev/null
+++ b/css/dialog.css
@@ -0,0 +1,54 @@
+.dialog-parent {
+ display: flex;
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ width: 100vw;
+ height: 100vh;
+
+ justify-content: center;
+ align-items: center;
+
+ background: rgba(0, 0, 0, 0.5);
+}
+
+.dialog {
+ display: inline-flex;
+ position: relative;
+ min-width: 320px;
+ min-height: 100px;
+ max-width: 90vw;
+ max-height: 90vh;
+
+ background: white;
+ box-shadow: 0px 0px 5px black;
+}
+
+.dialog-content {
+ display: inline-block;
+ margin-bottom: 35px;
+ padding: 10px;
+
+ overflow: scroll;
+}
+
+.dialog-buttons {
+ display: flex;
+ position: absolute;
+ left: 0px;
+ bottom: 0px;
+ height: 35px;
+ width: 100%;
+
+ justify-content: center;
+ align-items: center;
+ column-gap: 15px;
+}
+
+.dialog-content > input[type=text] {
+ width: 100%;
+}
+
+.dialog-content > label {
+ font-size: smaller;
+}
\ No newline at end of file
diff --git a/css/flow.css b/css/flow.css
new file mode 100644
index 0000000..bf6a1c6
--- /dev/null
+++ b/css/flow.css
@@ -0,0 +1,81 @@
+body {
+ font-size: 20px;
+}
+
+.page-content {
+ box-shadow: none;
+}
+
+.flow-parent {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ height: 100%;
+}
+
+.flow-header {
+ width: 100%;
+}
+
+.flow-header-value {
+ margin: 0px 18px;
+
+ font-weight: bold;
+ font-size: 125%;
+}
+
+.flow-element, .flow-header {
+ display: flex;
+ margin: 10px;
+ width: 100%;
+ /*max-width: 400px;*/
+}
+
+.flow-day-name,
+.flow-header-spacer {
+ font-weight: bold;
+ font-size: 175%;
+ margin: 0px 10px;
+ text-align: center;
+
+ min-width: 50px;
+}
+.flow-day-name .weekday {
+ font-size: 50%;
+}
+
+.flow-day-entries {
+ display: grid;
+ margin: 10px;
+ gap: 5px;
+
+ flex: 1;
+}
+
+.flow-day-entry {
+ background: yellow;
+ border: 1px solid darkorange;
+ border-radius: 5px;
+
+ padding: 4px 6px;
+ margin: 0px 2px;
+
+ min-width: 0;
+
+ display: inline-flex;
+ flex-direction: column;
+}
+
+.flow-day-entry > .time {
+ font-size: smaller;
+ font-weight: bold;
+ white-space: nowrap;
+}
+
+.flow-day-entry > .description {
+ display: inline-block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
\ No newline at end of file
diff --git a/css/style.css b/css/style.css
new file mode 100644
index 0000000..94b7284
--- /dev/null
+++ b/css/style.css
@@ -0,0 +1,36 @@
+body {
+ display: flex;
+ flex-direction: column;
+ margin: 0px;
+ width: 100vw;
+ height: 100vh;
+
+ font-family: sans-serif;
+ font-size: 14px;
+}
+
+.page-title {
+ font-size: larger;
+ font-weight: bold;
+ padding: 10px;
+ padding-bottom: 0px;
+}
+
+.page-content {
+ flex: 1;
+ min-height: 0;
+ margin: 10px;
+
+ box-shadow: 5px 7px #777;
+}
+
+.hidden {
+ display: none !important;
+}
+
+/* Disabled/empty link */
+a[href="#"] {
+ color: #555 !important;
+ cursor: not-allowed;
+ pointer-events: none;
+}
\ No newline at end of file
diff --git a/flow.php b/flow.php
new file mode 100644
index 0000000..25d0c39
--- /dev/null
+++ b/flow.php
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/php/calendar-month.php b/php/calendar-month.php
new file mode 100644
index 0000000..5764c74
--- /dev/null
+++ b/php/calendar-month.php
@@ -0,0 +1,217 @@
+ 12 ||
+ $requested_month < 1
+ ) {
+ $requested_month = idate("m");
+ }
+
+ return $requested_month;
+}
+
+/**
+ * Echoes the requested months' name
+ */
+function echoMonthName () {
+ echo getLocalizedMonthName (getRequestedMonth ());
+}
+
+/**
+ * Returns HTML for a days' events list (CSS class day-events-scrollbox)
+ *
+ * @param ICal $ical ICal object
+ * @param int $day Day of month
+ * @param int $month Month (defaults to current month)
+ * @param int $year Year (defaults to current year)
+ *
+ * @return string HTML containing DIV of class day-events-scrollbox
+ * @return false False on failure
+ */
+function generateDayEventsList ($ical, $day, $month = null, $year = null) {
+ if (! $ical) return false;
+
+ $html = '
+
+ ';
+
+ foreach (getEventsForDay ($ical, $day, $month, $year) as $event) {
+ $dtstart = $ical -> iCalDateToDateTime($event -> dtstart_array[3]);
+ $dtend = $ical -> iCalDateToDateTime($event -> dtend_array[3]);
+
+ $today_start = new DateTime(
+ date('Y-m-d H:i:s', mktime(0, 0, 0, $month, $day, $year)),
+ $dtstart -> getTimezone()
+ );
+ $today_end = clone $today_start; $today_end -> modify("+1 day");
+
+ $time_separator = "-";
+ $time_prefix = "";
+ $time_start = $dtstart -> format('H:i');
+ $time_end = $dtend -> format('H:i');
+
+ // When start date is in past, don't display start time
+ if ($dtstart < $today_start) {
+ $time_start = "";
+ $time_separator = "Bis ";
+ }
+
+ // When end date is in future, don't display end time
+ if ($dtend > $today_end) {
+ $time_end = "";
+ $time_separator = "";
+ $time_prefix = "Ab ";
+ }
+
+ // Assemble time display string
+ $time_str = "{$time_prefix}{$time_start}{$time_separator}{$time_end}";
+
+ // Check if event is all day
+ if ($time_start == "00:00" && $time_end == "00:00") {
+ //$time_str = "Ganztägig";
+ $time_str = "";
+ }
+
+ // Check if event is multi day
+ if ($time_start == "" && $time_end == "") {
+ $time_str = "Mehrtägig";
+ }
+
+ // Check if multi day event without start times
+ if ($time_start . $time_end == "00:00") {
+ $time_str = "Mehrtägig";
+ }
+
+ $html = sprintf (
+ $html,
+ sprintf(
+ '
+ %s
+ %s
+
+ %%s',
+ $time_str,
+ $event -> summary
+ ),
+ );
+ }
+
+ $html = sprintf ($html, "");
+ return $html;
+}
+
+/**
+ * Returns HTML with all elements going in the DIV with CSS class calendar-body
+ *
+ * @param ICal $Ical ICal object
+ * @param int $month Month
+ *
+ * @return string HTML containing elements for calendar body
+ * @return false False on failure
+ */
+function generateCalendarBodyItems ($ical, $month) {
+ if ($month > 12 || $month < 1) return false;
+
+ $html = '%s';
+ $odd_row = false;
+
+ // Number of days in month
+ $month_max_days = idate ("t", mktime (0, 0, 0, $month));
+
+ // First of month unix timestamp
+ $first_of_month = new DateTime (
+ date('Y-m-d H:i:s', mktime (0, 0, 0, $month, 1))
+ );
+
+ // Days needed to pad the calendar at the start
+ $pad_days = idate ("N", $first_of_month -> getTimestamp()) - 1;
+
+ // First of the month, minus the pad days
+ $day = $first_of_month -> modify ("-{$pad_days} days");
+
+ // Number of total entries in calendar
+ $num_rows = ceil (($pad_days + $month_max_days) / 7);
+ $num_entries = ($num_rows * 8);
+
+ // Loop over all entry slots and generate entries
+ for ($item_index = 0; $item_index < $num_entries; $item_index ++) {
+
+ // If item_index is beginning of row, add week number entry
+ if ($item_index % 8 == 0) {
+ $week = $day -> format ("W");
+ $html = sprintf (
+ $html,
+ "
+ %s",
+ );
+
+ // Swap odd and even rows
+ $odd_row = ! $odd_row;
+
+ continue;
+ }
+
+ $calendar_entry_html = '
+
+
+ %s
+
+ %s
+ ';
+
+ $html = sprintf (
+ $html,
+ sprintf(
+ $calendar_entry_html,
+ $odd_row? "odd-row" : "even-row",
+ (isDateTimeWithinMonth ($day, $month)) ? "" : "other-month",
+ (isDateTimeDuringWeekend ($day)) ? "weekend" : "",
+ (isDateTimeToday ($day))? "today" : "",
+ $day -> format ("d"),
+ generateDayEventsList(
+ $ical,
+ intval ($day -> format ("d")),
+ intval ($day -> format ("m")),
+ ),
+ "%s"
+ )
+ );
+
+ $day -> modify ("+1 day");
+ }
+
+ return sprintf($html, "");
+}
+
+/**
+ * Echoes HTML to insert all calendar day entries
+ */
+function echoCalendarEntries() {
+ global $ical;
+
+ echo generateCalendarBodyItems($ical, getRequestedMonth());
+}
\ No newline at end of file
diff --git a/php/flow.php b/php/flow.php
new file mode 100644
index 0000000..923b8bc
--- /dev/null
+++ b/php/flow.php
@@ -0,0 +1,207 @@
+
+
+
+ %%s
+
+
+ ',
+ $date -> format ("d"),
+ getLocalizedShortDayName (
+ intval ($date -> format ("N"))
+ )
+ );
+
+ // Fetch events for the specified date
+ $events = getEventsForDate ($ical, $date);
+
+ // Skip empty days
+ if (count ($events) == 0) return "";
+
+ // Iterate over each event and add an event entry to the HTML
+ foreach ($events as $event) {
+ $dtstart = $ical -> iCalDateToDateTime($event -> dtstart_array[3]);
+ $dtend = $ical -> iCalDateToDateTime($event -> dtend_array[3]);
+
+ $today_end = clone $date; $today_end -> modify("+1 day");
+
+ $time_separator = " - ";
+ $time_prefix = "";
+ $time_start = $dtstart -> format('H:i');
+ $time_end = $dtend -> format('H:i');
+
+ // When start date is in past, don't display start time
+ if ($dtstart < $date) {
+ $time_start = "";
+ $time_separator = "Bis ";
+ }
+
+ // When end date is in future, don't display end time
+ if ($dtend > $today_end) {
+ $time_end = "";
+ $time_separator = "";
+ $time_prefix = "Ab ";
+ }
+
+ // Assemble time display string
+ $time_str = "{$time_prefix}{$time_start}{$time_separator}{$time_end}";
+
+ // Check if event is all day
+ if ($time_start == "00:00" && $time_end == "00:00") {
+ $time_str = "Ganztägig";
+ }
+
+ // Check if event is multi day
+ if ($time_start == "" && $time_end == "") {
+ $time_str = "Mehrtägig";
+ }
+
+ // Check if multi day event without start times
+ if ($time_start . $time_end == "00:00") {
+ $time_str = "Mehrtägig";
+ }
+
+ $html = sprintf (
+ $html,
+ sprintf (
+ '
+ %s
+ %s
+
+ %%s',
+ $event -> summary,
+ $time_str
+ ),
+ );
+ }
+
+ $html = sprintf ($html, '');
+ return $html;
+}
+
+/**
+ * Return HTML with flow header for specific date
+ *
+ * @param DateTime $date DateTime object
+ *
+ * @return string Returns HTML with flow header
+ * @return false False on failure
+ */
+function generateFlowHeader ($date) {
+ if ($date == null) return false;
+
+ return sprintf (
+ '',
+ getLocalizedMonthName (intval ($date -> format ("m")))
+ );
+}
+
+/**
+ * Return HTML for the flow body
+ *
+ * @param ICal $ical ICal object
+ * @param DateTime $date DateTime object
+ * @param int $days Days to display
+ *
+ * @return string Returns HTML with the entire flow data
+ * @return false False on failure
+ */
+function generateFlowData ($ical, $date, $days) {
+ if ($ical == null || $date == null) return false;
+
+ $html = generateFlowHeader ($date) . "\n%s";
+
+ $end_date = clone $date; $end_date -> modify ("+{$days} days");
+ while ($date <= $end_date) {
+ if ($date -> format ("d") == "01") $html = sprintf (
+ $html,
+ generateFlowHeader ($date) . "\n%s"
+ );
+
+ $html = sprintf (
+ $html,
+ generateFlowEntryForDay($ical, $date) . "%s"
+ );
+
+ $date -> modify ("+1 day");
+ }
+
+ $html = sprintf ($html, "");
+ return $html;
+}
+
+/**
+ * Echoes the entire HTML for the flow body
+ */
+function echoFlowBody () {
+ global $ical;
+
+ echo generateFlowData (
+ $ical,
+ getRequestedStartDate(),
+ getRequestedDisplayedDays()
+ );
+}
\ No newline at end of file
diff --git a/php/html-components.php b/php/html-components.php
new file mode 100644
index 0000000..cda5e5e
--- /dev/null
+++ b/php/html-components.php
@@ -0,0 +1,73 @@
+", $message);
+ $message = str_replace(" ", " ", $message);
+
+ global $_ERRORS;
+ $_ERRORS[] = $message;
+}
+
+/**
+ * Write text into stderr
+ *
+ * @param string $text Text to log
+ */
+function debug ($text) {
+ error_log("\n> $text" . PHP_EOL);
+}
+
+/**
+ * Echoes HTML code to display all collected error messages
+ */
+function insertErrorsHtml() {
+ global $_ERRORS;
+
+ if (count ($_ERRORS) == 0) return;
+
+ // Dialog frame HTML
+ $base_html = '
+
+
+
+ Fehler beim Laden der Seite:
+
+ %s
+
+
+ OK
+
+
+
+ ';
+
+ $errors_html = "";
+
+ foreach ($_ERRORS as $error) {
+ $errors_html .= "{$error} ";
+ }
+
+ printf ($base_html, $errors_html);
+}
+
+/**
+ * Hides the dialog box asking for an ICS URL if one
+ * was already specified by the user
+ */
+function hideMissingIcsDialog () {
+ global $_GET;
+
+ if (isset ($_GET["ics_url"]) && $_GET["ics_url"] != "") {
+ echo "hidden";
+ }
+}
\ No newline at end of file
diff --git a/php/ics.php b/php/ics.php
new file mode 100644
index 0000000..be615ef
--- /dev/null
+++ b/php/ics.php
@@ -0,0 +1,206 @@
+ 12 || $month < 1) return "";
+
+ return [
+ "Januar",
+ "Februar",
+ "März",
+ "April",
+ "Mai",
+ "Juni",
+ "Juli",
+ "August",
+ "September",
+ "Oktober",
+ "November",
+ "Dezember"
+ ][$month - 1];
+}
+
+/**
+ * Returns the short name of a day of week
+ *
+ * @param int $day_of_week DoW index, 1 to 7
+ *
+ * @return string Day of week name
+ * @return string Blank string if invalid DoW was given
+ */
+function getLocalizedShortDayName ($day_of_week) {
+ if ($day_of_week < 1 || $day_of_week > 7) return "";
+
+ return [
+ "Mo.",
+ "Di.",
+ "Mi.",
+ "Do.",
+ "Fr.",
+ "Sa.",
+ "So."
+ ][$day_of_week - 1];
+}
+
+/**
+ * Tests if a given date is today
+ *
+ * @param DateTime $date DateTime object to compare against
+ *
+ * @return true when the DateTime object is today
+ * @return false when the DateTime object is not today
+ */
+function isDateTimeToday ($date) {
+ return $date -> format ("Y-m-d") == date ("Y-m-d");
+}
+
+/**
+ * Tests if a given date is within the current month
+ *
+ * @param DateTime $date DateTime object to compare against
+ * @param int $month Month to check against (defaults to current month)
+ *
+ * @return true when the DateTime object is within the current month
+ * @return false when the DateTime object is not within the current month
+ */
+function isDateTimeWithinMonth ($date, $month = null) {
+ $compareDate = new DateTime ();
+ if ($month != null) $compareDate = new DateTime ("this year $month/01");
+
+ return $date -> format ("Y-m") == $compareDate -> format ("Y-m");
+}
+
+/**
+ * Tests if a given date is during the weekend
+ *
+ * @param DateTime $date DateTime object to test
+ *
+ * @return true when the DateTime object is a weekend day
+ * @return false when the DateTime object is a week day
+ */
+function isDateTimeDuringWeekend ($date) {
+ return intval ($date -> format ("N")) > 5;
+}
+
+/**
+ * Fetches an ICal object from an ICS URL
+ *
+ * @param string $url ICS URL to fetch from
+ *
+ * @return ICal ICal object
+ */
+function getCalendar ($url) {
+ try {
+ $ical = new ICal (false, array(
+ 'defaultSpan' => 2, // Default value
+ 'defaultTimeZone' => 'Europe/Berlin',
+ 'defaultWeekStart' => 'MO', // Default value
+ 'disableCharacterReplacement' => false, // Default value
+ 'filterDaysAfter' => null, // Default value
+ 'filterDaysBefore' => null
+ ));
+
+ $ical -> initUrl(
+ $url,
+ $username = null,
+ $password = null,
+ $userAgent = null
+ );
+ } catch (Exception $e) {
+ showError( "ICal error: " . $e -> getMessage() );
+ return $ical;
+ }
+
+ return $ical;
+}
+
+/**
+ * Returns all events from an iCal object
+ *
+ * @param ICal $ical ICal object
+ *
+ * @return array Array of event objects
+ * @return false False on failure
+ */
+function getEvents ($ical) {
+ if (! $ical) return false;
+
+ return $ical -> sortEventsWithOrder (
+ $ical -> events ()
+ );
+}
+
+/**
+ * Returns all events from an ICal object for a specific day
+ *
+ * @param ICal $ical ICal object
+ * @param int $day Day
+ * @param int $month Month (defaults to current month)
+ * @param int $year Year (default to current year)
+ *
+ * @return array Array of event objects
+ * @return false False on failure
+ */
+function getEventsForDay ($ical, $day, $month = null, $year = null) {
+ if (! $ical) return false;
+
+ $date_format = "Y-m-d h:i:s";
+
+ return $ical -> sortEventsWithOrder (
+ $ical -> eventsFromRange (
+ date ($date_format, mktime (0, 0, 0, $month, $day, $year)),
+ date ($date_format, mktime (23, 59, 59, $month, $day, $year)),
+ )
+ );
+}
+
+/**
+ * Wrapper for getEventsForDay.
+ * Returns all events from an ICal object for a specific date
+ *
+ * @param ICal $ical ICal object
+ * @param DateTime $date DateTime object
+ *
+ * @return array Array of event objects
+ * @return false False on failure
+ */
+function getEventsForDate ($ical, $date) {
+ return getEventsForDay (
+ $ical,
+ intval($date -> format ("d")),
+ intval($date -> format ("m")),
+ intval($date -> format ("Y"))
+ );
+}
+
+/**
+ * Get events from ICal object limited by a start date
+ * and amount to days forward from the start date
+ *
+ * @param ICal $ical ICal object
+ * @param DateTime $startDate Date to start from
+ * @param int $days Amount of days to get
+ */
+function getEventsFromRange ($ical, $startDate, $days) {
+ if (! $ical) return false;
+
+ return $ical -> sortEventsWithOrder (
+ $ical -> getEventsFromRange (
+ $startDate -> format ("Y-m-d H:i:s"),
+ $startDate -> modify ("+ {$days} days") -> format ("Y-m-d H:i:s")
+ )
+ );
+}
\ No newline at end of file
diff --git a/script/autoscroll.js b/script/autoscroll.js
new file mode 100644
index 0000000..5ddcd15
--- /dev/null
+++ b/script/autoscroll.js
@@ -0,0 +1,44 @@
+// Defines the framerate of autoscroller
+setInterval(autoScroll, 50);
+
+/**
+ * Auto scrolls all elements with class autoscroll
+ */
+function autoScroll () {
+ Array.from (document.getElementsByClassName("autoscroll")).forEach((objDiv) => {
+ // Skip non-scrollable
+ if (objDiv.scrollHeight - objDiv.clientHeight == 0) return;
+
+ // Wait timeout until scroll direction change
+ let scrollpause = parseInt(objDiv.dataset.scrollpause);
+ if (scrollpause != NaN && scrollpause > 0) {
+ scrollpause --;
+ objDiv.dataset.scrollpause = scrollpause;
+
+ return;
+ }
+
+ // Step either forward or backwards by one
+ let direction = 1;
+ if (objDiv.dataset.scrolldirection == "up") direction = -1;
+ objDiv.scrollTop += direction;
+
+ // If this is the first scroll step after, remove the scrollpause tag
+ if (scrollpause != NaN && scrollpause == 0) {
+ delete objDiv.dataset.scrollpause;
+ return;
+ }
+
+ // Reached the bottom, turn around
+ if (objDiv.scrollTop == objDiv.scrollHeight - objDiv.clientHeight) {
+ objDiv.dataset.scrolldirection = "up";
+ objDiv.dataset.scrollpause = 50;
+ }
+
+ // Reached the top, turn around
+ if (objDiv.scrollTop == 0) {
+ delete objDiv.dataset.scrolldirection;
+ objDiv.dataset.scrollpause = 50;
+ }
+ });
+}
\ No newline at end of file
diff --git a/script/calendar.js b/script/calendar.js
new file mode 100644
index 0000000..87fb506
--- /dev/null
+++ b/script/calendar.js
@@ -0,0 +1,100 @@
+/**
+ * Global variables
+ */
+var requestedMonth;
+
+/**
+ * Checks the scroll status of an element on a scroll
+ * event and adds box shadows either top, bottom or both
+ * depening on the position of the scroll viewport
+ *
+ * @param {Event} e Scroll event object
+ */
+function onscrollTestScrollShadow(e) {
+ let shadowTarget = e.target.parentElement.children[1];
+
+ if (e.target.clientHeight == e.target.scrollHeight) {
+ // Target has no overflowing content and is not scrollable
+ shadowTarget.classList.remove("box-shadow-top");
+ shadowTarget.classList.remove("box-shadow-bottom");
+ shadowTarget.classList.remove("box-shadow-topbottom");
+ return;
+ }
+
+ if (e.target.scrollTop == e.target.scrollHeight - e.target.clientHeight) {
+ // Scroll is at bottom
+ shadowTarget.classList.add("box-shadow-top");
+ shadowTarget.classList.remove("box-shadow-bottom");
+ shadowTarget.classList.remove("box-shadow-topbottom");
+ } else if (e.target.scrollTop == 0) {
+ // Scroll is at top
+ shadowTarget.classList.remove("box-shadow-top");
+ shadowTarget.classList.add("box-shadow-bottom");
+ shadowTarget.classList.remove("box-shadow-topbottom");
+ } else {
+ // Scroll is in middle somewhere
+ shadowTarget.classList.remove("box-shadow-top");
+ shadowTarget.classList.remove("box-shadow-bottom");
+ shadowTarget.classList.add("box-shadow-topbottom");
+ }
+}
+
+/**
+ * Prepares an element with class day-events for scrolling, resetting
+ * the scroll position to top, initializing the onscrollTestScrollShadow
+ * function and adding the same function as scroll event handler
+ *
+ * @param {Element} parent Parent element with class calendar-element
+ */
+function setupScroll (parent) {
+ if (! parent.classList.contains("calendar-entry")) return;
+ if (parent.children.length <= 1) return;
+ if (parent.children[1].childElementCount == 0) return;
+
+ let target = parent.children[1].children[0];
+ if (! target.classList.contains("day-events")) return;
+
+ target.scrollTop = 0;
+ onscrollTestScrollShadow ( {target: target} );
+ target.addEventListener("scroll", onscrollTestScrollShadow);
+}
+
+/**
+ * Populates the month forward and backward buttons with proper links
+ * and displays the correct month name in the top left corner
+ */
+function setupMonthDisplayAndButtons () {
+ fetchRequestedMonth();
+
+ let monthNameElement = document.getElementById("monthName");
+ let monthFwdElement = document.getElementById("monthFwd");
+ let monthBckElement = document.getElementById("monthBck");
+ let urlSearchParams = new URLSearchParams(window.location.search);
+
+ monthNameElement.innerText = getMonthName(requestedMonth);
+
+ if (requestedMonth + 1 > 12) {
+ monthFwdElement.href = "#";
+ } else {
+ urlSearchParams.set("month", requestedMonth + 1);
+ monthFwdElement.href = "?" + urlSearchParams.toString();
+ }
+
+ if (requestedMonth - 1 < 1) {
+ monthBckElement.href = "#";
+ } else {
+ urlSearchParams.set("month", requestedMonth - 1);
+ monthBckElement.href = "?" + urlSearchParams.toString();
+ }
+
+}
+
+
+// Add shadow indicators to calendar event lists when scrolling is available
+window.addEventListener("DOMContentLoaded", () => {
+ let calendarElement = document.getElementsByClassName("calendar-body")[0];
+ Array.from(calendarElement.children).forEach( setupScroll );
+
+ //setupMonthDisplayAndButtons();
+ fetchRequestedMonth();
+});
\ No newline at end of file
diff --git a/script/dialog.js b/script/dialog.js
new file mode 100644
index 0000000..412c481
--- /dev/null
+++ b/script/dialog.js
@@ -0,0 +1,11 @@
+/**
+ * Closes a dialog box containing the element calling this function
+ *
+ * @param {Element} sender Sender element
+ */
+function closeDialog (sender) {
+ sender.
+ parentElement.
+ parentElement.
+ parentElement.remove();
+}
\ No newline at end of file
diff --git a/vendor/autoload.php b/vendor/autoload.php
new file mode 100644
index 0000000..1cec3db
--- /dev/null
+++ b/vendor/autoload.php
@@ -0,0 +1,25 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Autoload;
+
+/**
+ * ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
+ *
+ * $loader = new \Composer\Autoload\ClassLoader();
+ *
+ * // register classes with namespaces
+ * $loader->add('Symfony\Component', __DIR__.'/component');
+ * $loader->add('Symfony', __DIR__.'/framework');
+ *
+ * // activate the autoloader
+ * $loader->register();
+ *
+ * // to enable searching the include path (eg. for PEAR packages)
+ * $loader->setUseIncludePath(true);
+ *
+ * In this example, if you try to use a class in the Symfony\Component
+ * namespace or one of its children (Symfony\Component\Console for instance),
+ * the autoloader will first look for the class under the component/
+ * directory, and it will then fallback to the framework/ directory if not
+ * found before giving up.
+ *
+ * This class is loosely based on the Symfony UniversalClassLoader.
+ *
+ * @author Fabien Potencier
+ * @author Jordi Boggiano
+ * @see https://www.php-fig.org/psr/psr-0/
+ * @see https://www.php-fig.org/psr/psr-4/
+ */
+class ClassLoader
+{
+ /** @var \Closure(string):void */
+ private static $includeFile;
+
+ /** @var string|null */
+ private $vendorDir;
+
+ // PSR-4
+ /**
+ * @var array>
+ */
+ private $prefixLengthsPsr4 = array();
+ /**
+ * @var array>
+ */
+ private $prefixDirsPsr4 = array();
+ /**
+ * @var list
+ */
+ private $fallbackDirsPsr4 = array();
+
+ // PSR-0
+ /**
+ * List of PSR-0 prefixes
+ *
+ * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
+ *
+ * @var array>>
+ */
+ private $prefixesPsr0 = array();
+ /**
+ * @var list
+ */
+ private $fallbackDirsPsr0 = array();
+
+ /** @var bool */
+ private $useIncludePath = false;
+
+ /**
+ * @var array
+ */
+ private $classMap = array();
+
+ /** @var bool */
+ private $classMapAuthoritative = false;
+
+ /**
+ * @var array
+ */
+ private $missingClasses = array();
+
+ /** @var string|null */
+ private $apcuPrefix;
+
+ /**
+ * @var array
+ */
+ private static $registeredLoaders = array();
+
+ /**
+ * @param string|null $vendorDir
+ */
+ public function __construct($vendorDir = null)
+ {
+ $this->vendorDir = $vendorDir;
+ self::initializeIncludeClosure();
+ }
+
+ /**
+ * @return array>
+ */
+ public function getPrefixes()
+ {
+ if (!empty($this->prefixesPsr0)) {
+ return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
+ }
+
+ return array();
+ }
+
+ /**
+ * @return array>
+ */
+ public function getPrefixesPsr4()
+ {
+ return $this->prefixDirsPsr4;
+ }
+
+ /**
+ * @return list
+ */
+ public function getFallbackDirs()
+ {
+ return $this->fallbackDirsPsr0;
+ }
+
+ /**
+ * @return list
+ */
+ public function getFallbackDirsPsr4()
+ {
+ return $this->fallbackDirsPsr4;
+ }
+
+ /**
+ * @return array Array of classname => path
+ */
+ public function getClassMap()
+ {
+ return $this->classMap;
+ }
+
+ /**
+ * @param array $classMap Class to filename map
+ *
+ * @return void
+ */
+ public function addClassMap(array $classMap)
+ {
+ if ($this->classMap) {
+ $this->classMap = array_merge($this->classMap, $classMap);
+ } else {
+ $this->classMap = $classMap;
+ }
+ }
+
+ /**
+ * Registers a set of PSR-0 directories for a given prefix, either
+ * appending or prepending to the ones previously set for this prefix.
+ *
+ * @param string $prefix The prefix
+ * @param list|string $paths The PSR-0 root directories
+ * @param bool $prepend Whether to prepend the directories
+ *
+ * @return void
+ */
+ public function add($prefix, $paths, $prepend = false)
+ {
+ $paths = (array) $paths;
+ if (!$prefix) {
+ if ($prepend) {
+ $this->fallbackDirsPsr0 = array_merge(
+ $paths,
+ $this->fallbackDirsPsr0
+ );
+ } else {
+ $this->fallbackDirsPsr0 = array_merge(
+ $this->fallbackDirsPsr0,
+ $paths
+ );
+ }
+
+ return;
+ }
+
+ $first = $prefix[0];
+ if (!isset($this->prefixesPsr0[$first][$prefix])) {
+ $this->prefixesPsr0[$first][$prefix] = $paths;
+
+ return;
+ }
+ if ($prepend) {
+ $this->prefixesPsr0[$first][$prefix] = array_merge(
+ $paths,
+ $this->prefixesPsr0[$first][$prefix]
+ );
+ } else {
+ $this->prefixesPsr0[$first][$prefix] = array_merge(
+ $this->prefixesPsr0[$first][$prefix],
+ $paths
+ );
+ }
+ }
+
+ /**
+ * Registers a set of PSR-4 directories for a given namespace, either
+ * appending or prepending to the ones previously set for this namespace.
+ *
+ * @param string $prefix The prefix/namespace, with trailing '\\'
+ * @param list|string $paths The PSR-4 base directories
+ * @param bool $prepend Whether to prepend the directories
+ *
+ * @throws \InvalidArgumentException
+ *
+ * @return void
+ */
+ public function addPsr4($prefix, $paths, $prepend = false)
+ {
+ $paths = (array) $paths;
+ if (!$prefix) {
+ // Register directories for the root namespace.
+ if ($prepend) {
+ $this->fallbackDirsPsr4 = array_merge(
+ $paths,
+ $this->fallbackDirsPsr4
+ );
+ } else {
+ $this->fallbackDirsPsr4 = array_merge(
+ $this->fallbackDirsPsr4,
+ $paths
+ );
+ }
+ } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
+ // Register directories for a new namespace.
+ $length = strlen($prefix);
+ if ('\\' !== $prefix[$length - 1]) {
+ throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+ }
+ $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+ $this->prefixDirsPsr4[$prefix] = $paths;
+ } elseif ($prepend) {
+ // Prepend directories for an already registered namespace.
+ $this->prefixDirsPsr4[$prefix] = array_merge(
+ $paths,
+ $this->prefixDirsPsr4[$prefix]
+ );
+ } else {
+ // Append directories for an already registered namespace.
+ $this->prefixDirsPsr4[$prefix] = array_merge(
+ $this->prefixDirsPsr4[$prefix],
+ $paths
+ );
+ }
+ }
+
+ /**
+ * Registers a set of PSR-0 directories for a given prefix,
+ * replacing any others previously set for this prefix.
+ *
+ * @param string $prefix The prefix
+ * @param list|string $paths The PSR-0 base directories
+ *
+ * @return void
+ */
+ public function set($prefix, $paths)
+ {
+ if (!$prefix) {
+ $this->fallbackDirsPsr0 = (array) $paths;
+ } else {
+ $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
+ }
+ }
+
+ /**
+ * Registers a set of PSR-4 directories for a given namespace,
+ * replacing any others previously set for this namespace.
+ *
+ * @param string $prefix The prefix/namespace, with trailing '\\'
+ * @param list|string $paths The PSR-4 base directories
+ *
+ * @throws \InvalidArgumentException
+ *
+ * @return void
+ */
+ public function setPsr4($prefix, $paths)
+ {
+ if (!$prefix) {
+ $this->fallbackDirsPsr4 = (array) $paths;
+ } else {
+ $length = strlen($prefix);
+ if ('\\' !== $prefix[$length - 1]) {
+ throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+ }
+ $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+ $this->prefixDirsPsr4[$prefix] = (array) $paths;
+ }
+ }
+
+ /**
+ * Turns on searching the include path for class files.
+ *
+ * @param bool $useIncludePath
+ *
+ * @return void
+ */
+ public function setUseIncludePath($useIncludePath)
+ {
+ $this->useIncludePath = $useIncludePath;
+ }
+
+ /**
+ * Can be used to check if the autoloader uses the include path to check
+ * for classes.
+ *
+ * @return bool
+ */
+ public function getUseIncludePath()
+ {
+ return $this->useIncludePath;
+ }
+
+ /**
+ * Turns off searching the prefix and fallback directories for classes
+ * that have not been registered with the class map.
+ *
+ * @param bool $classMapAuthoritative
+ *
+ * @return void
+ */
+ public function setClassMapAuthoritative($classMapAuthoritative)
+ {
+ $this->classMapAuthoritative = $classMapAuthoritative;
+ }
+
+ /**
+ * Should class lookup fail if not found in the current class map?
+ *
+ * @return bool
+ */
+ public function isClassMapAuthoritative()
+ {
+ return $this->classMapAuthoritative;
+ }
+
+ /**
+ * APCu prefix to use to cache found/not-found classes, if the extension is enabled.
+ *
+ * @param string|null $apcuPrefix
+ *
+ * @return void
+ */
+ public function setApcuPrefix($apcuPrefix)
+ {
+ $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
+ }
+
+ /**
+ * The APCu prefix in use, or null if APCu caching is not enabled.
+ *
+ * @return string|null
+ */
+ public function getApcuPrefix()
+ {
+ return $this->apcuPrefix;
+ }
+
+ /**
+ * Registers this instance as an autoloader.
+ *
+ * @param bool $prepend Whether to prepend the autoloader or not
+ *
+ * @return void
+ */
+ public function register($prepend = false)
+ {
+ spl_autoload_register(array($this, 'loadClass'), true, $prepend);
+
+ if (null === $this->vendorDir) {
+ return;
+ }
+
+ if ($prepend) {
+ self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
+ } else {
+ unset(self::$registeredLoaders[$this->vendorDir]);
+ self::$registeredLoaders[$this->vendorDir] = $this;
+ }
+ }
+
+ /**
+ * Unregisters this instance as an autoloader.
+ *
+ * @return void
+ */
+ public function unregister()
+ {
+ spl_autoload_unregister(array($this, 'loadClass'));
+
+ if (null !== $this->vendorDir) {
+ unset(self::$registeredLoaders[$this->vendorDir]);
+ }
+ }
+
+ /**
+ * Loads the given class or interface.
+ *
+ * @param string $class The name of the class
+ * @return true|null True if loaded, null otherwise
+ */
+ public function loadClass($class)
+ {
+ if ($file = $this->findFile($class)) {
+ $includeFile = self::$includeFile;
+ $includeFile($file);
+
+ return true;
+ }
+
+ return null;
+ }
+
+ /**
+ * Finds the path to the file where the class is defined.
+ *
+ * @param string $class The name of the class
+ *
+ * @return string|false The path if found, false otherwise
+ */
+ public function findFile($class)
+ {
+ // class map lookup
+ if (isset($this->classMap[$class])) {
+ return $this->classMap[$class];
+ }
+ if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
+ return false;
+ }
+ if (null !== $this->apcuPrefix) {
+ $file = apcu_fetch($this->apcuPrefix.$class, $hit);
+ if ($hit) {
+ return $file;
+ }
+ }
+
+ $file = $this->findFileWithExtension($class, '.php');
+
+ // Search for Hack files if we are running on HHVM
+ if (false === $file && defined('HHVM_VERSION')) {
+ $file = $this->findFileWithExtension($class, '.hh');
+ }
+
+ if (null !== $this->apcuPrefix) {
+ apcu_add($this->apcuPrefix.$class, $file);
+ }
+
+ if (false === $file) {
+ // Remember that this class does not exist.
+ $this->missingClasses[$class] = true;
+ }
+
+ return $file;
+ }
+
+ /**
+ * Returns the currently registered loaders keyed by their corresponding vendor directories.
+ *
+ * @return array
+ */
+ public static function getRegisteredLoaders()
+ {
+ return self::$registeredLoaders;
+ }
+
+ /**
+ * @param string $class
+ * @param string $ext
+ * @return string|false
+ */
+ private function findFileWithExtension($class, $ext)
+ {
+ // PSR-4 lookup
+ $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
+
+ $first = $class[0];
+ if (isset($this->prefixLengthsPsr4[$first])) {
+ $subPath = $class;
+ while (false !== $lastPos = strrpos($subPath, '\\')) {
+ $subPath = substr($subPath, 0, $lastPos);
+ $search = $subPath . '\\';
+ if (isset($this->prefixDirsPsr4[$search])) {
+ $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
+ foreach ($this->prefixDirsPsr4[$search] as $dir) {
+ if (file_exists($file = $dir . $pathEnd)) {
+ return $file;
+ }
+ }
+ }
+ }
+ }
+
+ // PSR-4 fallback dirs
+ foreach ($this->fallbackDirsPsr4 as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
+ return $file;
+ }
+ }
+
+ // PSR-0 lookup
+ if (false !== $pos = strrpos($class, '\\')) {
+ // namespaced class name
+ $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
+ . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
+ } else {
+ // PEAR-like class name
+ $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
+ }
+
+ if (isset($this->prefixesPsr0[$first])) {
+ foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
+ if (0 === strpos($class, $prefix)) {
+ foreach ($dirs as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+ return $file;
+ }
+ }
+ }
+ }
+ }
+
+ // PSR-0 fallback dirs
+ foreach ($this->fallbackDirsPsr0 as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+ return $file;
+ }
+ }
+
+ // PSR-0 include paths.
+ if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
+ return $file;
+ }
+
+ return false;
+ }
+
+ /**
+ * @return void
+ */
+ private static function initializeIncludeClosure()
+ {
+ if (self::$includeFile !== null) {
+ return;
+ }
+
+ /**
+ * Scope isolated include.
+ *
+ * Prevents access to $this/self from included files.
+ *
+ * @param string $file
+ * @return void
+ */
+ self::$includeFile = \Closure::bind(static function($file) {
+ include $file;
+ }, null, null);
+ }
+}
diff --git a/vendor/composer/InstalledVersions.php b/vendor/composer/InstalledVersions.php
new file mode 100644
index 0000000..51e734a
--- /dev/null
+++ b/vendor/composer/InstalledVersions.php
@@ -0,0 +1,359 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer;
+
+use Composer\Autoload\ClassLoader;
+use Composer\Semver\VersionParser;
+
+/**
+ * This class is copied in every Composer installed project and available to all
+ *
+ * See also https://getcomposer.org/doc/07-runtime.md#installed-versions
+ *
+ * To require its presence, you can require `composer-runtime-api ^2.0`
+ *
+ * @final
+ */
+class InstalledVersions
+{
+ /**
+ * @var mixed[]|null
+ * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null
+ */
+ private static $installed;
+
+ /**
+ * @var bool|null
+ */
+ private static $canGetVendors;
+
+ /**
+ * @var array[]
+ * @psalm-var array}>
+ */
+ private static $installedByVendor = array();
+
+ /**
+ * Returns a list of all package names which are present, either by being installed, replaced or provided
+ *
+ * @return string[]
+ * @psalm-return list
+ */
+ public static function getInstalledPackages()
+ {
+ $packages = array();
+ foreach (self::getInstalled() as $installed) {
+ $packages[] = array_keys($installed['versions']);
+ }
+
+ if (1 === \count($packages)) {
+ return $packages[0];
+ }
+
+ return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
+ }
+
+ /**
+ * Returns a list of all package names with a specific type e.g. 'library'
+ *
+ * @param string $type
+ * @return string[]
+ * @psalm-return list
+ */
+ public static function getInstalledPackagesByType($type)
+ {
+ $packagesByType = array();
+
+ foreach (self::getInstalled() as $installed) {
+ foreach ($installed['versions'] as $name => $package) {
+ if (isset($package['type']) && $package['type'] === $type) {
+ $packagesByType[] = $name;
+ }
+ }
+ }
+
+ return $packagesByType;
+ }
+
+ /**
+ * Checks whether the given package is installed
+ *
+ * This also returns true if the package name is provided or replaced by another package
+ *
+ * @param string $packageName
+ * @param bool $includeDevRequirements
+ * @return bool
+ */
+ public static function isInstalled($packageName, $includeDevRequirements = true)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (isset($installed['versions'][$packageName])) {
+ return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks whether the given package satisfies a version constraint
+ *
+ * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
+ *
+ * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
+ *
+ * @param VersionParser $parser Install composer/semver to have access to this class and functionality
+ * @param string $packageName
+ * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
+ * @return bool
+ */
+ public static function satisfies(VersionParser $parser, $packageName, $constraint)
+ {
+ $constraint = $parser->parseConstraints((string) $constraint);
+ $provided = $parser->parseConstraints(self::getVersionRanges($packageName));
+
+ return $provided->matches($constraint);
+ }
+
+ /**
+ * Returns a version constraint representing all the range(s) which are installed for a given package
+ *
+ * It is easier to use this via isInstalled() with the $constraint argument if you need to check
+ * whether a given version of a package is installed, and not just whether it exists
+ *
+ * @param string $packageName
+ * @return string Version constraint usable with composer/semver
+ */
+ public static function getVersionRanges($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ $ranges = array();
+ if (isset($installed['versions'][$packageName]['pretty_version'])) {
+ $ranges[] = $installed['versions'][$packageName]['pretty_version'];
+ }
+ if (array_key_exists('aliases', $installed['versions'][$packageName])) {
+ $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
+ }
+ if (array_key_exists('replaced', $installed['versions'][$packageName])) {
+ $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
+ }
+ if (array_key_exists('provided', $installed['versions'][$packageName])) {
+ $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
+ }
+
+ return implode(' || ', $ranges);
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @param string $packageName
+ * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
+ */
+ public static function getVersion($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ if (!isset($installed['versions'][$packageName]['version'])) {
+ return null;
+ }
+
+ return $installed['versions'][$packageName]['version'];
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @param string $packageName
+ * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
+ */
+ public static function getPrettyVersion($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ if (!isset($installed['versions'][$packageName]['pretty_version'])) {
+ return null;
+ }
+
+ return $installed['versions'][$packageName]['pretty_version'];
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @param string $packageName
+ * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
+ */
+ public static function getReference($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ if (!isset($installed['versions'][$packageName]['reference'])) {
+ return null;
+ }
+
+ return $installed['versions'][$packageName]['reference'];
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @param string $packageName
+ * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
+ */
+ public static function getInstallPath($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @return array
+ * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
+ */
+ public static function getRootPackage()
+ {
+ $installed = self::getInstalled();
+
+ return $installed[0]['root'];
+ }
+
+ /**
+ * Returns the raw installed.php data for custom implementations
+ *
+ * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
+ * @return array[]
+ * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}
+ */
+ public static function getRawData()
+ {
+ @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
+
+ if (null === self::$installed) {
+ // only require the installed.php file if this file is loaded from its dumped location,
+ // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
+ if (substr(__DIR__, -8, 1) !== 'C') {
+ self::$installed = include __DIR__ . '/installed.php';
+ } else {
+ self::$installed = array();
+ }
+ }
+
+ return self::$installed;
+ }
+
+ /**
+ * Returns the raw data of all installed.php which are currently loaded for custom implementations
+ *
+ * @return array[]
+ * @psalm-return list}>
+ */
+ public static function getAllRawData()
+ {
+ return self::getInstalled();
+ }
+
+ /**
+ * Lets you reload the static array from another file
+ *
+ * This is only useful for complex integrations in which a project needs to use
+ * this class but then also needs to execute another project's autoloader in process,
+ * and wants to ensure both projects have access to their version of installed.php.
+ *
+ * A typical case would be PHPUnit, where it would need to make sure it reads all
+ * the data it needs from this class, then call reload() with
+ * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
+ * the project in which it runs can then also use this class safely, without
+ * interference between PHPUnit's dependencies and the project's dependencies.
+ *
+ * @param array[] $data A vendor/composer/installed.php data set
+ * @return void
+ *
+ * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data
+ */
+ public static function reload($data)
+ {
+ self::$installed = $data;
+ self::$installedByVendor = array();
+ }
+
+ /**
+ * @return array[]
+ * @psalm-return list}>
+ */
+ private static function getInstalled()
+ {
+ if (null === self::$canGetVendors) {
+ self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
+ }
+
+ $installed = array();
+
+ if (self::$canGetVendors) {
+ foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
+ if (isset(self::$installedByVendor[$vendorDir])) {
+ $installed[] = self::$installedByVendor[$vendorDir];
+ } elseif (is_file($vendorDir.'/composer/installed.php')) {
+ /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */
+ $required = require $vendorDir.'/composer/installed.php';
+ $installed[] = self::$installedByVendor[$vendorDir] = $required;
+ if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
+ self::$installed = $installed[count($installed) - 1];
+ }
+ }
+ }
+ }
+
+ if (null === self::$installed) {
+ // only require the installed.php file if this file is loaded from its dumped location,
+ // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
+ if (substr(__DIR__, -8, 1) !== 'C') {
+ /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */
+ $required = require __DIR__ . '/installed.php';
+ self::$installed = $required;
+ } else {
+ self::$installed = array();
+ }
+ }
+
+ if (self::$installed !== array()) {
+ $installed[] = self::$installed;
+ }
+
+ return $installed;
+ }
+}
diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE
new file mode 100644
index 0000000..62ecfd8
--- /dev/null
+++ b/vendor/composer/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) Nils Adermann, Jordi Boggiano
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php
new file mode 100644
index 0000000..0fb0a2c
--- /dev/null
+++ b/vendor/composer/autoload_classmap.php
@@ -0,0 +1,10 @@
+ $vendorDir . '/composer/InstalledVersions.php',
+);
diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php
new file mode 100644
index 0000000..0cb9ddf
--- /dev/null
+++ b/vendor/composer/autoload_namespaces.php
@@ -0,0 +1,10 @@
+ array($vendorDir . '/johngrogg/ics-parser/src'),
+);
diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php
new file mode 100644
index 0000000..3890ddc
--- /dev/null
+++ b/vendor/composer/autoload_psr4.php
@@ -0,0 +1,9 @@
+register(true);
+
+ return $loader;
+ }
+}
diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php
new file mode 100644
index 0000000..e261be7
--- /dev/null
+++ b/vendor/composer/autoload_static.php
@@ -0,0 +1,31 @@
+
+ array (
+ 'ICal' =>
+ array (
+ 0 => __DIR__ . '/..' . '/johngrogg/ics-parser/src',
+ ),
+ ),
+ );
+
+ public static $classMap = array (
+ 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
+ );
+
+ public static function getInitializer(ClassLoader $loader)
+ {
+ return \Closure::bind(function () use ($loader) {
+ $loader->prefixesPsr0 = ComposerStaticInitf1be773dbf3c5fddbb5187c8c187ceb4::$prefixesPsr0;
+ $loader->classMap = ComposerStaticInitf1be773dbf3c5fddbb5187c8c187ceb4::$classMap;
+
+ }, null, ClassLoader::class);
+ }
+}
diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json
new file mode 100644
index 0000000..9f210b2
--- /dev/null
+++ b/vendor/composer/installed.json
@@ -0,0 +1,73 @@
+{
+ "packages": [
+ {
+ "name": "johngrogg/ics-parser",
+ "version": "v3.3.1",
+ "version_normalized": "3.3.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/u01jmg3/ics-parser.git",
+ "reference": "eeb51c4c0c06e6df3266f85ea774ca314536aba4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/u01jmg3/ics-parser/zipball/eeb51c4c0c06e6df3266f85ea774ca314536aba4",
+ "reference": "eeb51c4c0c06e6df3266f85ea774ca314536aba4",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": ">=5.6.40"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5|^9|^10"
+ },
+ "time": "2023-10-10T09:58:49+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-0": {
+ "ICal": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jonathan Goode",
+ "role": "Developer/Owner"
+ },
+ {
+ "name": "John Grogg",
+ "email": "john.grogg@gmail.com",
+ "role": "Developer/Prior Owner"
+ }
+ ],
+ "description": "ICS Parser",
+ "homepage": "https://github.com/u01jmg3/ics-parser",
+ "keywords": [
+ "iCalendar",
+ "ical",
+ "ical-parser",
+ "ics",
+ "ics-parser",
+ "ifb"
+ ],
+ "support": {
+ "issues": "https://github.com/u01jmg3/ics-parser/issues",
+ "source": "https://github.com/u01jmg3/ics-parser/tree/v3.3.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sponsors/u01jmg3",
+ "type": "github"
+ }
+ ],
+ "install-path": "../johngrogg/ics-parser"
+ }
+ ],
+ "dev": true,
+ "dev-package-names": []
+}
diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php
new file mode 100644
index 0000000..cb8b9a6
--- /dev/null
+++ b/vendor/composer/installed.php
@@ -0,0 +1,32 @@
+ array(
+ 'name' => '__root__',
+ 'pretty_version' => '1.0.0+no-version-set',
+ 'version' => '1.0.0.0',
+ 'reference' => NULL,
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../../',
+ 'aliases' => array(),
+ 'dev' => true,
+ ),
+ 'versions' => array(
+ '__root__' => array(
+ 'pretty_version' => '1.0.0+no-version-set',
+ 'version' => '1.0.0.0',
+ 'reference' => NULL,
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../../',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ 'johngrogg/ics-parser' => array(
+ 'pretty_version' => 'v3.3.1',
+ 'version' => '3.3.1.0',
+ 'reference' => 'eeb51c4c0c06e6df3266f85ea774ca314536aba4',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../johngrogg/ics-parser',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ ),
+);
diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php
new file mode 100644
index 0000000..6439788
--- /dev/null
+++ b/vendor/composer/platform_check.php
@@ -0,0 +1,26 @@
+= 50640)) {
+ $issues[] = 'Your Composer dependencies require a PHP version ">= 5.6.40". You are running ' . PHP_VERSION . '.';
+}
+
+if ($issues) {
+ if (!headers_sent()) {
+ header('HTTP/1.1 500 Internal Server Error');
+ }
+ if (!ini_get('display_errors')) {
+ if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
+ fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
+ } elseif (!headers_sent()) {
+ echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
+ }
+ }
+ trigger_error(
+ 'Composer detected issues in your platform: ' . implode(' ', $issues),
+ E_USER_ERROR
+ );
+}
diff --git a/vendor/johngrogg/ics-parser/.editorconfig b/vendor/johngrogg/ics-parser/.editorconfig
new file mode 100644
index 0000000..9c1f764
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/.editorconfig
@@ -0,0 +1,11 @@
+# https://editorconfig.org/
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 4
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
diff --git a/vendor/johngrogg/ics-parser/.github/ISSUE_TEMPLATE/bug_report.yml b/vendor/johngrogg/ics-parser/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..5e3515f
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,53 @@
+name: Bug Report
+description: "Report something that's broken."
+labels: ["bug-normal"]
+body:
+ - type: markdown
+ attributes:
+ value: "Before raising an issue, please check the issue has not already been fixed in `dev-master`. You can also search through our [closed issues](../issues?q=is%3Aissue+is%3Aclosed+)."
+ - type: input
+ id: php-version
+ attributes:
+ label: PHP Version
+ description: Provide the PHP version that you are using.
+ placeholder: 8.1.4
+ validations:
+ required: true
+ - type: input
+ id: php-date-timezone
+ attributes:
+ label: PHP date.timezone
+ description: Provide the PHP date.timezone that you are using.
+ placeholder: "[Country] / [City]"
+ validations:
+ required: true
+ - type: input
+ id: ics-parser-version
+ attributes:
+ label: ICS Parser Version
+ description: Provide the `ics-parser` library version that you are using.
+ placeholder: 3.2.1
+ validations:
+ required: true
+ - type: input
+ id: operating-system
+ attributes:
+ label: Operating System
+ description: Provide the operating system that you are using.
+ placeholder: "Windows / Mac / Linux"
+ validations:
+ required: true
+ - type: textarea
+ id: description
+ attributes:
+ label: Description
+ description: Provide a detailed description of the issue that you are facing.
+ validations:
+ required: true
+ - type: textarea
+ id: steps-to-reproduce
+ attributes:
+ label: Steps to Reproduce
+ description: Provide detailed steps to reproduce your issue. It is **essential** that you supply a copy of the iCal file that is causing the parser to behave incorrectly to allow us to investigate. Prior to uploading the iCal file, please remove any personal or identifying information.
+ validations:
+ required: true
diff --git a/vendor/johngrogg/ics-parser/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/vendor/johngrogg/ics-parser/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
new file mode 100644
index 0000000..5a92ac1
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
@@ -0,0 +1,12 @@
+> :information_source:
+> - File a bug on our [issue tracker](https://github.com/u01jmg3/ics-parser/issues) (if there isn't one already).
+> - If your patch is going to be large it might be a good idea to get the discussion started early. We are happy to discuss it in a new issue beforehand.
+> - Please follow the coding standards already adhered to in the file you're editing before committing
+> - This includes the use of *4 spaces* over tabs for indentation
+> - Trim all trailing whitespace
+> - Using single quotes (`'`) where possible
+> - Use `PHP_EOL` where possible or default to `\n`
+> - Using the [1TBS](https://en.wikipedia.org/wiki/Indent_style#Variant:_1TBS_.28OTBS.29) indent style
+> - If a function is added or changed, please remember to update the [API documentation in the README](https://github.com/u01jmg3/ics-parser/blob/master/README.md#api)
+> - Please include unit tests to verify any new functionality
+> - Also check that existing tests still pass: `composer test`
diff --git a/vendor/johngrogg/ics-parser/.github/dependabot.yml b/vendor/johngrogg/ics-parser/.github/dependabot.yml
new file mode 100644
index 0000000..3f0c15e
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/.github/dependabot.yml
@@ -0,0 +1,13 @@
+version: 2
+updates:
+- package-ecosystem: composer
+ directory: "/"
+ schedule:
+ interval: daily
+ open-pull-requests-limit: 10
+ reviewers:
+ - u01jmg3
+- package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: weekly
diff --git a/vendor/johngrogg/ics-parser/.github/release_template.md b/vendor/johngrogg/ics-parser/.github/release_template.md
new file mode 100644
index 0000000..483f50b
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/.github/release_template.md
@@ -0,0 +1,9 @@
+# Release Checklist
+
+- [ ] Update docblock in `src/ICal/ICal.php`
+- [ ] Ensure the documentation is up to date
+- [ ] Push the code changes to GitHub (`git push`)
+- [ ] Tag the release (`git tag v1.2.3`)
+- [ ] Push the tag (`git push --tag`)
+- [ ] Check [Packagist](https://packagist.org/packages/johngrogg/ics-parser) is updated
+- [ ] Notify anyone who opened [an issue or PR](https://github.com/u01jmg3/ics-parser/issues?q=is%3Aopen) of the fix
diff --git a/vendor/johngrogg/ics-parser/.github/workflows/coding-standards.yml b/vendor/johngrogg/ics-parser/.github/workflows/coding-standards.yml
new file mode 100644
index 0000000..ad33fd3
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/.github/workflows/coding-standards.yml
@@ -0,0 +1,67 @@
+name: Coding Standards
+
+on:
+ push:
+ branches: [ "master" ]
+ pull_request:
+ branches: [ "master" ]
+
+permissions:
+ contents: read
+
+jobs:
+ Scan:
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix:
+ php: [5.6, 7.4, '8.0', 8.1, 8.2]
+
+ name: PHP ${{ matrix.php }}
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ tools: composer:2.2
+ coverage: none
+
+ - name: Install dependencies for PHP 5.6
+ run: composer update --quiet --no-scripts
+ if: matrix.php == 5.6
+
+ - name: Install dependencies for PHP 7.4+
+ run: composer install --quiet --no-scripts
+ if: matrix.php >= 7.4
+
+ - name: Execute tests
+ run: vendor/bin/phpunit --verbose
+
+ - name: Install additional dependencies
+ run: |
+ composer config allow-plugins.bamarni/composer-bin-plugin true --no-plugins
+ composer require bamarni/composer-bin-plugin rector/rector squizlabs/php_codesniffer --dev --quiet --no-scripts
+ composer bin easy-coding-standard config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
+ composer bin easy-coding-standard require symplify/easy-coding-standard slevomat/coding-standard --dev --quiet --no-scripts
+ if: matrix.php == 8.2
+
+ - name: Execute PHPCodeSniffer
+ run: vendor/bin/phpcs -n -s --standard=psr12 src
+ if: matrix.php == 8.2
+
+ - name: Execute Rector
+ run: vendor/bin/rector process src --dry-run
+ if: matrix.php == 8.2
+
+ - name: Execute ECS
+ run: vendor/bin/ecs check .
+ if: matrix.php == 8.2
+
+ - name: Execute PHPStan
+ run: vendor/bin/phpstan analyse src
+ if: matrix.php == 8.2
diff --git a/vendor/johngrogg/ics-parser/.gitignore b/vendor/johngrogg/ics-parser/.gitignore
new file mode 100644
index 0000000..80c161d
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/.gitignore
@@ -0,0 +1,59 @@
+###################
+# Compiled Source #
+###################
+*.com
+*.class
+*.dll
+*.exe
+*.o
+*.so
+
+############
+# Packages #
+############
+*.7z
+*.dmg
+*.gz
+*.iso
+*.jar
+*.rar
+*.tar
+*.zip
+
+######################
+# Logs and Databases #
+######################
+*.log
+*.sqlite
+
+######################
+# OS Generated Files #
+######################
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+.phpunit.result.cache
+ehthumbs.db
+Thumbs.db
+workbench
+
+####################
+# Package Managers #
+####################
+auth.json
+node_modules
+vendor
+
+##########
+# Custom #
+##########
+*.git
+*-report.*
+
+########
+# IDEs #
+########
+.idea
+*.iml
diff --git a/vendor/johngrogg/ics-parser/CONTRIBUTING.md b/vendor/johngrogg/ics-parser/CONTRIBUTING.md
new file mode 100644
index 0000000..9e7ed85
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/CONTRIBUTING.md
@@ -0,0 +1,18 @@
+## Contributing
+
+ICS Parser is an open source project. It is licensed under the [MIT license](https://opensource.org/licenses/MIT).
+We appreciate pull requests, here are our guidelines:
+
+1. Firstly, check if your issue is present within the latest version (`dev-master`) as the problem may already have been fixed.
+1. Log a bug in our [issue tracker](https://github.com/u01jmg3/ics-parser/issues) (if there isn't one already).
+ - If your patch is going to be large it might be a good idea to get the discussion started early.
+ - We are happy to discuss it in an issue beforehand.
+ - If you could provide an iCal snippet causing the parser to behave incorrectly it is extremely useful for debugging
+ - Please remove all irrelevant events
+1. Please follow the coding standard already present in the file you are editing _before_ committing
+ - Adhere to the [PSR-2](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md) coding standard
+ - Use *4 spaces* instead of tabs for indentation
+ - Trim all trailing whitespace and blank lines
+ - Use single quotes (`'`) where possible instead of double
+ - Use `PHP_EOL` where possible or default to `\n`
+ - Abide by the [1TBS](https://en.wikipedia.org/wiki/Indent_style#Variant:_1TBS_.28OTBS.29) indentation style
diff --git a/vendor/johngrogg/ics-parser/FUNDING.yml b/vendor/johngrogg/ics-parser/FUNDING.yml
new file mode 100644
index 0000000..e6e77b3
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/FUNDING.yml
@@ -0,0 +1 @@
+github: u01jmg3
diff --git a/vendor/johngrogg/ics-parser/LICENSE b/vendor/johngrogg/ics-parser/LICENSE
new file mode 100644
index 0000000..0708674
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/LICENSE
@@ -0,0 +1,15 @@
+The MIT License (MIT)
+Copyright (c) 2018
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy,
+modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
+Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/vendor/johngrogg/ics-parser/README.md b/vendor/johngrogg/ics-parser/README.md
new file mode 100644
index 0000000..7fc2c7f
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/README.md
@@ -0,0 +1,255 @@
+# PHP ICS Parser
+
+[](https://packagist.org/packages/johngrogg/ics-parser)
+[](https://packagist.org/packages/johngrogg/ics-parser)
+
+---
+
+## Installation
+
+### Requirements
+ - PHP 5 (≥ 5.6.40)
+ - [Valid ICS](https://icalendar.org/validator.html) (`.ics`, `.ical`, `.ifb`) file
+ - [IANA](https://www.iana.org/time-zones), [Unicode CLDR](http://cldr.unicode.org/translation/timezones) or [Windows](https://support.microsoft.com/en-ca/help/973627/microsoft-time-zone-index-values) Time Zones
+
+### Setup
+
+ - Install [Composer](https://getcomposer.org/)
+ - Add the following dependency to `composer.json`
+ - :warning: **Note with Composer the owner is `johngrogg` and not `u01jmg3`**
+ - To access the latest stable branch (`v3`) use the following
+ - To access new features you can require [`dev-master`](https://getcomposer.org/doc/articles/aliases.md#branch-alias)
+
+ ```yaml
+ {
+ "require": {
+ "johngrogg/ics-parser": "^3"
+ }
+ }
+ ```
+
+## Running tests
+
+```sh
+composer test
+```
+
+## How to use
+
+### How to instantiate the Parser
+
+ - Using the example script as a guide, [refer to this code](https://github.com/u01jmg3/ics-parser/blob/master/examples/index.php#L1-L22)
+
+#### What will the parser return?
+
+ - Each key/value pair from the iCal file will be parsed creating an associative array for both the calendar and every event it contains.
+ - Also injected will be content under `dtstart_tz` and `dtend_tz` for accessing start and end dates with time zone data applied.
+ - Where possible [`DateTime`](https://secure.php.net/manual/en/class.datetime.php) objects are used and returned.
+ - :information_source: **Note the parser is limited to [relative date formats](https://www.php.net/manual/en/datetime.formats.relative.php) which can inhibit how complex recurrence rule parts are processed (e.g. `BYDAY` combined with `BYSETPOS`)**
+
+ ```php
+ // Dump the whole calendar
+ var_dump($ical->cal);
+
+ // Dump every event
+ var_dump($ical->events());
+ ```
+
+ - Also included are special `{property}_array` arrays which further resolve the contents of a key/value pair.
+
+ ```php
+ // Dump a parsed event's start date
+ var_dump($event->dtstart_array);
+
+ // array (size=4)
+ // 0 =>
+ // array (size=1)
+ // 'TZID' => string 'America/Detroit' (length=15)
+ // 1 => string '20160409T090000' (length=15)
+ // 2 => int 1460192400
+ // 3 => string 'TZID=America/Detroit:20160409T090000' (length=36)
+ ```
+
+### Are you using Outlook?
+
+Outlook has a quirk where it requires the User Agent string to be set in your request headers.
+
+We have done this for you by injecting a default User Agent string, if one has not been specified.
+
+If you wish to provide your own User agent string you can do so by using the `httpUserAgent` argument when creating your ICal object.
+
+```php
+$ical = new ICal($url, array('httpUserAgent' => 'A Different User Agent'));
+```
+
+---
+
+## When Parsing an iCal Feed
+
+Parsing [iCal/iCalendar/ICS](https://en.wikipedia.org/wiki/ICalendar) resources can pose several challenges. One challenge is that
+the specification is a moving target; the original RFC has only been updated four times in ten years. The other challenge is that vendors
+were both liberal (read: creative) in interpreting the specification and productive implementing proprietary extensions.
+
+However, what impedes efficient parsing most directly are recurrence rules for events. This library parses the original
+calendar into an easy to work with memory model. This requires that each recurring event is expanded or exploded. Hence,
+a single event that occurs daily will generate a new event instance for each day as this parser processes the
+calendar ([`$defaultSpan`](#variables) limits this). To get an idea how this is done take a look at the
+[call graph](https://user-images.githubusercontent.com/624195/45904641-f3cd0a80-bded-11e8-925f-7bcee04b8575.png).
+
+As a consequence the _entire_ calendar is parsed line-by-line, and thus loaded into memory, first. As you can imagine
+large calendars tend to get huge when exploded i.e. with all their recurrence rules evaluated. This is exacerbated when
+old calendars do not remove past events as they get fatter and fatter every year.
+
+This limitation is particularly painful if you only need a window into the original calendar. It seems wasteful to parse
+the entire fully exploded calendar into memory if you later are going to call the
+[`eventsFromInterval()` or `eventsFromRange()`](#methods) on it.
+
+In late 2018 [#190](https://github.com/u01jmg3/ics-parser/pull/190) added the option to drop all events outside a given
+range very early in the parsing process at the cost of some precision (time zone calculations are not calculated at that point). This
+massively reduces the total time for parsing a calendar. The same goes for memory consumption. The precondition is that
+you know upfront that you don't care about events outside a given range.
+
+Let's say you are only interested in events from yesterday, today and tomorrow. To compensate for the fact that the
+tricky time zone transformations and calculations have not been executed yet by the time the parser has to decide whether
+to keep or drop an event you can set it to filter for **+-2d** instead of +-1d. Once it is done you would then call
+`eventsFromRange()` with +-1d to get precisely the events in the window you are interested in. That is what the variables
+[`$filterDaysBefore` and `$filterDaysAfter`](#variables) are for.
+
+In Q1 2019 [#213](https://github.com/u01jmg3/ics-parser/pull/213) further improved the performance by immediately
+dropping _non-recurring_ events once parsed if they are outside that fuzzy window. This greatly reduces the maximum
+memory consumption for large calendars. PHP by default does not allocate more than 128MB heap and would otherwise crash
+with `Fatal error: Allowed memory size of 134217728 bytes exhausted`. It goes without saying that recurring events first
+need to be evaluated before non-fitting events can be dropped.
+
+---
+
+## API
+
+### `ICal` API
+
+#### Variables
+
+| Name | Configurable | Default Value | Description |
+|--------------------------------|:------------------------:|-------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `$alarmCount` | :heavy_multiplication_x: | N/A | Tracks the number of alarms in the current iCal feed |
+| `$cal` | :heavy_multiplication_x: | N/A | The parsed calendar |
+| `$defaultSpan` | :ballot_box_with_check: | `2` | The value in years to use for indefinite, recurring events |
+| `$defaultTimeZone` | :ballot_box_with_check: | [System default](https://secure.php.net/manual/en/function.date-default-timezone-get.php) | Enables customisation of the default time zone |
+| `$defaultWeekStart` | :ballot_box_with_check: | `MO` | The two letter representation of the first day of the week |
+| `$disableCharacterReplacement` | :ballot_box_with_check: | `false` | Toggles whether to disable all character replacement. Will replace curly quotes and other special characters with their standard equivalents if `false`. Can be a costly operation! |
+| `$eventCount` | :heavy_multiplication_x: | N/A | Tracks the number of events in the current iCal feed |
+| `$filterDaysAfter` | :ballot_box_with_check: | `null` | When set the parser will ignore all events more than roughly this many days _after_ now. To be on the safe side it is advised that you make the filter window `+/- 1` day larger than necessary. For performance reasons this filter is applied before any date and time zone calculations are done. Hence, depending the time zone settings of the parser and the calendar the cut-off date is not "calibrated". You can then use `$ical->eventsFromRange()` to precisely shrink the window. |
+| `$filterDaysBefore` | :ballot_box_with_check: | `null` | When set the parser will ignore all events more than roughly this many days _before_ now. See `$filterDaysAfter` above for more details. |
+| `$freeBusyCount` | :heavy_multiplication_x: | N/A | Tracks the free/busy count in the current iCal feed |
+| `$httpBasicAuth` | :heavy_multiplication_x: | `array()` | Holds the username and password for HTTP basic authentication |
+| `$httpUserAgent` | :ballot_box_with_check: | `null` | Holds the custom User Agent string header |
+| `$httpAcceptLanguage` | :heavy_multiplication_x: | `null` | Holds the custom Accept Language request header, e.g. "en" or "de" |
+| `$httpProtocolVersion` | :heavy_multiplication_x: | `null` | Holds the custom HTTP Protocol version, e.g. "1.0" or "1.1" |
+| `$shouldFilterByWindow` | :heavy_multiplication_x: | `false` | `true` if either `$filterDaysBefore` or `$filterDaysAfter` are set |
+| `$skipRecurrence` | :ballot_box_with_check: | `false` | Toggles whether to skip the parsing of recurrence rules |
+| `$todoCount` | :heavy_multiplication_x: | N/A | Tracks the number of todos in the current iCal feed |
+| `$windowMaxTimestamp` | :heavy_multiplication_x: | `null` | If `$filterDaysBefore` or `$filterDaysAfter` are set then the events are filtered according to the window defined by this field and `$windowMinTimestamp` |
+| `$windowMinTimestamp` | :heavy_multiplication_x: | `null` | If `$filterDaysBefore` or `$filterDaysAfter` are set then the events are filtered according to the window defined by this field and `$windowMaxTimestamp` |
+
+#### Methods
+
+| Method | Parameter(s) | Visibility | Description |
+|-------------------------------------------------|-----------------------------------------------------------------------------------------------|-------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `__construct` | `$files = false`, `$options = array()` | `public` | Creates the ICal object |
+| `initFile` | `$file` | `protected` | Initialises lines from a file |
+| `initLines` | `$lines` | `protected` | Initialises the parser using an array containing each line of iCal content |
+| `initString` | `$string` | `protected` | Initialises lines from a string |
+| `initUrl` | `$url`, `$username = null`, `$password = null`, `$userAgent = null`, `$acceptLanguage = null` | `protected` | Initialises lines from a URL. Accepts a username/password combination for HTTP basic authentication, a custom User Agent string and the accepted client language |
+| `addCalendarComponentWithKeyAndValue` | `$component`, `$keyword`, `$value` | `protected` | Add one key and value pair to the `$this->cal` array |
+| `calendarDescription` | - | `public` | Returns the calendar description |
+| `calendarName` | - | `public` | Returns the calendar name |
+| `calendarTimeZone` | `$ignoreUtc` | `public` | Returns the calendar time zone |
+| `cleanCharacters` | `$data` | `protected` | Replaces curly quotes and other special characters with their standard equivalents |
+| `eventsFromInterval` | `$interval` | `public` | Returns a sorted array of events following a given string |
+| `eventsFromRange` | `$rangeStart = false`, `$rangeEnd = false` | `public` | Returns a sorted array of events in a given range, or an empty array if no events exist in the range |
+| `events` | - | `public` | Returns an array of Events |
+| `fileOrUrl` | `$filename` | `protected` | Reads an entire file or URL into an array |
+| `filterValuesUsingBySetPosRRule` | `$bysetpos`, `$valueslist` | `protected` | Filters a provided values-list by applying a BYSETPOS RRule |
+| `freeBusyEvents` | - | `public` | Returns an array of arrays with all free/busy events |
+| `getDaysOfMonthMatchingByDayRRule` | `$bydays`, `$initialDateTime` | `protected` | Find all days of a month that match the BYDAY stanza of an RRULE |
+| `getDaysOfMonthMatchingByMonthDayRRule` | `$byMonthDays`, `$initialDateTime` | `protected` | Find all days of a month that match the BYMONTHDAY stanza of an RRULE |
+| `getDaysOfYearMatchingByDayRRule` | `$byDays`, `$initialDateTime` | `protected` | Find all days of a year that match the BYDAY stanza of an RRULE |
+| `getDaysOfYearMatchingByMonthDayRRule` | `$byMonthDays`, `$initialDateTime` | `protected` | Find all days of a year that match the BYMONTHDAY stanza of an RRULE |
+| `getDaysOfYearMatchingByWeekNoRRule` | `$byWeekNums`, `$initialDateTime` | `protected` | Find all days of a year that match the BYWEEKNO stanza of an RRULE |
+| `getDaysOfYearMatchingByYearDayRRule` | `$byYearDays`, `$initialDateTime` | `protected` | Find all days of a year that match the BYYEARDAY stanza of an RRULE |
+| `hasEvents` | - | `public` | Returns a boolean value whether the current calendar has events or not |
+| `iCalDateToDateTime` | `$icalDate` | `public` | Returns a `DateTime` object from an iCal date time format |
+| `iCalDateToUnixTimestamp` | `$icalDate` | `public` | Returns a Unix timestamp from an iCal date time format |
+| `iCalDateWithTimeZone` | `$event`, `$key`, `$format = DATE_TIME_FORMAT` | `public` | Returns a date adapted to the calendar time zone depending on the event `TZID` |
+| `doesEventStartOutsideWindow` | `$event` | `protected` | Determines whether the event start date is outside `$windowMinTimestamp` / `$windowMaxTimestamp` |
+| `isFileOrUrl` | `$filename` | `protected` | Checks if a filename exists as a file or URL |
+| `isOutOfRange` | `$calendarDate`, `$minTimestamp`, `$maxTimestamp` | `protected` | Determines whether a valid iCalendar date is within a given range |
+| `isValidCldrTimeZoneId` | `$timeZone` | `protected` | Checks if a time zone is a valid CLDR time zone |
+| `isValidDate` | `$value` | `public` | Checks if a date string is a valid date |
+| `isValidIanaTimeZoneId` | `$timeZone` | `protected` | Checks if a time zone is a valid IANA time zone |
+| `isValidWindowsTimeZoneId` | `$timeZone` | `protected` | Checks if a time zone is a recognised Windows (non-CLDR) time zone |
+| `isValidTimeZoneId` | `$timeZone` | `protected` | Checks if a time zone is valid (IANA, CLDR, or Windows) |
+| `keyValueFromString` | `$text` | `public` | Gets the key value pair from an iCal string |
+| `parseLine` | `$line` | `protected` | Parses a line from an iCal file into an array of tokens |
+| `mb_chr` | `$code` | `protected` | Provides a polyfill for PHP 7.2's `mb_chr()`, which is a multibyte safe version of `chr()` |
+| `escapeParamText` | `$candidateText` | `protected` | Places double-quotes around texts that have characters not permitted in parameter-texts, but are permitted in quoted-texts. |
+| `parseDuration` | `$date`, `$duration`, `$format = 'U'` | `protected` | Parses a duration and applies it to a date |
+| `parseExdates` | `$event` | `public` | Parses a list of excluded dates to be applied to an Event |
+| `processDateConversions` | - | `protected` | Processes date conversions using the time zone |
+| `processEvents` | - | `protected` | Performs admin tasks on all events as read from the iCal file |
+| `processRecurrences` | - | `protected` | Processes recurrence rules |
+| `reduceEventsToMinMaxRange` | | `protected` | Reduces the number of events to the defined minimum and maximum range |
+| `removeLastEventIfOutsideWindowAndNonRecurring` | | `protected` | Removes the last event (i.e. most recently parsed) if its start date is outside the window spanned by `$windowMinTimestamp` / `$windowMaxTimestamp` |
+| `removeUnprintableChars` | `$data` | `protected` | Removes unprintable ASCII and UTF-8 characters |
+| `resolveIndicesOfRange` | `$indexes`, `$limit` | `protected` | Resolves values from indices of the range 1 -> `$limit` |
+| `sortEventsWithOrder` | `$events`, `$sortOrder = SORT_ASC` | `public` | Sorts events based on a given sort order |
+| `timeZoneStringToDateTimeZone` | `$timeZoneString` | `public` | Returns a `DateTimeZone` object based on a string containing a time zone name. |
+| `unfold` | `$lines` | `protected` | Unfolds an iCal file in preparation for parsing |
+
+#### Constants
+
+| Name | Description |
+|---------------------------|-----------------------------------------------|
+| `DATE_TIME_FORMAT_PRETTY` | Default pretty date time format to use |
+| `DATE_TIME_FORMAT` | Default date time format to use |
+| `ICAL_DATE_TIME_TEMPLATE` | String template to generate an iCal date time |
+| `ISO_8601_WEEK_START` | First day of the week, as defined by ISO-8601 |
+| `RECURRENCE_EVENT` | Used to isolate generated recurrence events |
+| `SECONDS_IN_A_WEEK` | The number of seconds in a week |
+| `TIME_FORMAT` | Default time format to use |
+| `TIME_ZONE_UTC` | UTC time zone string |
+| `UNIX_FORMAT` | Unix timestamp date format |
+| `UNIX_MIN_YEAR` | The year Unix time began |
+
+---
+
+### `Event` API (extends `ICal` API)
+
+#### Methods
+
+| Method | Parameter(s) | Visibility | Description |
+|---------------|---------------------------------------------|-------------|---------------------------------------------------------------------|
+| `__construct` | `$data = array()` | `public` | Creates the Event object |
+| `prepareData` | `$value` | `protected` | Prepares the data for output |
+| `printData` | `$html = HTML_TEMPLATE` | `public` | Returns Event data excluding anything blank within an HTML template |
+| `snakeCase` | `$input`, `$glue = '_'`, `$separator = '-'` | `protected` | Converts the given input to snake_case |
+
+#### Constants
+
+| Name | Description |
+|-----------------|-----------------------------------------------------|
+| `HTML_TEMPLATE` | String template to use when pretty printing content |
+
+---
+
+## Credits
+ - [Jonathan Goode](https://github.com/u01jmg3) (programming, bug fixing, codebase enhancement, coding standard adoption)
+ - [s0600204](https://github.com/s0600204) (major enhancements to RRULE support, many bug fixes and other contributions)
+
+---
+
+## Tools for Testing
+
+ - [iCal Validator](https://icalendar.org/validator.html)
+ - [Recurrence Rule Tester](https://jakubroztocil.github.io/rrule/)
+ - [Unix Timestamp Converter](https://www.unixtimestamp.com)
diff --git a/vendor/johngrogg/ics-parser/composer.json b/vendor/johngrogg/ics-parser/composer.json
new file mode 100644
index 0000000..a9d348e
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/composer.json
@@ -0,0 +1,49 @@
+{
+ "name": "johngrogg/ics-parser",
+ "description": "ICS Parser",
+ "homepage": "https://github.com/u01jmg3/ics-parser",
+ "keywords": [
+ "ical",
+ "ical-parser",
+ "icalendar",
+ "ics",
+ "ics-parser",
+ "ifb"
+ ],
+ "type": "library",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Jonathan Goode",
+ "role": "Developer/Owner"
+ },
+ {
+ "name": "John Grogg",
+ "email": "john.grogg@gmail.com",
+ "role": "Developer/Prior Owner"
+ }
+ ],
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/u01jmg3"
+ }
+ ],
+ "require": {
+ "php": ">=5.6.40",
+ "ext-mbstring": "*"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5|^9|^10"
+ },
+ "autoload": {
+ "psr-0": {
+ "ICal": "src/"
+ }
+ },
+ "scripts": {
+ "test": [
+ "phpunit --colors=always"
+ ]
+ }
+}
diff --git a/vendor/johngrogg/ics-parser/composer.lock b/vendor/johngrogg/ics-parser/composer.lock
new file mode 100644
index 0000000..0366984
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/composer.lock
@@ -0,0 +1,1754 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "6e5a6da49b03030889a52c62bd2e7eab",
+ "packages": [],
+ "packages-dev": [
+ {
+ "name": "doctrine/instantiator",
+ "version": "1.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/instantiator.git",
+ "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b",
+ "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^9 || ^11",
+ "ext-pdo": "*",
+ "ext-phar": "*",
+ "phpbench/phpbench": "^0.16 || ^1",
+ "phpstan/phpstan": "^1.4",
+ "phpstan/phpstan-phpunit": "^1",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
+ "vimeo/psalm": "^4.30 || ^5.4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com",
+ "homepage": "https://ocramius.github.io/"
+ }
+ ],
+ "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
+ "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
+ "keywords": [
+ "constructor",
+ "instantiate"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/instantiator/issues",
+ "source": "https://github.com/doctrine/instantiator/tree/1.5.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-12-30T00:15:36+00:00"
+ },
+ {
+ "name": "myclabs/deep-copy",
+ "version": "1.11.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/DeepCopy.git",
+ "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
+ "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "conflict": {
+ "doctrine/collections": "<1.6.8",
+ "doctrine/common": "<2.13.3 || >=3,<3.2.2"
+ },
+ "require-dev": {
+ "doctrine/collections": "^1.6.8",
+ "doctrine/common": "^2.13.3 || ^3.2.2",
+ "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/DeepCopy/deep_copy.php"
+ ],
+ "psr-4": {
+ "DeepCopy\\": "src/DeepCopy/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Create deep copies (clones) of your objects",
+ "keywords": [
+ "clone",
+ "copy",
+ "duplicate",
+ "object",
+ "object graph"
+ ],
+ "support": {
+ "issues": "https://github.com/myclabs/DeepCopy/issues",
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-03-08T13:26:56+00:00"
+ },
+ {
+ "name": "nikic/php-parser",
+ "version": "v4.17.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nikic/PHP-Parser.git",
+ "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d",
+ "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d",
+ "shasum": ""
+ },
+ "require": {
+ "ext-tokenizer": "*",
+ "php": ">=7.0"
+ },
+ "require-dev": {
+ "ircmaxell/php-yacc": "^0.0.7",
+ "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0"
+ },
+ "bin": [
+ "bin/php-parse"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.9-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PhpParser\\": "lib/PhpParser"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Nikita Popov"
+ }
+ ],
+ "description": "A PHP parser written in PHP",
+ "keywords": [
+ "parser",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/nikic/PHP-Parser/issues",
+ "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1"
+ },
+ "time": "2023-08-13T19:53:39+00:00"
+ },
+ {
+ "name": "phar-io/manifest",
+ "version": "2.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/manifest.git",
+ "reference": "97803eca37d319dfa7826cc2437fc020857acb53"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53",
+ "reference": "97803eca37d319dfa7826cc2437fc020857acb53",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-phar": "*",
+ "ext-xmlwriter": "*",
+ "phar-io/version": "^3.0.1",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
+ "support": {
+ "issues": "https://github.com/phar-io/manifest/issues",
+ "source": "https://github.com/phar-io/manifest/tree/2.0.3"
+ },
+ "time": "2021-07-20T11:28:43+00:00"
+ },
+ {
+ "name": "phar-io/version",
+ "version": "3.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/version.git",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Library for handling version information and constraints",
+ "support": {
+ "issues": "https://github.com/phar-io/version/issues",
+ "source": "https://github.com/phar-io/version/tree/3.2.1"
+ },
+ "time": "2022-02-21T01:04:05+00:00"
+ },
+ {
+ "name": "phpunit/php-code-coverage",
+ "version": "9.2.27",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+ "reference": "b0a88255cb70d52653d80c890bd7f38740ea50d1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b0a88255cb70d52653d80c890bd7f38740ea50d1",
+ "reference": "b0a88255cb70d52653d80c890bd7f38740ea50d1",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-xmlwriter": "*",
+ "nikic/php-parser": "^4.15",
+ "php": ">=7.3",
+ "phpunit/php-file-iterator": "^3.0.3",
+ "phpunit/php-text-template": "^2.0.2",
+ "sebastian/code-unit-reverse-lookup": "^2.0.2",
+ "sebastian/complexity": "^2.0",
+ "sebastian/environment": "^5.1.2",
+ "sebastian/lines-of-code": "^1.0.3",
+ "sebastian/version": "^3.0.1",
+ "theseer/tokenizer": "^1.2.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-pcov": "PHP extension that provides line coverage",
+ "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "9.2-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
+ "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
+ "keywords": [
+ "coverage",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
+ "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.27"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-07-26T13:44:30+00:00"
+ },
+ {
+ "name": "phpunit/php-file-iterator",
+ "version": "3.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
+ "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
+ "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "FilterIterator implementation that filters files based on a list of suffixes.",
+ "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
+ "keywords": [
+ "filesystem",
+ "iterator"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
+ "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2021-12-02T12:48:52+00:00"
+ },
+ {
+ "name": "phpunit/php-invoker",
+ "version": "3.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-invoker.git",
+ "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "ext-pcntl": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-pcntl": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Invoke callables with a timeout",
+ "homepage": "https://github.com/sebastianbergmann/php-invoker/",
+ "keywords": [
+ "process"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-invoker/issues",
+ "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:58:55+00:00"
+ },
+ {
+ "name": "phpunit/php-text-template",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-text-template.git",
+ "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Simple template engine.",
+ "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+ "keywords": [
+ "template"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
+ "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T05:33:50+00:00"
+ },
+ {
+ "name": "phpunit/php-timer",
+ "version": "5.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-timer.git",
+ "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Utility class for timing",
+ "homepage": "https://github.com/sebastianbergmann/php-timer/",
+ "keywords": [
+ "timer"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-timer/issues",
+ "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:16:10+00:00"
+ },
+ {
+ "name": "phpunit/phpunit",
+ "version": "9.6.11",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpunit.git",
+ "reference": "810500e92855eba8a7a5319ae913be2da6f957b0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/810500e92855eba8a7a5319ae913be2da6f957b0",
+ "reference": "810500e92855eba8a7a5319ae913be2da6f957b0",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/instantiator": "^1.3.1 || ^2",
+ "ext-dom": "*",
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-xml": "*",
+ "ext-xmlwriter": "*",
+ "myclabs/deep-copy": "^1.10.1",
+ "phar-io/manifest": "^2.0.3",
+ "phar-io/version": "^3.0.2",
+ "php": ">=7.3",
+ "phpunit/php-code-coverage": "^9.2.13",
+ "phpunit/php-file-iterator": "^3.0.5",
+ "phpunit/php-invoker": "^3.1.1",
+ "phpunit/php-text-template": "^2.0.3",
+ "phpunit/php-timer": "^5.0.2",
+ "sebastian/cli-parser": "^1.0.1",
+ "sebastian/code-unit": "^1.0.6",
+ "sebastian/comparator": "^4.0.8",
+ "sebastian/diff": "^4.0.3",
+ "sebastian/environment": "^5.1.3",
+ "sebastian/exporter": "^4.0.5",
+ "sebastian/global-state": "^5.0.1",
+ "sebastian/object-enumerator": "^4.0.3",
+ "sebastian/resource-operations": "^3.0.3",
+ "sebastian/type": "^3.2",
+ "sebastian/version": "^3.0.2"
+ },
+ "suggest": {
+ "ext-soap": "To be able to generate mocks based on WSDL files",
+ "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
+ },
+ "bin": [
+ "phpunit"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "9.6-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/Framework/Assert/Functions.php"
+ ],
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "The PHP Unit Testing framework.",
+ "homepage": "https://phpunit.de/",
+ "keywords": [
+ "phpunit",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/phpunit/issues",
+ "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.11"
+ },
+ "funding": [
+ {
+ "url": "https://phpunit.de/sponsors.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-08-19T07:10:56+00:00"
+ },
+ {
+ "name": "sebastian/cli-parser",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/cli-parser.git",
+ "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2",
+ "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for parsing CLI options",
+ "homepage": "https://github.com/sebastianbergmann/cli-parser",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/cli-parser/issues",
+ "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T06:08:49+00:00"
+ },
+ {
+ "name": "sebastian/code-unit",
+ "version": "1.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit.git",
+ "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120",
+ "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/code-unit",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:08:54+00:00"
+ },
+ {
+ "name": "sebastian/code-unit-reverse-lookup",
+ "version": "2.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
+ "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+ "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Looks up which function or method a line of code belongs to",
+ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:30:19+00:00"
+ },
+ {
+ "name": "sebastian/comparator",
+ "version": "4.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/comparator.git",
+ "reference": "fa0f136dd2334583309d32b62544682ee972b51a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a",
+ "reference": "fa0f136dd2334583309d32b62544682ee972b51a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/diff": "^4.0",
+ "sebastian/exporter": "^4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@2bepublished.at"
+ }
+ ],
+ "description": "Provides the functionality to compare PHP values for equality",
+ "homepage": "https://github.com/sebastianbergmann/comparator",
+ "keywords": [
+ "comparator",
+ "compare",
+ "equality"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/comparator/issues",
+ "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2022-09-14T12:41:17+00:00"
+ },
+ {
+ "name": "sebastian/complexity",
+ "version": "2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/complexity.git",
+ "reference": "739b35e53379900cc9ac327b2147867b8b6efd88"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88",
+ "reference": "739b35e53379900cc9ac327b2147867b8b6efd88",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.7",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for calculating the complexity of PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/complexity",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/complexity/issues",
+ "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T15:52:27+00:00"
+ },
+ {
+ "name": "sebastian/diff",
+ "version": "4.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/diff.git",
+ "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131",
+ "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3",
+ "symfony/process": "^4.2 || ^5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Kore Nordmann",
+ "email": "mail@kore-nordmann.de"
+ }
+ ],
+ "description": "Diff implementation",
+ "homepage": "https://github.com/sebastianbergmann/diff",
+ "keywords": [
+ "diff",
+ "udiff",
+ "unidiff",
+ "unified diff"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/diff/issues",
+ "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-05-07T05:35:17+00:00"
+ },
+ {
+ "name": "sebastian/environment",
+ "version": "5.1.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/environment.git",
+ "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
+ "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-posix": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides functionality to handle HHVM/PHP environments",
+ "homepage": "http://www.github.com/sebastianbergmann/environment",
+ "keywords": [
+ "Xdebug",
+ "environment",
+ "hhvm"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/environment/issues",
+ "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:03:51+00:00"
+ },
+ {
+ "name": "sebastian/exporter",
+ "version": "4.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/exporter.git",
+ "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d",
+ "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "ext-mbstring": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Provides the functionality to export PHP variables for visualization",
+ "homepage": "https://www.github.com/sebastianbergmann/exporter",
+ "keywords": [
+ "export",
+ "exporter"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/exporter/issues",
+ "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2022-09-14T06:03:37+00:00"
+ },
+ {
+ "name": "sebastian/global-state",
+ "version": "5.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/global-state.git",
+ "reference": "bde739e7565280bda77be70044ac1047bc007e34"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34",
+ "reference": "bde739e7565280bda77be70044ac1047bc007e34",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/object-reflector": "^2.0",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "ext-dom": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-uopz": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Snapshotting of global state",
+ "homepage": "http://www.github.com/sebastianbergmann/global-state",
+ "keywords": [
+ "global state"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/global-state/issues",
+ "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-08-02T09:26:13+00:00"
+ },
+ {
+ "name": "sebastian/lines-of-code",
+ "version": "1.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/lines-of-code.git",
+ "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc",
+ "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.6",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for counting the lines of code in PHP source code",
+ "homepage": "https://github.com/sebastianbergmann/lines-of-code",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/lines-of-code/issues",
+ "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-11-28T06:42:11+00:00"
+ },
+ {
+ "name": "sebastian/object-enumerator",
+ "version": "4.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-enumerator.git",
+ "reference": "5c9eeac41b290a3712d88851518825ad78f45c71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71",
+ "reference": "5c9eeac41b290a3712d88851518825ad78f45c71",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/object-reflector": "^2.0",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Traverses array structures and object graphs to enumerate all referenced objects",
+ "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
+ "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:12:34+00:00"
+ },
+ {
+ "name": "sebastian/object-reflector",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-reflector.git",
+ "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+ "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Allows reflection of object attributes, including inherited and non-public ones",
+ "homepage": "https://github.com/sebastianbergmann/object-reflector/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
+ "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:14:26+00:00"
+ },
+ {
+ "name": "sebastian/recursion-context",
+ "version": "4.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/recursion-context.git",
+ "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1",
+ "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ }
+ ],
+ "description": "Provides functionality to recursively process PHP variables",
+ "homepage": "https://github.com/sebastianbergmann/recursion-context",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:07:39+00:00"
+ },
+ {
+ "name": "sebastian/resource-operations",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/resource-operations.git",
+ "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8",
+ "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides a list of PHP built-in functions that operate on resources",
+ "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/resource-operations/issues",
+ "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T06:45:17+00:00"
+ },
+ {
+ "name": "sebastian/type",
+ "version": "3.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/type.git",
+ "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
+ "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.2-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the types of the PHP type system",
+ "homepage": "https://github.com/sebastianbergmann/type",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/type/issues",
+ "source": "https://github.com/sebastianbergmann/type/tree/3.2.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:13:03+00:00"
+ },
+ {
+ "name": "sebastian/version",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/version.git",
+ "reference": "c6c1022351a901512170118436c764e473f6de8c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c",
+ "reference": "c6c1022351a901512170118436c764e473f6de8c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that helps with managing the version number of Git-hosted PHP projects",
+ "homepage": "https://github.com/sebastianbergmann/version",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/version/issues",
+ "source": "https://github.com/sebastianbergmann/version/tree/3.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T06:39:44+00:00"
+ },
+ {
+ "name": "theseer/tokenizer",
+ "version": "1.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/theseer/tokenizer.git",
+ "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e",
+ "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+ "support": {
+ "issues": "https://github.com/theseer/tokenizer/issues",
+ "source": "https://github.com/theseer/tokenizer/tree/1.2.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2021-07-28T10:34:58+00:00"
+ }
+ ],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": {
+ "php": ">=5.6.40",
+ "ext-mbstring": "*"
+ },
+ "platform-dev": [],
+ "plugin-api-version": "2.2.0"
+}
diff --git a/vendor/johngrogg/ics-parser/ecs.php b/vendor/johngrogg/ics-parser/ecs.php
new file mode 100644
index 0000000..025c69c
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/ecs.php
@@ -0,0 +1,205 @@
+disableParallel();
+
+ // https://github.com/easy-coding-standard/easy-coding-standard/blob/main/config/set/psr12.php
+ $ecsConfig->import(SetList::PSR_12);
+
+ $ecsConfig->lineEnding("\n");
+
+ $ecsConfig->skip(array(
+ // Fixers
+ 'PhpCsFixer\Fixer\Whitespace\StatementIndentationFixer' => array('examples/index.php'),
+ 'PhpCsFixer\Fixer\Basic\BracesFixer' => null,
+ 'PhpCsFixer\Fixer\Operator\BinaryOperatorSpacesFixer' => null,
+ 'PhpCsFixer\Fixer\Operator\NotOperatorWithSuccessorSpaceFixer' => null,
+ 'PhpCsFixer\Fixer\Phpdoc\PhpdocScalarFixer' => null,
+ 'PhpCsFixer\Fixer\Phpdoc\PhpdocSummaryFixer' => null,
+ 'PhpCsFixer\Fixer\Phpdoc\PhpdocVarWithoutNameFixer' => null,
+ 'PhpCsFixer\Fixer\ReturnNotation\SimplifiedNullReturnFixer' => null,
+ // Requires PHP 7.1 and above
+ 'PhpCsFixer\Fixer\ClassNotation\VisibilityRequiredFixer' => null,
+ ));
+
+ $ecsConfig->ruleWithConfiguration(SpaceAfterNotSniff::class, array('spacing' => 0));
+
+ $ecsConfig->ruleWithConfiguration(ArraySyntaxFixer::class, array('syntax' => 'long'));
+
+ $ecsConfig->ruleWithConfiguration(
+ YodaStyleFixer::class,
+ array(
+ 'equal' => false,
+ 'identical' => false,
+ 'less_and_greater' => false,
+ )
+ );
+
+ $ecsConfig->ruleWithConfiguration(ListSyntaxFixer::class, array('syntax' => 'long')); // PHP 5.6
+
+ $ecsConfig->ruleWithConfiguration(
+ BlankLineBeforeStatementFixer::class,
+ array(
+ 'statements' => array(
+ 'continue',
+ 'declare',
+ 'return',
+ 'throw',
+ 'try',
+ ),
+ )
+ );
+
+ $ecsConfig->rules(
+ array(
+ AlphabeticallySortedUsesSniff::class,
+ UnusedVariableSniff::class,
+ SelfMemberReferenceSniff::class,
+ BlankLinesBeforeNamespaceFixer::class,
+ CastSpacesFixer::class,
+ ClassDefinitionFixer::class,
+ CompactNullableTypehintFixer::class,
+ ConstantCaseFixer::class,
+ ElseifFixer::class,
+ EncodingFixer::class,
+ FullOpeningTagFixer::class,
+ FunctionDeclarationFixer::class,
+ HeredocToNowdocFixer::class,
+ IncludeFixer::class,
+ LambdaNotUsedImportFixer::class,
+ LineEndingFixer::class,
+ LowercaseKeywordsFixer::class,
+ LowercaseStaticReferenceFixer::class,
+ MagicConstantCasingFixer::class,
+ MagicMethodCasingFixer::class,
+ MethodArgumentSpaceFixer::class,
+ MultilineWhitespaceBeforeSemicolonsFixer::class,
+ NativeFunctionCasingFixer::class,
+ NativeFunctionTypeDeclarationCasingFixer::class,
+ NoAliasFunctionsFixer::class,
+ NoClosingTagFixer::class,
+ NoEmptyPhpdocFixer::class,
+ NoEmptyStatementFixer::class,
+ NoExtraBlankLinesFixer::class,
+ NoLeadingNamespaceWhitespaceFixer::class,
+ NoMixedEchoPrintFixer::class,
+ NoMultilineWhitespaceAroundDoubleArrowFixer::class,
+ NoShortBoolCastFixer::class,
+ NoSpacesAfterFunctionNameFixer::class,
+ NoSpacesInsideParenthesisFixer::class,
+ NoTrailingCommaInSinglelineFixer::class,
+ NoTrailingWhitespaceInCommentFixer::class,
+ NoUnneededControlParenthesesFixer::class,
+ NoUnneededCurlyBracesFixer::class,
+ NoUnreachableDefaultArgumentValueFixer::class,
+ NoUnusedImportsFixer::class,
+ NoUselessReturnFixer::class,
+ NoWhitespaceInBlankLineFixer::class,
+ NormalizeIndexBraceFixer::class,
+ ObjectOperatorWithoutWhitespaceFixer::class,
+ PhpdocIndentFixer::class,
+ PhpdocInlineTagNormalizerFixer::class,
+ PhpdocNoAccessFixer::class,
+ PhpdocNoPackageFixer::class,
+ PhpdocNoUselessInheritdocFixer::class,
+ PhpdocParamOrderFixer::class,
+ PhpdocSingleLineVarSpacingFixer::class,
+ PhpdocToCommentFixer::class,
+ PhpdocTrimFixer::class,
+ PhpdocTypesFixer::class,
+ SingleBlankLineAtEofFixer::class,
+ SingleClassElementPerStatementFixer::class,
+ SingleImportPerStatementFixer::class,
+ SingleLineAfterImportsFixer::class,
+ SingleLineCommentStyleFixer::class,
+ SingleQuoteFixer::class,
+ SpaceAfterSemicolonFixer::class,
+ StandardizeNotEqualsFixer::class,
+ SwitchCaseSemicolonToColonFixer::class,
+ SwitchCaseSpaceFixer::class,
+ TrailingCommaInMultilineFixer::class,
+ TrimArraySpacesFixer::class,
+ TypeDeclarationSpacesFixer::class,
+ )
+ );
+};
diff --git a/vendor/johngrogg/ics-parser/examples/ICal.ics b/vendor/johngrogg/ics-parser/examples/ICal.ics
new file mode 100644
index 0000000..9c1346c
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/examples/ICal.ics
@@ -0,0 +1,338 @@
+BEGIN:VCALENDAR
+PRODID:-//Google Inc//Google Calendar 70.9054//EN
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:PUBLISH
+X-WR-CALNAME:Testkalender
+X-WR-TIMEZONE:UTC
+X-WR-CALDESC:Nur zum testen vom Google Kalender
+BEGIN:VFREEBUSY
+UID:f06ff6b3564b2f696bf42d393f8dea59
+ORGANIZER:MAILTO:jane_smith@host1.com
+DTSTAMP:20170316T204607Z
+DTSTART:20170213T204607Z
+DTEND:20180517T204607Z
+URL:https://www.host.com/calendar/busytime/jsmith.ifb
+FREEBUSY;FBTYPE=BUSY:20170623T070000Z/20170223T110000Z
+FREEBUSY;FBTYPE=BUSY:20170624T131500Z/20170316T151500Z
+FREEBUSY;FBTYPE=BUSY:20170715T131500Z/20170416T150000Z
+FREEBUSY;FBTYPE=BUSY:20170716T131500Z/20170516T100500Z
+END:VFREEBUSY
+BEGIN:VEVENT
+DTSTART:20171032T000000
+DTEND:20171101T2300
+DESCRIPTION:Invalid date - parser will skip the event
+SUMMARY:Invalid date - parser will skip the event
+DTSTAMP:20170406T063924
+LOCATION:
+UID:f81b0b41a2e138ae0903daee0a966e1e
+SEQUENCE:0
+END:VEVENT
+BEGIN:VEVENT
+DTSTART;VALUE=DATE;TZID=America/Los_Angeles:19410512
+DTEND;VALUE=DATE;TZID=America/Los_Angeles:19410512
+DTSTAMP;TZID=America/Los_Angeles:19410512T195741Z
+UID:dh3fki5du0opa7cs5n5s87ca02@google.com
+CREATED:20380101T141901Z
+DESCRIPTION;LANGUAGE=en-gb:
+LAST-MODIFIED:20380101T141901Z
+LOCATION:
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY;LANGUAGE=en-gb:Before 1970-Test: Konrad Zuse invents the Z3, the "first
+ digital Computer"
+TRANSP:TRANSPARENT
+END:VEVENT
+BEGIN:VEVENT
+DTSTART;VALUE=DATE:20380201
+DTEND;VALUE=DATE:20380202
+DTSTAMP;TZID="GMT Standard Time":20380101T195741Z
+UID:dh3fki5du0opa7cs5n5s87ca01@google.com
+CREATED:20380101T141901Z
+DESCRIPTION;LANGUAGE=en-gb:
+LAST-MODIFIED:20380101T141901Z
+LOCATION:
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY;LANGUAGE=en-gb:Year 2038 problem test
+TRANSP:TRANSPARENT
+END:VEVENT
+BEGIN:VEVENT
+DTSTART:20160105T090000Z
+DTEND:20160107T173000Z
+DTSTAMP;TZID="Greenwich Mean Time:Dublin; Edinburgh; Lisbon; London":20110121T195741Z
+UID:15lc1nvupht8dtfiptenljoiv4@google.com
+CREATED:20110121T195616Z
+DESCRIPTION;LANGUAGE=en-gb:This is a short description\nwith a new line. Some "special" 's
+ igns' may be interesting\, too.
+ And a non-breaking space.
+LAST-MODIFIED:20150409T150000Z
+LOCATION:Kansas
+SEQUENCE:2
+STATUS:CONFIRMED
+SUMMARY;LANGUAGE=en-gb:My Holidays
+TRANSP:TRANSPARENT
+ORGANIZER;CN="My Name":mailto:my.name@mydomain.com
+END:VEVENT
+BEGIN:VEVENT
+ATTENDEE;CN="Page, Larry (l.page@google.com)";ROLE=REQ-PARTICIPANT;RSVP=FALSE:mailto:l.page@google.com
+ATTENDEE;CN="Brin, Sergey (s.brin@google.com)";ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:s.brin@google.com
+DTSTART;VALUE=DATE:20160112
+DTEND;VALUE=DATE:20160116
+DTSTAMP;TZID="GMT Standard Time":20110121T195741Z
+UID:1koigufm110c5hnq6ln57murd4@google.com
+CREATED:20110119T142901Z
+DESCRIPTION;LANGUAGE=en-gb:Project xyz Review Meeting Minutes\n
+ Agenda\n1. Review of project version 1.0 requirements.\n2.
+ Definition
+ of project processes.\n3. Review of project schedule.\n
+ Participants: John Smith, Jane Doe, Jim Dandy\n-It was
+ decided that the requirements need to be signed off by
+ product marketing.\n-Project processes were accepted.\n
+ -Project schedule needs to account for scheduled holidays
+ and employee vacation time. Check with HR for specific
+ dates.\n-New schedule will be distributed by Friday.\n-
+ Next weeks meeting is cancelled. No meeting until 3/23.
+LAST-MODIFIED:20150409T150000Z
+LOCATION:
+SEQUENCE:2
+STATUS:CONFIRMED
+SUMMARY;LANGUAGE=en-gb:Test 2
+TRANSP:TRANSPARENT
+END:VEVENT
+BEGIN:VEVENT
+DTSTART;VALUE=DATE:20160119
+DTEND;VALUE=DATE:20160120
+DTSTAMP;TZID="GMT Standard Time":20110121T195741Z
+UID:rq8jng4jgq0m1lvpj8486fttu0@google.com
+CREATED:20110119T141904Z
+DESCRIPTION;LANGUAGE=en-gb:
+LAST-MODIFIED:20150409T150000Z
+LOCATION:
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY;LANGUAGE=en-gb:DST Change
+TRANSP:TRANSPARENT
+END:VEVENT
+BEGIN:VEVENT
+DTSTART;VALUE=DATE:20160119
+DTEND;VALUE=DATE:20160120
+DTSTAMP;TZID="GMT Standard Time":20110121T195741Z
+UID:dh3fki5du0opa7cs5n5s87ca00@google.com
+CREATED:20110119T141901Z
+DESCRIPTION;LANGUAGE=en-gb:
+LAST-MODIFIED:20150409T150000Z
+LOCATION:
+RRULE:FREQ=WEEKLY;COUNT=5;INTERVAL=2;BYDAY=TU
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY;LANGUAGE=en-gb:Test 1
+TRANSP:TRANSPARENT
+END:VEVENT
+BEGIN:VEVENT
+SUMMARY:Duration Test
+DTSTART:20160425T150000Z
+DTSTAMP:20160424T150000Z
+DURATION:PT1H15M5S
+RRULE:FREQ=DAILY;COUNT=2
+UID:calendar-62-e7c39bf02382917349672271dd781c89
+END:VEVENT
+BEGIN:VEVENT
+SUMMARY:BYMONTHDAY Test
+DTSTART:20160922T130000Z
+DTEND:20160922T150000Z
+DTSTAMP:20160921T130000Z
+RRULE:FREQ=MONTHLY;UNTIL=20170923T000000Z;INTERVAL=1;BYMONTHDAY=23
+UID:33844fe8df15fbfc13c97fc41c0c4b00392c6870@google.com
+END:VEVENT
+BEGIN:VEVENT
+DTSTART;TZID=Europe/Paris:20160921T080000
+DTEND;TZID=Europe/Paris:20160921T090000
+RRULE:FREQ=WEEKLY;BYDAY=WE
+DTSTAMP:20161117T165045Z
+UID:884bc8350185031337d9ec49d2e7e101dd5ae5fb@google.com
+CREATED:20160920T133918Z
+DESCRIPTION:
+LAST-MODIFIED:20160920T133923Z
+LOCATION:
+SEQUENCE:1
+STATUS:CONFIRMED
+SUMMARY:Paris Timezone Test
+TRANSP:OPAQUE
+END:VEVENT
+BEGIN:VEVENT
+DTSTART:20160215T080000Z
+DTEND:20160515T090000Z
+DTSTAMP:20161121T113027Z
+CREATED:20161121T113027Z
+UID:65323c541a30dd1f180e2bbfa2724995
+DESCRIPTION:
+LAST-MODIFIED:20161121T113027Z
+LOCATION:
+SEQUENCE:1
+STATUS:CONFIRMED
+SUMMARY:Long event covering the range from example with special chars:
+ ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÕÖÒÓÔØÙÚÛÜÝÞß
+ àáâãäåæçèéêėëìíîïðñòóôõöøùúûüūýþÿž
+ ‘ ’ ‚ ‛ “ ” „ ‟ – — …
+TRANSP:OPAQUE
+END:VEVENT
+BEGIN:VEVENT
+CLASS:PUBLIC
+CREATED:20160706T161104Z
+DTEND;TZID="(UTC-05:00) Eastern Time (US & Canada)":20160409T110000
+DTSTAMP:20160706T150005Z
+DTSTART;TZID="(UTC-05:00) Eastern Time (US & Canada)":20160409T090000
+EXDATE;TZID="(UTC-05:00) Eastern Time (US & Canada)":
+ 20160528T090000,
+ 20160625T090000
+LAST-MODIFIED:20160707T182011Z
+EXDATE;TZID="(UTC-05:00) Eastern Time (US & Canada)":20160709T090000
+EXDATE;TZID="(UTC-05:00) Eastern Time (US & Canada)":20160723T090000
+LOCATION:Sanctuary
+PRIORITY:5
+RRULE:FREQ=WEEKLY;COUNT=15;BYDAY=SA
+SEQUENCE:0
+SUMMARY:Microsoft Unicode CLDR EXDATE Test
+TRANSP:OPAQUE
+UID:040000008200E00074C5B7101A82E0080000000020F6512D0B48CF0100000000000000001000000058BFB8CBB85D504CB99FBA637BCFD6BF
+X-MICROSOFT-CDO-BUSYSTATUS:BUSY
+X-MICROSOFT-CDO-IMPORTANCE:1
+X-MICROSOFT-DISALLOW-COUNTER:FALSE
+END:VEVENT
+BEGIN:VEVENT
+DTSTART;VALUE=DATE:20170118
+DTEND;VALUE=DATE:20170118
+DTSTAMP;TZID="GMT Standard Time":20170121T195741Z
+RRULE:FREQ=MONTHLY;BYSETPOS=3;BYDAY=WE;COUNT=5
+UID:4dnsuc3nknin15kv25cn7ridss@google.com
+CREATED:20170119T142059Z
+DESCRIPTION;LANGUAGE=en-gb:BYDAY Test 1
+LAST-MODIFIED:20170409T150000Z
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY;LANGUAGE=en-gb:BYDAY Test 1
+TRANSP:TRANSPARENT
+END:VEVENT
+BEGIN:VEVENT
+DTSTART;VALUE=DATE:20190101
+DTEND;VALUE=DATE:20190101
+DTSTAMP;TZID="GMT Standard Time":20190101T195741Z
+RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=1
+UID:4dnsuc3nknin15kv25cn7ridssy@google.com
+CREATED:20190101T142059Z
+DESCRIPTION;LANGUAGE=en-gb:BYSETPOS First weekday of every month
+LAST-MODIFIED:20190101T150000Z
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY;LANGUAGE=en-gb:BYSETPOS First weekday of every month
+TRANSP:TRANSPARENT
+END:VEVENT
+BEGIN:VEVENT
+DTSTART;VALUE=DATE:20190131
+DTEND;VALUE=DATE:20190131
+DTSTAMP;TZID="GMT Standard Time":20190121T195741Z
+RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=SU,MO,TU,WE,TH,FR,SA;BYSETPOS=-1
+UID:4dnsuc3nknin15kv25cn7ridssx@google.com
+CREATED:20190119T142059Z
+DESCRIPTION;LANGUAGE=en-gb:BYSETPOS Last day of every month
+LAST-MODIFIED:20190409T150000Z
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY;LANGUAGE=en-gb:BYSETPOS Last day of every month
+TRANSP:TRANSPARENT
+END:VEVENT
+BEGIN:VEVENT
+DTSTART;VALUE=DATE:20170301
+DTEND;VALUE=DATE:20170301
+DTSTAMP;TZID="GMT Standard Time":20170121T195741Z
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=WE
+UID:h6f7sdjbpt47v3dkral8lnsgcc@google.com
+CREATED:20170119T142040Z
+DESCRIPTION;LANGUAGE=en-gb:BYDAY Test 2
+LAST-MODIFIED:20170409T150000Z
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY;LANGUAGE=en-gb:BYDAY Test 2
+TRANSP:TRANSPARENT
+END:VEVENT
+BEGIN:VEVENT
+DTSTART;VALUE=DATE:20170111
+DTEND;VALUE=DATE:20170111
+DTSTAMP;TZID="GMT Standard Time":20170121T195741Z
+RRULE:FREQ=YEARLY;INTERVAL=2;COUNT=5;BYMONTH=1,2,3
+UID:f50e8b89a4a3b0070e0b687d03@google.com
+CREATED:20170119T142040Z
+DESCRIPTION;LANGUAGE=en-gb:BYMONTH Multiple Test 1
+LAST-MODIFIED:20170409T150000Z
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY;LANGUAGE=en-gb:BYMONTH Multiple Test 1
+TRANSP:TRANSPARENT
+END:VEVENT
+BEGIN:VEVENT
+DTSTART;VALUE=DATE:20170405
+DTEND;VALUE=DATE:20170405
+DTSTAMP;TZID="GMT Standard Time":20170121T195741Z
+RRULE:FREQ=YEARLY;BYMONTH=4,5,6;BYDAY=WE;COUNT=5
+UID:675f06aa795665ae50904ebf0e@google.com
+CREATED:20170119T142040Z
+DESCRIPTION;LANGUAGE=en-gb:BYMONTH Multiple Test 2
+LAST-MODIFIED:20170409T150000Z
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY;LANGUAGE=en-gb:BYMONTH Multiple Test 2
+TRANSP:TRANSPARENT
+END:VEVENT
+BEGIN:VEVENT
+BEGIN:VALARM
+TRIGGER;VALUE=DURATION:-PT30M
+ACTION:DISPLAY
+DESCRIPTION:Buzz buzz
+END:VALARM
+DTSTART;VALUE=DATE;TZID=Germany/Berlin:20170123
+DTEND;VALUE=DATE;TZID=Germany/Berlin:20170123
+DTSTAMP;TZID="GMT Standard Time":20170121T195741Z
+RRULE:FREQ=MONTHLY;BYDAY=-2MO;COUNT=5
+EXDATE;VALUE=DATE:20171020
+UID:d287b7ec808fcf084983f10837@google.com
+CREATED:20170119T142040Z
+DESCRIPTION;LANGUAGE=en-gb:Negative BYDAY
+LAST-MODIFIED:20170409T150000Z
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY;LANGUAGE=en-gb:Negative BYDAY
+TRANSP:TRANSPARENT
+END:VEVENT
+BEGIN:VEVENT
+DTSTART;TZID=Australia/Sydney:20170813T190000
+DTEND;TZID=Australia/Sydney:20170813T213000
+RRULE:FREQ=MONTHLY;INTERVAL=2;BYDAY=2SU;COUNT=2
+DTSTAMP:20170809T114431Z
+UID:testuid@google.com
+CREATED:20170802T135539Z
+DESCRIPTION:
+LAST-MODIFIED:20170802T135935Z
+LOCATION:
+SEQUENCE:1
+STATUS:CONFIRMED
+SUMMARY:Parent Recurrence Event
+TRANSP:OPAQUE
+END:VEVENT
+BEGIN:VEVENT
+DTSTART;TZID=Australia/Sydney:20170813T190000
+DTEND;TZID=Australia/Sydney:20170813T213000
+DTSTAMP:20170809T114431Z
+UID:testuid@google.com
+RECURRENCE-ID;TZID=Australia/Sydney:20170813T190000
+CREATED:20170802T135539Z
+DESCRIPTION:
+LAST-MODIFIED:20170809T105604Z
+LOCATION:Melbourne VIC\, Australia
+SEQUENCE:1
+STATUS:CONFIRMED
+SUMMARY:Override Parent Recurrence Event
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
diff --git a/vendor/johngrogg/ics-parser/examples/index.php b/vendor/johngrogg/ics-parser/examples/index.php
new file mode 100644
index 0000000..d7e118a
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/examples/index.php
@@ -0,0 +1,175 @@
+ 2, // Default value
+ 'defaultTimeZone' => 'UTC',
+ 'defaultWeekStart' => 'MO', // Default value
+ 'disableCharacterReplacement' => false, // Default value
+ 'filterDaysAfter' => null, // Default value
+ 'filterDaysBefore' => null, // Default value
+ 'httpUserAgent' => null, // Default value
+ 'skipRecurrence' => false, // Default value
+ ));
+ // $ical->initFile('ICal.ics');
+ // $ical->initUrl('https://raw.githubusercontent.com/u01jmg3/ics-parser/master/examples/ICal.ics', $username = null, $password = null, $userAgent = null);
+} catch (\Exception $e) {
+ die($e);
+}
+?>
+
+
+
+
+
+
+ PHP ICS Parser example
+
+
+
+
+
PHP ICS Parser example
+
+
+ The number of events
+ eventCount ?>
+
+
+ The number of free/busy time slots
+ freeBusyCount ?>
+
+
+ The number of todos
+ todoCount ?>
+
+
+ The number of alarms
+ alarmCount ?>
+
+
+
+ true,
+ 'range' => true,
+ 'all' => true,
+ );
+ ?>
+
+ eventsFromInterval('1 week');
+
+ if ($events) {
+ echo 'Events in the next 7 days: ';
+ }
+
+ $count = 1;
+ ?>
+
+
+
+
+
+
iCalDateToDateTime($event->dtstart_array[3]);
+ echo $event->summary . ' (' . $dtstart->format('d-m-Y H:i') . ')';
+ ?>
+ printData() ?>
+
+
+
+ 1 && $count % 3 === 0) {
+ echo '
';
+ }
+
+ $count++;
+ ?>
+
+
+
+
+ eventsFromRange('2017-03-01 12:00:00', '2017-04-31 17:00:00');
+
+ if ($events) {
+ echo 'Events March through April: ';
+ }
+
+ $count = 1;
+ ?>
+
+
+
+
+
+
iCalDateToDateTime($event->dtstart_array[3]);
+ echo $event->summary . ' (' . $dtstart->format('d-m-Y H:i') . ')';
+ ?>
+ printData() ?>
+
+
+
+ 1 && $count % 3 === 0) {
+ echo '
';
+ }
+
+ $count++;
+ ?>
+
+
+
+
+ sortEventsWithOrder($ical->events());
+
+ if ($events) {
+ echo 'All Events: ';
+ }
+ ?>
+
+
+
+
+
+
iCalDateToDateTime($event->dtstart_array[3]);
+ echo $event->summary . ' (' . $dtstart->format('d-m-Y H:i') . ')';
+ ?>
+ printData() ?>
+
+
+
+ 1 && $count % 3 === 0) {
+ echo '
';
+ }
+
+ $count++;
+ ?>
+
+
+
+
+
+
diff --git a/vendor/johngrogg/ics-parser/phpstan.neon.dist b/vendor/johngrogg/ics-parser/phpstan.neon.dist
new file mode 100644
index 0000000..a202a97
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/phpstan.neon.dist
@@ -0,0 +1,7 @@
+parameters:
+ paths:
+ - src
+
+ level: 6
+
+ checkMissingIterableValueType: false
diff --git a/vendor/johngrogg/ics-parser/phpunit.xml b/vendor/johngrogg/ics-parser/phpunit.xml
new file mode 100644
index 0000000..f40c752
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/phpunit.xml
@@ -0,0 +1,7 @@
+
+
+
+ tests
+
+
+
diff --git a/vendor/johngrogg/ics-parser/rector.php b/vendor/johngrogg/ics-parser/rector.php
new file mode 100644
index 0000000..c7282bd
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/rector.php
@@ -0,0 +1,86 @@
+disableParallel();
+
+ $rectorConfig->importShortClasses(false);
+
+ $rectorConfig->phpVersion(PhpVersion::PHP_56);
+
+ $rectorConfig->skip(
+ array(
+ Rector\CodeQuality\Rector\Class_\CompleteDynamicPropertiesRector::class,
+ Rector\CodeQuality\Rector\Concat\JoinStringConcatRector::class,
+ Rector\CodeQuality\Rector\FuncCall\ChangeArrayPushToArrayAssignRector::class,
+ Rector\CodeQuality\Rector\FuncCall\CompactToVariablesRector::class,
+ Rector\CodeQuality\Rector\FuncCall\InlineIsAInstanceOfRector::class,
+ Rector\CodeQuality\Rector\FuncCall\IntvalToTypeCastRector::class,
+ Rector\CodeQuality\Rector\FunctionLike\SimplifyUselessVariableRector::class,
+ Rector\CodeQuality\Rector\Identical\BooleanNotIdenticalToNotIdenticalRector::class,
+ Rector\CodeQuality\Rector\Identical\SimplifyBoolIdenticalTrueRector::class,
+ Rector\CodeQuality\Rector\If_\CombineIfRector::class,
+ Rector\CodeQuality\Rector\If_\ExplicitBoolCompareRector::class,
+ Rector\CodeQuality\Rector\If_\SimplifyIfElseToTernaryRector::class,
+ Rector\CodeQuality\Rector\If_\SimplifyIfReturnBoolRector::class,
+ Rector\CodeQuality\Rector\Isset_\IssetOnPropertyObjectToPropertyExistsRector::class,
+ Rector\CodingStyle\Rector\ClassMethod\UnSpreadOperatorRector::class,
+ Rector\CodingStyle\Rector\Closure\StaticClosureRector::class,
+ Rector\CodingStyle\Rector\Encapsed\EncapsedStringsToSprintfRector::class,
+ Rector\CodingStyle\Rector\PostInc\PostIncDecToPreIncDecRector::class,
+ Rector\CodingStyle\Rector\Stmt\NewlineAfterStatementRector::class,
+ Rector\CodingStyle\Rector\String_\SymplifyQuoteEscapeRector::class,
+ Rector\DeadCode\Rector\Assign\RemoveUnusedVariableAssignRector::class,
+ Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPromotedPropertyRector::class,
+ Rector\DeadCode\Rector\ClassMethod\RemoveUselessParamTagRector::class,
+ Rector\DeadCode\Rector\ClassMethod\RemoveUselessReturnTagRector::class,
+ Rector\DeadCode\Rector\StaticCall\RemoveParentCallWithoutParentRector::class,
+ Rector\Php70\Rector\MethodCall\ThisCallOnStaticMethodToStaticCallRector::class,
+ Rector\Php70\Rector\StaticCall\StaticCallOnNonStaticToInstanceCallRector::class,
+ Rector\Php71\Rector\FuncCall\CountOnNullRector::class,
+ Rector\Php73\Rector\FuncCall\JsonThrowOnErrorRector::class,
+ Rector\Php74\Rector\Closure\ClosureToArrowFunctionRector::class,
+ Rector\Transform\Rector\String_\StringToClassConstantRector::class,
+ // PHP 5.6 incompatible
+ Rector\CodeQuality\Rector\Ternary\ArrayKeyExistsTernaryThenValueToCoalescingRector::class, // PHP 7
+ Rector\Php70\Rector\If_\IfToSpaceshipRector::class,
+ Rector\Php70\Rector\Ternary\TernaryToSpaceshipRector::class,
+ Rector\Php71\Rector\BooleanOr\IsIterableRector::class,
+ Rector\Php71\Rector\List_\ListToArrayDestructRector::class,
+ Rector\Php71\Rector\TryCatch\MultiExceptionCatchRector::class,
+ Rector\Php73\Rector\FuncCall\ArrayKeyFirstLastRector::class,
+ Rector\Php73\Rector\BooleanOr\IsCountableRector::class,
+ Rector\Php74\Rector\Assign\NullCoalescingOperatorRector::class,
+ Rector\Php74\Rector\FuncCall\ArraySpreadInsteadOfArrayMergeRector::class,
+ Rector\Php74\Rector\LNumber\AddLiteralSeparatorToNumberRector::class,
+ Rector\Php74\Rector\StaticCall\ExportToReflectionFunctionRector::class,
+ Rector\CodingStyle\Rector\ClassConst\RemoveFinalFromConstRector::class, // PHP 8
+ )
+ );
+
+ $rectorConfig->sets(
+ array(
+ SetList::CODE_QUALITY,
+ SetList::CODING_STYLE,
+ SetList::DEAD_CODE,
+ SetList::PHP_70,
+ SetList::PHP_71,
+ SetList::PHP_72,
+ SetList::PHP_73,
+ SetList::PHP_74,
+ SetList::PHP_80,
+ SetList::PHP_81,
+ SetList::PHP_82,
+ )
+ );
+
+ $rectorConfig->rule(TernaryToElvisRector::class);
+};
diff --git a/vendor/johngrogg/ics-parser/src/ICal/Event.php b/vendor/johngrogg/ics-parser/src/ICal/Event.php
new file mode 100644
index 0000000..4657d27
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/src/ICal/Event.php
@@ -0,0 +1,259 @@
+%s: %s
';
+
+ /**
+ * https://www.kanzaki.com/docs/ical/summary.html
+ *
+ * @var string
+ */
+ public $summary;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/dtstart.html
+ *
+ * @var string
+ */
+ public $dtstart;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/dtend.html
+ *
+ * @var string
+ */
+ public $dtend;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/duration.html
+ *
+ * @var string
+ */
+ public $duration;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/dtstamp.html
+ *
+ * @var string
+ */
+ public $dtstamp;
+
+ /**
+ * When the event starts, represented as a timezone-adjusted string
+ *
+ * @var string
+ */
+ public $dtstart_tz;
+
+ /**
+ * When the event ends, represented as a timezone-adjusted string
+ *
+ * @var string
+ */
+ public $dtend_tz;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/uid.html
+ *
+ * @var string
+ */
+ public $uid;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/created.html
+ *
+ * @var string
+ */
+ public $created;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/lastModified.html
+ *
+ * @var string
+ */
+ public $last_modified;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/description.html
+ *
+ * @var string
+ */
+ public $description;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/location.html
+ *
+ * @var string
+ */
+ public $location;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/sequence.html
+ *
+ * @var string
+ */
+ public $sequence;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/status.html
+ *
+ * @var string
+ */
+ public $status;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/transp.html
+ *
+ * @var string
+ */
+ public $transp;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/organizer.html
+ *
+ * @var string
+ */
+ public $organizer;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/attendee.html
+ *
+ * @var string
+ */
+ public $attendee;
+
+ /**
+ * Manage additional properties
+ *
+ * @var array
+ */
+ private $additionalProperties = array();
+
+ /**
+ * Creates the Event object
+ *
+ * @param array $data
+ * @return void
+ */
+ public function __construct(array $data = array())
+ {
+ foreach ($data as $key => $value) {
+ $variable = self::snakeCase($key);
+ if (property_exists($this, $variable)) {
+ $this->{$variable} = $this->prepareData($value);
+ } else {
+ $this->additionalProperties[$variable] = $this->prepareData($value);
+ }
+ }
+ }
+
+ /**
+ * Magic getter method
+ *
+ * @param string $additionalPropertyName
+ * @return mixed
+ */
+ public function __get($additionalPropertyName)
+ {
+ if (array_key_exists($additionalPropertyName, $this->additionalProperties)) {
+ return $this->additionalProperties[$additionalPropertyName];
+ }
+
+ return null;
+ }
+
+ /**
+ * Magic isset method
+ *
+ * @param string $name
+ * @return boolean
+ */
+ public function __isset($name)
+ {
+ return is_null($this->$name) === false;
+ }
+
+ /**
+ * Prepares the data for output
+ *
+ * @param mixed $value
+ * @return mixed
+ */
+ protected function prepareData($value)
+ {
+ if (is_string($value)) {
+ return stripslashes(trim(str_replace('\n', "\n", $value)));
+ }
+
+ if (is_array($value)) {
+ return array_map(function ($value) {
+ return $this->prepareData($value);
+ }, $value);
+ }
+
+ return $value;
+ }
+
+ /**
+ * Returns Event data excluding anything blank
+ * within an HTML template
+ *
+ * @param string $html HTML template to use
+ * @return string
+ */
+ public function printData($html = self::HTML_TEMPLATE)
+ {
+ $data = array(
+ 'SUMMARY' => $this->summary,
+ 'DTSTART' => $this->dtstart,
+ 'DTEND' => $this->dtend,
+ 'DTSTART_TZ' => $this->dtstart_tz,
+ 'DTEND_TZ' => $this->dtend_tz,
+ 'DURATION' => $this->duration,
+ 'DTSTAMP' => $this->dtstamp,
+ 'UID' => $this->uid,
+ 'CREATED' => $this->created,
+ 'LAST-MODIFIED' => $this->last_modified,
+ 'DESCRIPTION' => $this->description,
+ 'LOCATION' => $this->location,
+ 'SEQUENCE' => $this->sequence,
+ 'STATUS' => $this->status,
+ 'TRANSP' => $this->transp,
+ 'ORGANISER' => $this->organizer,
+ 'ATTENDEE(S)' => $this->attendee,
+ );
+
+ // Remove any blank values
+ $data = array_filter($data);
+
+ $output = '';
+
+ foreach ($data as $key => $value) {
+ $output .= sprintf($html, $key, $value);
+ }
+
+ return $output;
+ }
+
+ /**
+ * Converts the given input to snake_case
+ *
+ * @param string $input
+ * @param string $glue
+ * @param string $separator
+ * @return string
+ */
+ protected static function snakeCase($input, $glue = '_', $separator = '-')
+ {
+ $input = preg_split('/(?<=[a-z])(?=[A-Z])/x', $input);
+ $input = implode($glue, $input);
+ $input = str_replace($separator, $glue, $input);
+
+ return strtolower($input);
+ }
+}
diff --git a/vendor/johngrogg/ics-parser/src/ICal/ICal.php b/vendor/johngrogg/ics-parser/src/ICal/ICal.php
new file mode 100644
index 0000000..9252975
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/src/ICal/ICal.php
@@ -0,0 +1,2658 @@
+
+ * @license https://opensource.org/licenses/mit-license.php MIT License
+ * @version 3.2.0
+ */
+
+namespace ICal;
+
+class ICal
+{
+ // phpcs:disable Generic.Arrays.DisallowLongArraySyntax
+
+ const DATE_TIME_FORMAT = 'Ymd\THis';
+ const DATE_TIME_FORMAT_PRETTY = 'F Y H:i:s';
+ const ICAL_DATE_TIME_TEMPLATE = 'TZID=%s:';
+ const ISO_8601_WEEK_START = 'MO';
+ const RECURRENCE_EVENT = 'Generated recurrence event';
+ const SECONDS_IN_A_WEEK = 604800;
+ const TIME_FORMAT = 'His';
+ const TIME_ZONE_UTC = 'UTC';
+ const UNIX_FORMAT = 'U';
+ const UNIX_MIN_YEAR = 1970;
+
+ /**
+ * Tracks the number of alarms in the current iCal feed
+ *
+ * @var integer
+ */
+ public $alarmCount = 0;
+
+ /**
+ * Tracks the number of events in the current iCal feed
+ *
+ * @var integer
+ */
+ public $eventCount = 0;
+
+ /**
+ * Tracks the free/busy count in the current iCal feed
+ *
+ * @var integer
+ */
+ public $freeBusyCount = 0;
+
+ /**
+ * Tracks the number of todos in the current iCal feed
+ *
+ * @var integer
+ */
+ public $todoCount = 0;
+
+ /**
+ * The value in years to use for indefinite, recurring events
+ *
+ * @var integer
+ */
+ public $defaultSpan = 2;
+
+ /**
+ * Enables customisation of the default time zone
+ *
+ * @var string|null
+ */
+ public $defaultTimeZone;
+
+ /**
+ * The two letter representation of the first day of the week
+ *
+ * @var string
+ */
+ public $defaultWeekStart = self::ISO_8601_WEEK_START;
+
+ /**
+ * Toggles whether to skip the parsing of recurrence rules
+ *
+ * @var boolean
+ */
+ public $skipRecurrence = false;
+
+ /**
+ * Toggles whether to disable all character replacement.
+ *
+ * @var boolean
+ */
+ public $disableCharacterReplacement = false;
+
+ /**
+ * With this being non-null the parser will ignore all events more than roughly this many days after now.
+ *
+ * @var integer|null
+ */
+ public $filterDaysBefore;
+
+ /**
+ * With this being non-null the parser will ignore all events more than roughly this many days before now.
+ *
+ * @var integer|null
+ */
+ public $filterDaysAfter;
+
+ /**
+ * The parsed calendar
+ *
+ * @var array
+ */
+ public $cal = array();
+
+ /**
+ * Tracks the VFREEBUSY component
+ *
+ * @var integer
+ */
+ protected $freeBusyIndex = 0;
+
+ /**
+ * Variable to track the previous keyword
+ *
+ * @var string
+ */
+ protected $lastKeyword;
+
+ /**
+ * Cache valid IANA time zone IDs to avoid unnecessary lookups
+ *
+ * @var array
+ */
+ protected $validIanaTimeZones = array();
+
+ /**
+ * Event recurrence instances that have been altered
+ *
+ * @var array
+ */
+ protected $alteredRecurrenceInstances = array();
+
+ /**
+ * An associative array containing weekday conversion data
+ *
+ * The order of the days in the array follow the ISO-8601 specification of a week.
+ *
+ * @var array
+ */
+ protected $weekdays = array(
+ 'MO' => 'monday',
+ 'TU' => 'tuesday',
+ 'WE' => 'wednesday',
+ 'TH' => 'thursday',
+ 'FR' => 'friday',
+ 'SA' => 'saturday',
+ 'SU' => 'sunday',
+ );
+
+ /**
+ * An associative array containing frequency conversion terms
+ *
+ * @var array
+ */
+ protected $frequencyConversion = array(
+ 'DAILY' => 'day',
+ 'WEEKLY' => 'week',
+ 'MONTHLY' => 'month',
+ 'YEARLY' => 'year',
+ );
+
+ /**
+ * Holds the username and password for HTTP basic authentication
+ *
+ * @var array
+ */
+ protected $httpBasicAuth = array();
+
+ /**
+ * Holds the custom User Agent string header
+ *
+ * @var string
+ */
+ protected $httpUserAgent;
+
+ /**
+ * Holds the custom Accept Language string header
+ *
+ * @var string
+ */
+ protected $httpAcceptLanguage;
+
+ /**
+ * Holds the custom HTTP Protocol version
+ *
+ * @var string
+ */
+ protected $httpProtocolVersion;
+
+ /**
+ * Define which variables can be configured
+ *
+ * @var array
+ */
+ private static $configurableOptions = array(
+ 'defaultSpan',
+ 'defaultTimeZone',
+ 'defaultWeekStart',
+ 'disableCharacterReplacement',
+ 'filterDaysAfter',
+ 'filterDaysBefore',
+ 'httpUserAgent',
+ 'skipRecurrence',
+ );
+
+ /**
+ * CLDR time zones mapped to IANA time zones.
+ *
+ * @var array
+ */
+ private static $cldrTimeZonesMap = array(
+ '(UTC-12:00) International Date Line West' => 'Etc/GMT+12',
+ '(UTC-11:00) Coordinated Universal Time-11' => 'Etc/GMT+11',
+ '(UTC-10:00) Hawaii' => 'Pacific/Honolulu',
+ '(UTC-09:00) Alaska' => 'America/Anchorage',
+ '(UTC-08:00) Pacific Time (US & Canada)' => 'America/Los_Angeles',
+ '(UTC-07:00) Arizona' => 'America/Phoenix',
+ '(UTC-07:00) Chihuahua, La Paz, Mazatlan' => 'America/Chihuahua',
+ '(UTC-07:00) Mountain Time (US & Canada)' => 'America/Denver',
+ '(UTC-06:00) Central America' => 'America/Guatemala',
+ '(UTC-06:00) Central Time (US & Canada)' => 'America/Chicago',
+ '(UTC-06:00) Guadalajara, Mexico City, Monterrey' => 'America/Mexico_City',
+ '(UTC-06:00) Saskatchewan' => 'America/Regina',
+ '(UTC-05:00) Bogota, Lima, Quito, Rio Branco' => 'America/Bogota',
+ '(UTC-05:00) Chetumal' => 'America/Cancun',
+ '(UTC-05:00) Eastern Time (US & Canada)' => 'America/New_York',
+ '(UTC-05:00) Indiana (East)' => 'America/Indianapolis',
+ '(UTC-04:00) Asuncion' => 'America/Asuncion',
+ '(UTC-04:00) Atlantic Time (Canada)' => 'America/Halifax',
+ '(UTC-04:00) Caracas' => 'America/Caracas',
+ '(UTC-04:00) Cuiaba' => 'America/Cuiaba',
+ '(UTC-04:00) Georgetown, La Paz, Manaus, San Juan' => 'America/La_Paz',
+ '(UTC-04:00) Santiago' => 'America/Santiago',
+ '(UTC-03:30) Newfoundland' => 'America/St_Johns',
+ '(UTC-03:00) Brasilia' => 'America/Sao_Paulo',
+ '(UTC-03:00) Cayenne, Fortaleza' => 'America/Cayenne',
+ '(UTC-03:00) City of Buenos Aires' => 'America/Buenos_Aires',
+ '(UTC-03:00) Greenland' => 'America/Godthab',
+ '(UTC-03:00) Montevideo' => 'America/Montevideo',
+ '(UTC-03:00) Salvador' => 'America/Bahia',
+ '(UTC-02:00) Coordinated Universal Time-02' => 'Etc/GMT+2',
+ '(UTC-01:00) Azores' => 'Atlantic/Azores',
+ '(UTC-01:00) Cabo Verde Is.' => 'Atlantic/Cape_Verde',
+ '(UTC) Coordinated Universal Time' => 'Etc/GMT',
+ '(UTC+00:00) Casablanca' => 'Africa/Casablanca',
+ '(UTC+00:00) Dublin, Edinburgh, Lisbon, London' => 'Europe/London',
+ '(UTC+00:00) Monrovia, Reykjavik' => 'Atlantic/Reykjavik',
+ '(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna' => 'Europe/Berlin',
+ '(UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague' => 'Europe/Budapest',
+ '(UTC+01:00) Brussels, Copenhagen, Madrid, Paris' => 'Europe/Paris',
+ '(UTC+01:00) Sarajevo, Skopje, Warsaw, Zagreb' => 'Europe/Warsaw',
+ '(UTC+01:00) West Central Africa' => 'Africa/Lagos',
+ '(UTC+02:00) Amman' => 'Asia/Amman',
+ '(UTC+02:00) Athens, Bucharest' => 'Europe/Bucharest',
+ '(UTC+02:00) Beirut' => 'Asia/Beirut',
+ '(UTC+02:00) Cairo' => 'Africa/Cairo',
+ '(UTC+02:00) Chisinau' => 'Europe/Chisinau',
+ '(UTC+02:00) Damascus' => 'Asia/Damascus',
+ '(UTC+02:00) Harare, Pretoria' => 'Africa/Johannesburg',
+ '(UTC+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius' => 'Europe/Kiev',
+ '(UTC+02:00) Jerusalem' => 'Asia/Jerusalem',
+ '(UTC+02:00) Kaliningrad' => 'Europe/Kaliningrad',
+ '(UTC+02:00) Tripoli' => 'Africa/Tripoli',
+ '(UTC+02:00) Windhoek' => 'Africa/Windhoek',
+ '(UTC+03:00) Baghdad' => 'Asia/Baghdad',
+ '(UTC+03:00) Istanbul' => 'Europe/Istanbul',
+ '(UTC+03:00) Kuwait, Riyadh' => 'Asia/Riyadh',
+ '(UTC+03:00) Minsk' => 'Europe/Minsk',
+ '(UTC+03:00) Moscow, St. Petersburg, Volgograd' => 'Europe/Moscow',
+ '(UTC+03:00) Nairobi' => 'Africa/Nairobi',
+ '(UTC+03:30) Tehran' => 'Asia/Tehran',
+ '(UTC+04:00) Abu Dhabi, Muscat' => 'Asia/Dubai',
+ '(UTC+04:00) Baku' => 'Asia/Baku',
+ '(UTC+04:00) Izhevsk, Samara' => 'Europe/Samara',
+ '(UTC+04:00) Port Louis' => 'Indian/Mauritius',
+ '(UTC+04:00) Tbilisi' => 'Asia/Tbilisi',
+ '(UTC+04:00) Yerevan' => 'Asia/Yerevan',
+ '(UTC+04:30) Kabul' => 'Asia/Kabul',
+ '(UTC+05:00) Ashgabat, Tashkent' => 'Asia/Tashkent',
+ '(UTC+05:00) Ekaterinburg' => 'Asia/Yekaterinburg',
+ '(UTC+05:00) Islamabad, Karachi' => 'Asia/Karachi',
+ '(UTC+05:30) Chennai, Kolkata, Mumbai, New Delhi' => 'Asia/Calcutta',
+ '(UTC+05:30) Sri Jayawardenepura' => 'Asia/Colombo',
+ '(UTC+05:45) Kathmandu' => 'Asia/Katmandu',
+ '(UTC+06:00) Astana' => 'Asia/Almaty',
+ '(UTC+06:00) Dhaka' => 'Asia/Dhaka',
+ '(UTC+06:30) Yangon (Rangoon)' => 'Asia/Rangoon',
+ '(UTC+07:00) Bangkok, Hanoi, Jakarta' => 'Asia/Bangkok',
+ '(UTC+07:00) Krasnoyarsk' => 'Asia/Krasnoyarsk',
+ '(UTC+07:00) Novosibirsk' => 'Asia/Novosibirsk',
+ '(UTC+08:00) Beijing, Chongqing, Hong Kong, Urumqi' => 'Asia/Shanghai',
+ '(UTC+08:00) Irkutsk' => 'Asia/Irkutsk',
+ '(UTC+08:00) Kuala Lumpur, Singapore' => 'Asia/Singapore',
+ '(UTC+08:00) Perth' => 'Australia/Perth',
+ '(UTC+08:00) Taipei' => 'Asia/Taipei',
+ '(UTC+08:00) Ulaanbaatar' => 'Asia/Ulaanbaatar',
+ '(UTC+09:00) Osaka, Sapporo, Tokyo' => 'Asia/Tokyo',
+ '(UTC+09:00) Pyongyang' => 'Asia/Pyongyang',
+ '(UTC+09:00) Seoul' => 'Asia/Seoul',
+ '(UTC+09:00) Yakutsk' => 'Asia/Yakutsk',
+ '(UTC+09:30) Adelaide' => 'Australia/Adelaide',
+ '(UTC+09:30) Darwin' => 'Australia/Darwin',
+ '(UTC+10:00) Brisbane' => 'Australia/Brisbane',
+ '(UTC+10:00) Canberra, Melbourne, Sydney' => 'Australia/Sydney',
+ '(UTC+10:00) Guam, Port Moresby' => 'Pacific/Port_Moresby',
+ '(UTC+10:00) Hobart' => 'Australia/Hobart',
+ '(UTC+10:00) Vladivostok' => 'Asia/Vladivostok',
+ '(UTC+11:00) Chokurdakh' => 'Asia/Srednekolymsk',
+ '(UTC+11:00) Magadan' => 'Asia/Magadan',
+ '(UTC+11:00) Solomon Is., New Caledonia' => 'Pacific/Guadalcanal',
+ '(UTC+12:00) Anadyr, Petropavlovsk-Kamchatsky' => 'Asia/Kamchatka',
+ '(UTC+12:00) Auckland, Wellington' => 'Pacific/Auckland',
+ '(UTC+12:00) Coordinated Universal Time+12' => 'Etc/GMT-12',
+ '(UTC+12:00) Fiji' => 'Pacific/Fiji',
+ "(UTC+13:00) Nuku'alofa" => 'Pacific/Tongatapu',
+ '(UTC+13:00) Samoa' => 'Pacific/Apia',
+ '(UTC+14:00) Kiritimati Island' => 'Pacific/Kiritimati',
+ );
+
+ /**
+ * Maps Windows (non-CLDR) time zone ID to IANA ID. This is pragmatic but not 100% precise as one Windows zone ID
+ * maps to multiple IANA IDs (one for each territory). For all practical purposes this should be good enough, though.
+ *
+ * Source: http://unicode.org/repos/cldr/trunk/common/supplemental/windowsZones.xml
+ *
+ * @var array
+ */
+ private static $windowsTimeZonesMap = array(
+ 'AUS Central Standard Time' => 'Australia/Darwin',
+ 'AUS Eastern Standard Time' => 'Australia/Sydney',
+ 'Afghanistan Standard Time' => 'Asia/Kabul',
+ 'Alaskan Standard Time' => 'America/Anchorage',
+ 'Aleutian Standard Time' => 'America/Adak',
+ 'Altai Standard Time' => 'Asia/Barnaul',
+ 'Arab Standard Time' => 'Asia/Riyadh',
+ 'Arabian Standard Time' => 'Asia/Dubai',
+ 'Arabic Standard Time' => 'Asia/Baghdad',
+ 'Argentina Standard Time' => 'America/Buenos_Aires',
+ 'Astrakhan Standard Time' => 'Europe/Astrakhan',
+ 'Atlantic Standard Time' => 'America/Halifax',
+ 'Aus Central W. Standard Time' => 'Australia/Eucla',
+ 'Azerbaijan Standard Time' => 'Asia/Baku',
+ 'Azores Standard Time' => 'Atlantic/Azores',
+ 'Bahia Standard Time' => 'America/Bahia',
+ 'Bangladesh Standard Time' => 'Asia/Dhaka',
+ 'Belarus Standard Time' => 'Europe/Minsk',
+ 'Bougainville Standard Time' => 'Pacific/Bougainville',
+ 'Canada Central Standard Time' => 'America/Regina',
+ 'Cape Verde Standard Time' => 'Atlantic/Cape_Verde',
+ 'Caucasus Standard Time' => 'Asia/Yerevan',
+ 'Cen. Australia Standard Time' => 'Australia/Adelaide',
+ 'Central America Standard Time' => 'America/Guatemala',
+ 'Central Asia Standard Time' => 'Asia/Almaty',
+ 'Central Brazilian Standard Time' => 'America/Cuiaba',
+ 'Central Europe Standard Time' => 'Europe/Budapest',
+ 'Central European Standard Time' => 'Europe/Warsaw',
+ 'Central Pacific Standard Time' => 'Pacific/Guadalcanal',
+ 'Central Standard Time (Mexico)' => 'America/Mexico_City',
+ 'Central Standard Time' => 'America/Chicago',
+ 'Chatham Islands Standard Time' => 'Pacific/Chatham',
+ 'China Standard Time' => 'Asia/Shanghai',
+ 'Cuba Standard Time' => 'America/Havana',
+ 'Dateline Standard Time' => 'Etc/GMT+12',
+ 'E. Africa Standard Time' => 'Africa/Nairobi',
+ 'E. Australia Standard Time' => 'Australia/Brisbane',
+ 'E. Europe Standard Time' => 'Europe/Chisinau',
+ 'E. South America Standard Time' => 'America/Sao_Paulo',
+ 'Easter Island Standard Time' => 'Pacific/Easter',
+ 'Eastern Standard Time (Mexico)' => 'America/Cancun',
+ 'Eastern Standard Time' => 'America/New_York',
+ 'Egypt Standard Time' => 'Africa/Cairo',
+ 'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg',
+ 'FLE Standard Time' => 'Europe/Kiev',
+ 'Fiji Standard Time' => 'Pacific/Fiji',
+ 'GMT Standard Time' => 'Europe/London',
+ 'GTB Standard Time' => 'Europe/Bucharest',
+ 'Georgian Standard Time' => 'Asia/Tbilisi',
+ 'Greenland Standard Time' => 'America/Godthab',
+ 'Greenwich Standard Time' => 'Atlantic/Reykjavik',
+ 'Haiti Standard Time' => 'America/Port-au-Prince',
+ 'Hawaiian Standard Time' => 'Pacific/Honolulu',
+ 'India Standard Time' => 'Asia/Calcutta',
+ 'Iran Standard Time' => 'Asia/Tehran',
+ 'Israel Standard Time' => 'Asia/Jerusalem',
+ 'Jordan Standard Time' => 'Asia/Amman',
+ 'Kaliningrad Standard Time' => 'Europe/Kaliningrad',
+ 'Korea Standard Time' => 'Asia/Seoul',
+ 'Libya Standard Time' => 'Africa/Tripoli',
+ 'Line Islands Standard Time' => 'Pacific/Kiritimati',
+ 'Lord Howe Standard Time' => 'Australia/Lord_Howe',
+ 'Magadan Standard Time' => 'Asia/Magadan',
+ 'Magallanes Standard Time' => 'America/Punta_Arenas',
+ 'Marquesas Standard Time' => 'Pacific/Marquesas',
+ 'Mauritius Standard Time' => 'Indian/Mauritius',
+ 'Middle East Standard Time' => 'Asia/Beirut',
+ 'Montevideo Standard Time' => 'America/Montevideo',
+ 'Morocco Standard Time' => 'Africa/Casablanca',
+ 'Mountain Standard Time (Mexico)' => 'America/Chihuahua',
+ 'Mountain Standard Time' => 'America/Denver',
+ 'Myanmar Standard Time' => 'Asia/Rangoon',
+ 'N. Central Asia Standard Time' => 'Asia/Novosibirsk',
+ 'Namibia Standard Time' => 'Africa/Windhoek',
+ 'Nepal Standard Time' => 'Asia/Katmandu',
+ 'New Zealand Standard Time' => 'Pacific/Auckland',
+ 'Newfoundland Standard Time' => 'America/St_Johns',
+ 'Norfolk Standard Time' => 'Pacific/Norfolk',
+ 'North Asia East Standard Time' => 'Asia/Irkutsk',
+ 'North Asia Standard Time' => 'Asia/Krasnoyarsk',
+ 'North Korea Standard Time' => 'Asia/Pyongyang',
+ 'Omsk Standard Time' => 'Asia/Omsk',
+ 'Pacific SA Standard Time' => 'America/Santiago',
+ 'Pacific Standard Time (Mexico)' => 'America/Tijuana',
+ 'Pacific Standard Time' => 'America/Los_Angeles',
+ 'Pakistan Standard Time' => 'Asia/Karachi',
+ 'Paraguay Standard Time' => 'America/Asuncion',
+ 'Romance Standard Time' => 'Europe/Paris',
+ 'Russia Time Zone 10' => 'Asia/Srednekolymsk',
+ 'Russia Time Zone 11' => 'Asia/Kamchatka',
+ 'Russia Time Zone 3' => 'Europe/Samara',
+ 'Russian Standard Time' => 'Europe/Moscow',
+ 'SA Eastern Standard Time' => 'America/Cayenne',
+ 'SA Pacific Standard Time' => 'America/Bogota',
+ 'SA Western Standard Time' => 'America/La_Paz',
+ 'SE Asia Standard Time' => 'Asia/Bangkok',
+ 'Saint Pierre Standard Time' => 'America/Miquelon',
+ 'Sakhalin Standard Time' => 'Asia/Sakhalin',
+ 'Samoa Standard Time' => 'Pacific/Apia',
+ 'Sao Tome Standard Time' => 'Africa/Sao_Tome',
+ 'Saratov Standard Time' => 'Europe/Saratov',
+ 'Singapore Standard Time' => 'Asia/Singapore',
+ 'South Africa Standard Time' => 'Africa/Johannesburg',
+ 'Sri Lanka Standard Time' => 'Asia/Colombo',
+ 'Sudan Standard Time' => 'Africa/Tripoli',
+ 'Syria Standard Time' => 'Asia/Damascus',
+ 'Taipei Standard Time' => 'Asia/Taipei',
+ 'Tasmania Standard Time' => 'Australia/Hobart',
+ 'Tocantins Standard Time' => 'America/Araguaina',
+ 'Tokyo Standard Time' => 'Asia/Tokyo',
+ 'Tomsk Standard Time' => 'Asia/Tomsk',
+ 'Tonga Standard Time' => 'Pacific/Tongatapu',
+ 'Transbaikal Standard Time' => 'Asia/Chita',
+ 'Turkey Standard Time' => 'Europe/Istanbul',
+ 'Turks And Caicos Standard Time' => 'America/Grand_Turk',
+ 'US Eastern Standard Time' => 'America/Indianapolis',
+ 'US Mountain Standard Time' => 'America/Phoenix',
+ 'UTC' => 'Etc/GMT',
+ 'UTC+12' => 'Etc/GMT-12',
+ 'UTC+13' => 'Etc/GMT-13',
+ 'UTC-02' => 'Etc/GMT+2',
+ 'UTC-08' => 'Etc/GMT+8',
+ 'UTC-09' => 'Etc/GMT+9',
+ 'UTC-11' => 'Etc/GMT+11',
+ 'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar',
+ 'Venezuela Standard Time' => 'America/Caracas',
+ 'Vladivostok Standard Time' => 'Asia/Vladivostok',
+ 'W. Australia Standard Time' => 'Australia/Perth',
+ 'W. Central Africa Standard Time' => 'Africa/Lagos',
+ 'W. Europe Standard Time' => 'Europe/Berlin',
+ 'W. Mongolia Standard Time' => 'Asia/Hovd',
+ 'West Asia Standard Time' => 'Asia/Tashkent',
+ 'West Bank Standard Time' => 'Asia/Hebron',
+ 'West Pacific Standard Time' => 'Pacific/Port_Moresby',
+ 'Yakutsk Standard Time' => 'Asia/Yakutsk',
+ );
+
+ /**
+ * If `$filterDaysBefore` or `$filterDaysAfter` are set then the events are filtered according to the window defined
+ * by this field and `$windowMaxTimestamp`.
+ *
+ * @var integer
+ */
+ private $windowMinTimestamp;
+
+ /**
+ * If `$filterDaysBefore` or `$filterDaysAfter` are set then the events are filtered according to the window defined
+ * by this field and `$windowMinTimestamp`.
+ *
+ * @var integer
+ */
+ private $windowMaxTimestamp;
+
+ /**
+ * `true` if either `$filterDaysBefore` or `$filterDaysAfter` are set.
+ *
+ * @var boolean
+ */
+ private $shouldFilterByWindow = false;
+
+ /**
+ * Creates the ICal object
+ *
+ * @param mixed $files
+ * @param array $options
+ * @return void
+ */
+ public function __construct($files = false, array $options = array())
+ {
+ if (\PHP_VERSION_ID < 80100) {
+ ini_set('auto_detect_line_endings', '1');
+ }
+
+ foreach ($options as $option => $value) {
+ if (in_array($option, self::$configurableOptions)) {
+ $this->{$option} = $value;
+ }
+ }
+
+ // Fallback to use the system default time zone
+ if (!isset($this->defaultTimeZone) || !$this->isValidTimeZoneId($this->defaultTimeZone)) {
+ $this->defaultTimeZone = date_default_timezone_get();
+ }
+
+ // Ideally you would use `PHP_INT_MIN` from PHP 7
+ $php_int_min = -2147483648;
+
+ $this->windowMinTimestamp = is_null($this->filterDaysBefore) ? $php_int_min : (new \DateTime('now'))->sub(new \DateInterval('P' . $this->filterDaysBefore . 'D'))->getTimestamp();
+ $this->windowMaxTimestamp = is_null($this->filterDaysAfter) ? PHP_INT_MAX : (new \DateTime('now'))->add(new \DateInterval('P' . $this->filterDaysAfter . 'D'))->getTimestamp();
+
+ $this->shouldFilterByWindow = !is_null($this->filterDaysBefore) || !is_null($this->filterDaysAfter);
+
+ if ($files !== false) {
+ $files = is_array($files) ? $files : array($files);
+
+ foreach ($files as $file) {
+ if (!is_array($file) && $this->isFileOrUrl($file)) {
+ $lines = $this->fileOrUrl($file);
+ } else {
+ $lines = is_array($file) ? $file : array($file);
+ }
+
+ $this->initLines($lines);
+ }
+ }
+ }
+
+ /**
+ * Initialises lines from a string
+ *
+ * @param string $string
+ * @return ICal
+ */
+ public function initString($string)
+ {
+ $string = str_replace(array("\r\n", "\n\r", "\r"), "\n", $string);
+
+ if (empty($this->cal)) {
+ $lines = explode("\n", $string);
+
+ $this->initLines($lines);
+ } else {
+ trigger_error('ICal::initString: Calendar already initialised in constructor', E_USER_NOTICE);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Initialises lines from a file
+ *
+ * @param string $file
+ * @return ICal
+ */
+ public function initFile($file)
+ {
+ if (empty($this->cal)) {
+ $lines = $this->fileOrUrl($file);
+
+ $this->initLines($lines);
+ } else {
+ trigger_error('ICal::initFile: Calendar already initialised in constructor', E_USER_NOTICE);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Initialises lines from a URL
+ *
+ * @param string $url
+ * @param string $username
+ * @param string $password
+ * @param string $userAgent
+ * @param string $acceptLanguage
+ * @param string $httpProtocolVersion
+ * @return ICal
+ */
+ public function initUrl($url, $username = null, $password = null, $userAgent = null, $acceptLanguage = null, $httpProtocolVersion = null)
+ {
+ if (!is_null($username) && !is_null($password)) {
+ $this->httpBasicAuth['username'] = $username;
+ $this->httpBasicAuth['password'] = $password;
+ }
+
+ if (!is_null($userAgent)) {
+ $this->httpUserAgent = $userAgent;
+ }
+
+ if (!is_null($acceptLanguage)) {
+ $this->httpAcceptLanguage = $acceptLanguage;
+ }
+
+ if (!is_null($httpProtocolVersion)) {
+ $this->httpProtocolVersion = $httpProtocolVersion;
+ }
+
+ $this->initFile($url);
+
+ return $this;
+ }
+
+ /**
+ * Initialises the parser using an array
+ * containing each line of iCal content
+ *
+ * @param array $lines
+ * @return void
+ */
+ protected function initLines(array $lines)
+ {
+ $lines = $this->unfold($lines);
+
+ if (stristr($lines[0], 'BEGIN:VCALENDAR') !== false) {
+ $component = '';
+ foreach ($lines as $line) {
+ $line = rtrim($line); // Trim trailing whitespace
+ $line = $this->removeUnprintableChars($line);
+
+ if (empty($line)) {
+ continue;
+ }
+
+ if (!$this->disableCharacterReplacement) {
+ $line = str_replace(array(
+ ' ',
+ "\t",
+ "\xc2\xa0", // Non-breaking space
+ ), ' ', $line);
+
+ $line = $this->cleanCharacters($line);
+ }
+
+ $add = $this->keyValueFromString($line);
+
+ if ($add === false) {
+ continue;
+ }
+
+ $keyword = $add[0];
+ $values = $add[1]; // May be an array containing multiple values
+
+ if (!is_array($values)) {
+ if (!empty($values)) {
+ $values = array($values); // Make an array as not one already
+ $blankArray = array(); // Empty placeholder array
+ $values[] = $blankArray;
+ } else {
+ $values = array(); // Use blank array to ignore this line
+ }
+ } elseif (empty($values[0])) {
+ $values = array(); // Use blank array to ignore this line
+ }
+
+ // Reverse so that our array of properties is processed first
+ $values = array_reverse($values);
+
+ foreach ($values as $value) {
+ switch ($line) {
+ // https://www.kanzaki.com/docs/ical/vtodo.html
+ case 'BEGIN:VTODO':
+ if (!is_array($value)) {
+ $this->todoCount++;
+ }
+
+ $component = 'VTODO';
+
+ break;
+
+ case 'BEGIN:VEVENT':
+ // https://www.kanzaki.com/docs/ical/vevent.html
+ if (!is_array($value)) {
+ $this->eventCount++;
+ }
+
+ $component = 'VEVENT';
+
+ break;
+
+ case 'BEGIN:VFREEBUSY':
+ // https://www.kanzaki.com/docs/ical/vfreebusy.html
+ if (!is_array($value)) {
+ $this->freeBusyIndex++;
+ }
+
+ $component = 'VFREEBUSY';
+
+ break;
+
+ case 'BEGIN:VALARM':
+ if (!is_array($value)) {
+ $this->alarmCount++;
+ }
+
+ $component = 'VALARM';
+
+ break;
+
+ case 'END:VALARM':
+ $component = 'VEVENT';
+
+ break;
+
+ case 'BEGIN:DAYLIGHT':
+ case 'BEGIN:STANDARD':
+ case 'BEGIN:VCALENDAR':
+ case 'BEGIN:VTIMEZONE':
+ $component = $value;
+
+ break;
+
+ case 'END:DAYLIGHT':
+ case 'END:STANDARD':
+ case 'END:VCALENDAR':
+ case 'END:VFREEBUSY':
+ case 'END:VTIMEZONE':
+ case 'END:VTODO':
+ $component = 'VCALENDAR';
+
+ break;
+
+ case 'END:VEVENT':
+ if ($this->shouldFilterByWindow) {
+ $this->removeLastEventIfOutsideWindowAndNonRecurring();
+ }
+
+ $component = 'VCALENDAR';
+
+ break;
+
+ default:
+ $this->addCalendarComponentWithKeyAndValue($component, $keyword, $value);
+
+ break;
+ }
+ }
+ }
+
+ $this->processEvents();
+
+ if (!$this->skipRecurrence) {
+ $this->processRecurrences();
+
+ // Apply changes to altered recurrence instances
+ if (!empty($this->alteredRecurrenceInstances)) {
+ $events = $this->cal['VEVENT'];
+
+ foreach ($this->alteredRecurrenceInstances as $alteredRecurrenceInstance) {
+ if (isset($alteredRecurrenceInstance['altered-event'])) {
+ $alteredEvent = $alteredRecurrenceInstance['altered-event'];
+ $key = key($alteredEvent);
+ $events[$key] = $alteredEvent[$key];
+ }
+ }
+
+ $this->cal['VEVENT'] = $events;
+ }
+ }
+
+ if ($this->shouldFilterByWindow) {
+ $this->reduceEventsToMinMaxRange();
+ }
+
+ $this->processDateConversions();
+ }
+ }
+
+ /**
+ * Removes the last event (i.e. most recently parsed) if its start date is outside the window spanned by
+ * `$windowMinTimestamp` / `$windowMaxTimestamp`.
+ *
+ * @return void
+ */
+ protected function removeLastEventIfOutsideWindowAndNonRecurring()
+ {
+ $events = $this->cal['VEVENT'];
+
+ if (!empty($events)) {
+ $lastIndex = count($events) - 1;
+ $lastEvent = $events[$lastIndex];
+
+ if ((!isset($lastEvent['RRULE']) || $lastEvent['RRULE'] === '') && $this->doesEventStartOutsideWindow($lastEvent)) {
+ $this->eventCount--;
+
+ unset($events[$lastIndex]);
+ }
+
+ $this->cal['VEVENT'] = $events;
+ }
+ }
+
+ /**
+ * Reduces the number of events to the defined minimum and maximum range
+ *
+ * @return void
+ */
+ protected function reduceEventsToMinMaxRange()
+ {
+ $events = (isset($this->cal['VEVENT'])) ? $this->cal['VEVENT'] : array();
+
+ if (!empty($events)) {
+ foreach ($events as $key => $anEvent) {
+ if ($anEvent === null) {
+ unset($events[$key]);
+
+ continue;
+ }
+
+ if ($this->doesEventStartOutsideWindow($anEvent)) {
+ $this->eventCount--;
+
+ unset($events[$key]);
+
+ continue;
+ }
+ }
+
+ $this->cal['VEVENT'] = $events;
+ }
+ }
+
+ /**
+ * Determines whether the event start date is outside `$windowMinTimestamp` / `$windowMaxTimestamp`.
+ * Returns `true` for invalid dates.
+ *
+ * @param array $event
+ * @return boolean
+ */
+ protected function doesEventStartOutsideWindow(array $event)
+ {
+ return !$this->isValidDate($event['DTSTART']) || $this->isOutOfRange($event['DTSTART'], $this->windowMinTimestamp, $this->windowMaxTimestamp);
+ }
+
+ /**
+ * Determines whether a valid iCalendar date is within a given range
+ *
+ * @param string $calendarDate
+ * @param integer $minTimestamp
+ * @param integer $maxTimestamp
+ * @return boolean
+ */
+ protected function isOutOfRange($calendarDate, $minTimestamp, $maxTimestamp)
+ {
+ $timestamp = strtotime(explode('T', $calendarDate)[0]);
+
+ return $timestamp < $minTimestamp || $timestamp > $maxTimestamp;
+ }
+
+ /**
+ * Unfolds an iCal file in preparation for parsing
+ * (https://icalendar.org/iCalendar-RFC-5545/3-1-content-lines.html)
+ *
+ * @param array $lines
+ * @return array
+ */
+ protected function unfold(array $lines)
+ {
+ $string = implode(PHP_EOL, $lines);
+ $string = str_ireplace(' ', ' ', $string);
+ $string = preg_replace('/' . PHP_EOL . '[ \t]/', '', $string);
+
+ $lines = explode(PHP_EOL, $string);
+
+ return $lines;
+ }
+
+ /**
+ * Add one key and value pair to the `$this->cal` array
+ *
+ * @param string $component
+ * @param string|boolean $keyword
+ * @param string|array $value
+ * @return void
+ */
+ protected function addCalendarComponentWithKeyAndValue($component, $keyword, $value)
+ {
+ if ($keyword === false) {
+ $keyword = $this->lastKeyword;
+ }
+
+ switch ($component) {
+ case 'VALARM':
+ $key1 = 'VEVENT';
+ $key2 = ($this->eventCount - 1);
+ $key3 = $component;
+
+ if (!isset($this->cal[$key1][$key2][$key3]["{$keyword}_array"])) {
+ $this->cal[$key1][$key2][$key3]["{$keyword}_array"] = array();
+ }
+
+ if (is_array($value)) {
+ // Add array of properties to the end
+ $this->cal[$key1][$key2][$key3]["{$keyword}_array"][] = $value;
+ } else {
+ if (!isset($this->cal[$key1][$key2][$key3][$keyword])) {
+ $this->cal[$key1][$key2][$key3][$keyword] = $value;
+ }
+
+ if ($this->cal[$key1][$key2][$key3][$keyword] !== $value) {
+ $this->cal[$key1][$key2][$key3][$keyword] .= ',' . $value;
+ }
+ }
+ break;
+
+ case 'VEVENT':
+ $key1 = $component;
+ $key2 = ($this->eventCount - 1);
+
+ if (!isset($this->cal[$key1][$key2]["{$keyword}_array"])) {
+ $this->cal[$key1][$key2]["{$keyword}_array"] = array();
+ }
+
+ if (is_array($value)) {
+ // Add array of properties to the end
+ $this->cal[$key1][$key2]["{$keyword}_array"][] = $value;
+ } else {
+ if (!isset($this->cal[$key1][$key2][$keyword])) {
+ $this->cal[$key1][$key2][$keyword] = $value;
+ }
+
+ if ($keyword === 'EXDATE') {
+ if (trim($value) === $value) {
+ $array = array_filter(explode(',', $value));
+ $this->cal[$key1][$key2]["{$keyword}_array"][] = $array;
+ } else {
+ $value = explode(',', implode(',', $this->cal[$key1][$key2]["{$keyword}_array"][1]) . trim($value));
+ $this->cal[$key1][$key2]["{$keyword}_array"][1] = $value;
+ }
+ } else {
+ $this->cal[$key1][$key2]["{$keyword}_array"][] = $value;
+
+ if ($keyword === 'DURATION') {
+ $duration = new \DateInterval($value);
+ $this->cal[$key1][$key2]["{$keyword}_array"][] = $duration;
+ }
+ }
+
+ if ($this->cal[$key1][$key2][$keyword] !== $value) {
+ $this->cal[$key1][$key2][$keyword] .= ',' . $value;
+ }
+ }
+ break;
+
+ case 'VFREEBUSY':
+ $key1 = $component;
+ $key2 = ($this->freeBusyIndex - 1);
+ $key3 = $keyword;
+
+ if ($keyword === 'FREEBUSY') {
+ if (is_array($value)) {
+ $this->cal[$key1][$key2][$key3][][] = $value;
+ } else {
+ $this->freeBusyCount++;
+
+ end($this->cal[$key1][$key2][$key3]);
+ $key = key($this->cal[$key1][$key2][$key3]);
+
+ $value = explode('/', $value);
+ $this->cal[$key1][$key2][$key3][$key][] = $value;
+ }
+ } else {
+ $this->cal[$key1][$key2][$key3][] = $value;
+ }
+ break;
+
+ case 'VTODO':
+ $this->cal[$component][$this->todoCount - 1][$keyword] = $value;
+
+ break;
+
+ default:
+ $this->cal[$component][$keyword] = $value;
+
+ break;
+ }
+
+ $this->lastKeyword = $keyword;
+ }
+
+ /**
+ * Gets the key value pair from an iCal string
+ *
+ * @param string $text
+ * @return array|boolean
+ */
+ public function keyValueFromString($text)
+ {
+ $splitLine = $this->parseLine($text);
+ $object = array();
+ $paramObj = array();
+ $valueObj = '';
+ $i = 0;
+
+ while ($i < count($splitLine)) {
+ // The first token corresponds to the property name
+ if ($i === 0) {
+ $object[0] = $splitLine[$i];
+ $i++;
+
+ continue;
+ }
+
+ // After each semicolon define the property parameters
+ if ($splitLine[$i] == ';') {
+ $i++;
+ $paramName = $splitLine[$i];
+ $i += 2;
+ $paramValue = array();
+ $multiValue = false;
+ // A parameter can have multiple values separated by a comma
+ while ($i + 1 < count($splitLine) && $splitLine[$i + 1] === ',') {
+ $paramValue[] = $splitLine[$i];
+ $i += 2;
+ $multiValue = true;
+ }
+
+ if ($multiValue) {
+ $paramValue[] = $splitLine[$i];
+ } else {
+ $paramValue = $splitLine[$i];
+ }
+
+ // Create object with paramName => paramValue
+ $paramObj[$paramName] = $paramValue;
+ }
+
+ // After a colon all tokens are concatenated (non-standard behaviour because the property can have multiple values
+ // according to RFC5545)
+ if ($splitLine[$i] === ':') {
+ $i++;
+ while ($i < count($splitLine)) {
+ $valueObj .= $splitLine[$i];
+ $i++;
+ }
+ }
+
+ $i++;
+ }
+
+ // Object construction
+ if ($paramObj !== array()) {
+ $object[1][0] = $valueObj;
+ $object[1][1] = $paramObj;
+ } else {
+ $object[1] = $valueObj;
+ }
+
+ return $object;
+ }
+
+ /**
+ * Parses a line from an iCal file into an array of tokens
+ *
+ * @param string $line
+ * @return array
+ */
+ protected function parseLine($line)
+ {
+ $words = array();
+ $word = '';
+ // The use of str_split is not a problem here even if the character set is in utf8
+ // Indeed we only compare the characters , ; : = " which are on a single byte
+ $arrayOfChar = str_split($line);
+ $inDoubleQuotes = false;
+
+ foreach ($arrayOfChar as $char) {
+ // Don't stop the word on ; , : = if it is enclosed in double quotes
+ if ($char === '"') {
+ if ($word !== '') {
+ $words[] = $word;
+ }
+
+ $word = '';
+ $inDoubleQuotes = !$inDoubleQuotes;
+ } elseif (!in_array($char, array(';', ':', ',', '=')) || $inDoubleQuotes) {
+ $word .= $char;
+ } else {
+ if ($word !== '') {
+ $words[] = $word;
+ }
+
+ $words[] = $char;
+ $word = '';
+ }
+ }
+
+ $words[] = $word;
+
+ return $words;
+ }
+
+ /**
+ * Returns a `DateTime` object from an iCal date time format
+ *
+ * @param string $icalDate
+ * @return \DateTime
+ * @throws \Exception
+ */
+ public function iCalDateToDateTime($icalDate)
+ {
+ /**
+ * iCal times may be in 3 formats, (https://www.kanzaki.com/docs/ical/dateTime.html)
+ *
+ * UTC: Has a trailing 'Z'
+ * Floating: No time zone reference specified, no trailing 'Z', use local time
+ * TZID: Set time zone as specified
+ *
+ * Use DateTime class objects to get around limitations with `mktime` and `gmmktime`.
+ * Must have a local time zone set to process floating times.
+ */
+ $pattern = '/^(?:TZID=)?([^:]*|".*")'; // [1]: Time zone
+ $pattern .= ':?'; // Time zone delimiter
+ $pattern .= '([0-9]{8})'; // [2]: YYYYMMDD
+ $pattern .= 'T?'; // Time delimiter
+ $pattern .= '(?(?<=T)([0-9]{6}))'; // [3]: HHMMSS (filled if delimiter present)
+ $pattern .= '(Z?)/'; // [4]: UTC flag
+
+ preg_match($pattern, $icalDate, $date);
+
+ if (empty($date)) {
+ throw new \Exception('Invalid iCal date format.');
+ }
+
+ // A Unix timestamp usually cannot represent a date prior to 1 Jan 1970.
+ // PHP, on the other hand, uses negative numbers for that. Thus we don't
+ // need to special case them.
+
+ if ($date[4] === 'Z') {
+ $dateTimeZone = new \DateTimeZone(self::TIME_ZONE_UTC);
+ } elseif (isset($date[1]) && $date[1] !== '') {
+ $dateTimeZone = $this->timeZoneStringToDateTimeZone($date[1]);
+ } else {
+ $dateTimeZone = new \DateTimeZone($this->defaultTimeZone);
+ }
+
+ // The exclamation mark at the start of the format string indicates that if a
+ // time portion is not included, the time in the returned DateTime should be
+ // set to 00:00:00. Without it, the time would be set to the current system time.
+ $dateFormat = '!Ymd';
+ $dateBasic = $date[2];
+ if (isset($date[3]) && $date[3] !== '') {
+ $dateBasic .= "T{$date[3]}";
+ $dateFormat .= '\THis';
+ }
+
+ return \DateTime::createFromFormat($dateFormat, $dateBasic, $dateTimeZone);
+ }
+
+ /**
+ * Returns a Unix timestamp from an iCal date time format
+ *
+ * @param string $icalDate
+ * @return integer
+ */
+ public function iCalDateToUnixTimestamp($icalDate)
+ {
+ return $this->iCalDateToDateTime($icalDate)->getTimestamp();
+ }
+
+ /**
+ * Returns a date adapted to the calendar time zone depending on the event `TZID`
+ *
+ * @param array $event
+ * @param string $key
+ * @param string|null $format
+ * @return string|boolean|\DateTime
+ */
+ public function iCalDateWithTimeZone(array $event, $key, $format = self::DATE_TIME_FORMAT)
+ {
+ if (!isset($event["{$key}_array"]) || !isset($event[$key])) {
+ return false;
+ }
+
+ $dateArray = $event["{$key}_array"];
+
+ if ($key === 'DURATION') {
+ $dateTime = $this->parseDuration($event['DTSTART'], $dateArray[2], null);
+ } else {
+ // When constructing from a Unix Timestamp, no time zone needs passing.
+ $dateTime = new \DateTime("@{$dateArray[2]}");
+ }
+
+ // Set the time zone we wish to use when running `$dateTime->format`.
+ $dateTime->setTimezone(new \DateTimeZone($this->calendarTimeZone()));
+
+ if (is_null($format)) {
+ return $dateTime;
+ }
+
+ return $dateTime->format($format);
+ }
+
+ /**
+ * Performs admin tasks on all events as read from the iCal file.
+ * Adds a Unix timestamp to all `{DTSTART|DTEND|RECURRENCE-ID}_array` arrays
+ * Tracks modified recurrence instances
+ *
+ * @return void
+ */
+ protected function processEvents()
+ {
+ $checks = null;
+ $events = (isset($this->cal['VEVENT'])) ? $this->cal['VEVENT'] : array();
+
+ if (!empty($events)) {
+ foreach ($events as $key => $anEvent) {
+ foreach (array('DTSTART', 'DTEND', 'RECURRENCE-ID') as $type) {
+ if (isset($anEvent[$type])) {
+ $date = $anEvent["{$type}_array"][1];
+
+ if (isset($anEvent["{$type}_array"][0]['TZID'])) {
+ $timeZone = $this->escapeParamText($anEvent["{$type}_array"][0]['TZID']);
+ $date = sprintf(self::ICAL_DATE_TIME_TEMPLATE, $timeZone) . $date;
+ }
+
+ $anEvent["{$type}_array"][2] = $this->iCalDateToUnixTimestamp($date);
+ $anEvent["{$type}_array"][3] = $date;
+ }
+ }
+
+ if (isset($anEvent['RECURRENCE-ID'])) {
+ $uid = $anEvent['UID'];
+
+ if (!isset($this->alteredRecurrenceInstances[$uid])) {
+ $this->alteredRecurrenceInstances[$uid] = array();
+ }
+
+ $recurrenceDateUtc = $this->iCalDateToUnixTimestamp($anEvent['RECURRENCE-ID_array'][3]);
+ $this->alteredRecurrenceInstances[$uid][$key] = $recurrenceDateUtc;
+ }
+
+ $events[$key] = $anEvent;
+ }
+
+ $eventKeysToRemove = array();
+
+ foreach ($events as $key => $event) {
+ $checks[] = !isset($event['RECURRENCE-ID']);
+ $checks[] = isset($event['UID']);
+ $checks[] = isset($event['UID']) && isset($this->alteredRecurrenceInstances[$event['UID']]);
+
+ if ((bool) array_product($checks)) {
+ $eventDtstartUnix = $this->iCalDateToUnixTimestamp($event['DTSTART_array'][3]);
+
+ // phpcs:ignore CustomPHPCS.ControlStructures.AssignmentInCondition
+ if (($alteredEventKey = array_search($eventDtstartUnix, $this->alteredRecurrenceInstances[$event['UID']], true)) !== false) {
+ $eventKeysToRemove[] = $alteredEventKey;
+
+ $alteredEvent = array_replace_recursive($events[$key], $events[$alteredEventKey]);
+ $this->alteredRecurrenceInstances[$event['UID']]['altered-event'] = array($key => $alteredEvent);
+ }
+ }
+
+ unset($checks);
+ }
+
+ foreach ($eventKeysToRemove as $eventKeyToRemove) {
+ $events[$eventKeyToRemove] = null;
+ }
+
+ $this->cal['VEVENT'] = $events;
+ }
+ }
+
+ /**
+ * Processes recurrence rules
+ *
+ * @return void
+ */
+ protected function processRecurrences()
+ {
+ $events = (isset($this->cal['VEVENT'])) ? $this->cal['VEVENT'] : array();
+
+ // If there are no events, then we have nothing to process.
+ if (empty($events)) {
+ return;
+ }
+
+ $allEventRecurrences = array();
+ $eventKeysToRemove = array();
+
+ foreach ($events as $key => $anEvent) {
+ if (!isset($anEvent['RRULE']) || $anEvent['RRULE'] === '') {
+ continue;
+ }
+
+ // Tag as generated by a recurrence rule
+ $anEvent['RRULE_array'][2] = self::RECURRENCE_EVENT;
+
+ // Create new initial starting point.
+ $initialEventDate = $this->icalDateToDateTime($anEvent['DTSTART_array'][3]);
+
+ // Separate the RRULE stanzas, and explode the values that are lists.
+ $rrules = array();
+ foreach (array_filter(explode(';', $anEvent['RRULE'])) as $s) {
+ list($k, $v) = explode('=', $s);
+ if (in_array($k, array('BYSETPOS', 'BYDAY', 'BYMONTHDAY', 'BYMONTH', 'BYYEARDAY', 'BYWEEKNO'))) {
+ $rrules[$k] = explode(',', $v);
+ } else {
+ $rrules[$k] = $v;
+ }
+ }
+
+ // Get frequency
+ $frequency = $rrules['FREQ'];
+
+ // Reject RRULE if BYDAY stanza is invalid:
+ // > The BYDAY rule part MUST NOT be specified with a numeric value
+ // > when the FREQ rule part is not set to MONTHLY or YEARLY.
+ // > Furthermore, the BYDAY rule part MUST NOT be specified with a
+ // > numeric value with the FREQ rule part set to YEARLY when the
+ // > BYWEEKNO rule part is specified.
+ if (isset($rrules['BYDAY'])) {
+ $checkByDays = function ($carry, $weekday) {
+ return $carry && substr($weekday, -2) === $weekday;
+ };
+ if (!in_array($frequency, array('MONTHLY', 'YEARLY'))) {
+ if (!array_reduce($rrules['BYDAY'], $checkByDays, true)) {
+ error_log("ICal::ProcessRecurrences: A {$frequency} RRULE may not contain BYDAY values with numeric prefixes");
+
+ continue;
+ }
+ } elseif ($frequency === 'YEARLY' && (isset($rrules['BYWEEKNO']) && ($rrules['BYWEEKNO'] !== '' && $rrules['BYWEEKNO'] !== array()))) {
+ if (!array_reduce($rrules['BYDAY'], $checkByDays, true)) {
+ error_log('ICal::ProcessRecurrences: A YEARLY RRULE with a BYWEEKNO part may not contain BYDAY values with numeric prefixes');
+
+ continue;
+ }
+ }
+ }
+
+ // Get Interval
+ $interval = (empty($rrules['INTERVAL'])) ? 1 : (int) $rrules['INTERVAL'];
+
+ // Throw an error if this isn't an integer.
+ if (!is_int($this->defaultSpan)) {
+ trigger_error('ICal::defaultSpan: User defined value is not an integer', E_USER_NOTICE);
+ }
+
+ // Compute EXDATEs
+ $exdates = $this->parseExdates($anEvent);
+
+ // Determine if the initial date is also an EXDATE
+ $initialDateIsExdate = array_reduce($exdates, function ($carry, $exdate) use ($initialEventDate) {
+ return $carry || $exdate->getTimestamp() === $initialEventDate->getTimestamp();
+ }, false);
+
+ if ($initialDateIsExdate) {
+ $eventKeysToRemove[] = $key;
+ }
+
+ /**
+ * Determine at what point we should stop calculating recurrences
+ * by looking at the UNTIL or COUNT rrule stanza, or, if neither
+ * if set, using a fallback.
+ *
+ * If the initial date is also an EXDATE, it shouldn't be included
+ * in the count.
+ *
+ * Syntax:
+ * UNTIL={enddate}
+ * COUNT=
+ *
+ * Where:
+ * enddate = ||
+ */
+ $count = 1;
+ $countLimit = (isset($rrules['COUNT'])) ? intval($rrules['COUNT']) : PHP_INT_MAX;
+ $until = date_create()->modify("{$this->defaultSpan} years")->setTime(23, 59, 59)->getTimestamp();
+ $untilWhile = $until;
+
+ if (isset($rrules['UNTIL'])) {
+ $untilDT = $this->iCalDateToDateTime($rrules['UNTIL']);
+ $until = min($until, $untilDT->getTimestamp());
+
+ // There are certain edge cases where we need to go a little beyond the UNTIL to
+ // ensure we get all events. Consider:
+ //
+ // DTSTART:20200103
+ // RRULE:FREQ=MONTHLY;BYDAY=-5FR;UNTIL=20200502
+ //
+ // In this case the last occurrence should be 1st May, however when we transition
+ // from April to May:
+ //
+ // $until ~= 2nd May
+ // $frequencyRecurringDateTime ~= 3rd May
+ //
+ // And as the latter comes after the former, the while loop ends before any dates
+ // in May have the chance to be considered.
+ $untilWhile = min($untilWhile, $untilDT->modify("+1 {$this->frequencyConversion[$frequency]}")->getTimestamp());
+ }
+
+ $eventRecurrences = array();
+
+ $frequencyRecurringDateTime = clone $initialEventDate;
+ while ($frequencyRecurringDateTime->getTimestamp() <= $untilWhile && $count < $countLimit) {
+ $candidateDateTimes = array();
+
+ // phpcs:ignore Squiz.ControlStructures.SwitchDeclaration.MissingDefault
+ switch ($frequency) {
+ case 'DAILY':
+ if (isset($rrules['BYMONTHDAY']) && ($rrules['BYMONTHDAY'] !== array() && $rrules['BYMONTHDAY'] !== '')) {
+ if (!isset($monthDays)) {
+ // This variable is unset when we change months (see below)
+ $monthDays = $this->getDaysOfMonthMatchingByMonthDayRRule($rrules['BYMONTHDAY'], $frequencyRecurringDateTime);
+ }
+
+ if (!in_array($frequencyRecurringDateTime->format('j'), $monthDays)) {
+ break;
+ }
+ }
+
+ $candidateDateTimes[] = clone $frequencyRecurringDateTime;
+
+ break;
+
+ case 'WEEKLY':
+ $initialDayOfWeek = $frequencyRecurringDateTime->format('N');
+ $matchingDays = array($initialDayOfWeek);
+
+ if (isset($rrules['BYDAY']) && ($rrules['BYDAY'] !== array() && $rrules['BYDAY'] !== '')) {
+ // setISODate() below uses the ISO-8601 specification of weeks: start on
+ // a Monday, end on a Sunday. However, RRULEs (or the caller of the
+ // parser) may state an alternate WeeKSTart.
+ $wkstTransition = 7;
+
+ if (empty($rrules['WKST'])) {
+ if ($this->defaultWeekStart !== self::ISO_8601_WEEK_START) {
+ $wkstTransition = array_search($this->defaultWeekStart, array_keys($this->weekdays), true);
+ }
+ } elseif ($rrules['WKST'] !== self::ISO_8601_WEEK_START) {
+ $wkstTransition = array_search($rrules['WKST'], array_keys($this->weekdays), true);
+ }
+
+ $matchingDays = array_map(
+ function ($weekday) use ($initialDayOfWeek, $wkstTransition, $interval) {
+ $day = array_search($weekday, array_keys($this->weekdays), true);
+
+ if ($day < $initialDayOfWeek) {
+ $day += 7;
+ }
+
+ if ($day >= $wkstTransition) {
+ $day += 7 * ($interval - 1);
+ }
+
+ // Ignoring alternate week starts, $day at this point will have a
+ // value between 0 and 6. But setISODate() expects a value of 1 to 7.
+ // Even with alternate week starts, we still need to +1 to set the
+ // correct weekday.
+ $day++;
+
+ return $day;
+ },
+ $rrules['BYDAY']
+ );
+ }
+
+ sort($matchingDays);
+
+ foreach ($matchingDays as $day) {
+ $clonedDateTime = clone $frequencyRecurringDateTime;
+ $candidateDateTimes[] = $clonedDateTime->setISODate(
+ (int) $frequencyRecurringDateTime->format('o'),
+ (int) $frequencyRecurringDateTime->format('W'),
+ $day
+ );
+ }
+ break;
+
+ case 'MONTHLY':
+ $matchingDays = array();
+
+ if (isset($rrules['BYMONTHDAY']) && ($rrules['BYMONTHDAY'] !== array() && $rrules['BYMONTHDAY'] !== '')) {
+ $matchingDays = $this->getDaysOfMonthMatchingByMonthDayRRule($rrules['BYMONTHDAY'], $frequencyRecurringDateTime);
+ if (isset($rrules['BYDAY']) && ($rrules['BYDAY'] !== '' && $rrules['BYDAY'] !== array())) {
+ $matchingDays = array_filter(
+ $this->getDaysOfMonthMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime),
+ function ($monthDay) use ($matchingDays) {
+ return in_array($monthDay, $matchingDays);
+ }
+ );
+ }
+ } elseif (isset($rrules['BYDAY']) && ($rrules['BYDAY'] !== array() && $rrules['BYDAY'] !== '')) {
+ $matchingDays = $this->getDaysOfMonthMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime);
+ } else {
+ $matchingDays[] = $frequencyRecurringDateTime->format('d');
+ }
+
+ if (isset($rrules['BYSETPOS']) && ($rrules['BYSETPOS'] !== array() && $rrules['BYSETPOS'] !== '')) {
+ $matchingDays = $this->filterValuesUsingBySetPosRRule($rrules['BYSETPOS'], $matchingDays);
+ }
+
+ foreach ($matchingDays as $day) {
+ // Skip invalid dates (e.g. 30th February)
+ if ($day > $frequencyRecurringDateTime->format('t')) {
+ continue;
+ }
+
+ $clonedDateTime = clone $frequencyRecurringDateTime;
+ $candidateDateTimes[] = $clonedDateTime->setDate(
+ (int) $frequencyRecurringDateTime->format('Y'),
+ (int) $frequencyRecurringDateTime->format('m'),
+ $day
+ );
+ }
+ break;
+
+ case 'YEARLY':
+ $matchingDays = array();
+
+ if (isset($rrules['BYMONTH']) && ($rrules['BYMONTH'] !== array() && $rrules['BYMONTH'] !== '')) {
+ $bymonthRecurringDatetime = clone $frequencyRecurringDateTime;
+ foreach ($rrules['BYMONTH'] as $byMonth) {
+ $bymonthRecurringDatetime->setDate(
+ (int) $frequencyRecurringDateTime->format('Y'),
+ (int) $byMonth,
+ (int) $frequencyRecurringDateTime->format('d')
+ );
+
+ // Determine the days of the month affected
+ // (The interaction between BYMONTHDAY and BYDAY is resolved later.)
+ $monthDays = array();
+ if (isset($rrules['BYMONTHDAY']) && ($rrules['BYMONTHDAY'] !== '' && $rrules['BYMONTHDAY'] !== array())) {
+ $monthDays = $this->getDaysOfMonthMatchingByMonthDayRRule($rrules['BYMONTHDAY'], $bymonthRecurringDatetime);
+ } elseif (isset($rrules['BYDAY']) && ($rrules['BYDAY'] !== '' && $rrules['BYDAY'] !== array())) {
+ $monthDays = $this->getDaysOfMonthMatchingByDayRRule($rrules['BYDAY'], $bymonthRecurringDatetime);
+ } else {
+ $monthDays[] = $bymonthRecurringDatetime->format('d');
+ }
+
+ // And add each of them to the list of recurrences
+ foreach ($monthDays as $day) {
+ $matchingDays[] = $bymonthRecurringDatetime->setDate(
+ (int) $frequencyRecurringDateTime->format('Y'),
+ (int) $bymonthRecurringDatetime->format('m'),
+ $day
+ )->format('z') + 1;
+ }
+ }
+ } elseif (isset($rrules['BYWEEKNO']) && ($rrules['BYWEEKNO'] !== array() && $rrules['BYWEEKNO'] !== '')) {
+ $matchingDays = $this->getDaysOfYearMatchingByWeekNoRRule($rrules['BYWEEKNO'], $frequencyRecurringDateTime);
+ } elseif (isset($rrules['BYYEARDAY']) && ($rrules['BYYEARDAY'] !== array() && $rrules['BYYEARDAY'] !== '')) {
+ $matchingDays = $this->getDaysOfYearMatchingByYearDayRRule($rrules['BYYEARDAY'], $frequencyRecurringDateTime);
+ } elseif (isset($rrules['BYMONTHDAY']) && ($rrules['BYMONTHDAY'] !== array() && $rrules['BYMONTHDAY'] !== '')) {
+ $matchingDays = $this->getDaysOfYearMatchingByMonthDayRRule($rrules['BYMONTHDAY'], $frequencyRecurringDateTime);
+ }
+
+ if (isset($rrules['BYDAY']) && ($rrules['BYDAY'] !== array() && $rrules['BYDAY'] !== '')) {
+ if (isset($rrules['BYYEARDAY']) && ($rrules['BYYEARDAY'] !== '' && $rrules['BYYEARDAY'] !== array()) || isset($rrules['BYMONTHDAY']) && ($rrules['BYMONTHDAY'] !== '' && $rrules['BYMONTHDAY'] !== array()) || isset($rrules['BYWEEKNO']) && ($rrules['BYWEEKNO'] !== '' && $rrules['BYWEEKNO'] !== array())) {
+ $matchingDays = array_filter(
+ $this->getDaysOfYearMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime),
+ function ($yearDay) use ($matchingDays) {
+ return in_array($yearDay, $matchingDays);
+ }
+ );
+ } elseif ($matchingDays === array()) {
+ $matchingDays = $this->getDaysOfYearMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime);
+ }
+ }
+
+ if ($matchingDays === array()) {
+ $matchingDays = array($frequencyRecurringDateTime->format('z') + 1);
+ } else {
+ sort($matchingDays);
+ }
+
+ if (isset($rrules['BYSETPOS']) && ($rrules['BYSETPOS'] !== '' && $rrules['BYSETPOS'] !== array())) {
+ $matchingDays = $this->filterValuesUsingBySetPosRRule($rrules['BYSETPOS'], $matchingDays);
+ }
+
+ foreach ($matchingDays as $day) {
+ $clonedDateTime = clone $frequencyRecurringDateTime;
+ $candidateDateTimes[] = $clonedDateTime->setDate(
+ (int) $frequencyRecurringDateTime->format('Y'),
+ 1,
+ $day
+ );
+ }
+ break;
+ }
+
+ foreach ($candidateDateTimes as $candidate) {
+ $timestamp = $candidate->getTimestamp();
+ if ($timestamp <= $initialEventDate->getTimestamp()) {
+ continue;
+ }
+
+ if ($timestamp > $until) {
+ break;
+ }
+
+ // Exclusions
+ $isExcluded = array_filter($exdates, function ($exdate) use ($timestamp) {
+ return $exdate->getTimestamp() === $timestamp;
+ });
+
+ if (isset($this->alteredRecurrenceInstances[$anEvent['UID']])) {
+ if (in_array($timestamp, $this->alteredRecurrenceInstances[$anEvent['UID']])) {
+ $isExcluded = true;
+ }
+ }
+
+ if (!$isExcluded) {
+ $eventRecurrences[] = $candidate;
+ $this->eventCount++;
+ }
+
+ // Count all evaluated candidates including excluded ones,
+ // and if RRULE[COUNT] (if set) is reached then break.
+ $count++;
+ if ($count >= $countLimit) {
+ break 2;
+ }
+ }
+
+ // Move forwards $interval $frequency.
+ $monthPreMove = $frequencyRecurringDateTime->format('m');
+ $frequencyRecurringDateTime->modify("{$interval} {$this->frequencyConversion[$frequency]}");
+
+ // As noted in Example #2 on https://www.php.net/manual/en/datetime.modify.php,
+ // there are some occasions where adding months doesn't give the month you might
+ // expect. For instance: January 31st + 1 month == March 3rd (March 2nd on a leap
+ // year.) The following code crudely rectifies this.
+ if ($frequency === 'MONTHLY') {
+ $monthDiff = $frequencyRecurringDateTime->format('m') - $monthPreMove;
+
+ if (($monthDiff > 0 && $monthDiff > $interval) || ($monthDiff < 0 && $monthDiff > $interval - 12)) {
+ $frequencyRecurringDateTime->modify('-1 month');
+ }
+ }
+
+ // $monthDays is set in the DAILY frequency if the BYMONTHDAY stanza is present in
+ // the RRULE. The variable only needs to be updated when we change months, so we
+ // unset it here, prompting a recreation next iteration.
+ if (isset($monthDays) && $frequencyRecurringDateTime->format('m') !== $monthPreMove) {
+ unset($monthDays);
+ }
+ }
+
+ unset($monthDays); // Unset it here as well, so it doesn't bleed into the calculation of the next recurring event.
+
+ // Determine event length
+ $eventLength = 0;
+ if (isset($anEvent['DURATION'])) {
+ $clonedDateTime = clone $initialEventDate;
+ $endDate = $clonedDateTime->add($anEvent['DURATION_array'][2]);
+ $eventLength = $endDate->getTimestamp() - $anEvent['DTSTART_array'][2];
+ } elseif (isset($anEvent['DTEND_array'])) {
+ $eventLength = $anEvent['DTEND_array'][2] - $anEvent['DTSTART_array'][2];
+ }
+
+ // Whether or not the initial date was UTC
+ $initialDateWasUTC = substr($anEvent['DTSTART'], -1) === 'Z';
+
+ // Build the param array
+ $dateParamArray = array();
+ if (
+ !$initialDateWasUTC
+ && isset($anEvent['DTSTART_array'][0]['TZID'])
+ && $this->isValidTimeZoneId($anEvent['DTSTART_array'][0]['TZID'])
+ ) {
+ $dateParamArray['TZID'] = $anEvent['DTSTART_array'][0]['TZID'];
+ }
+
+ // Populate the `DT{START|END}[_array]`s
+ $eventRecurrences = array_map(
+ function ($recurringDatetime) use ($anEvent, $eventLength, $initialDateWasUTC, $dateParamArray) {
+ $tzidPrefix = (isset($dateParamArray['TZID'])) ? 'TZID=' . $this->escapeParamText($dateParamArray['TZID']) . ':' : '';
+
+ foreach (array('DTSTART', 'DTEND') as $dtkey) {
+ $anEvent[$dtkey] = $recurringDatetime->format(self::DATE_TIME_FORMAT) . (($initialDateWasUTC) ? 'Z' : '');
+
+ $anEvent["{$dtkey}_array"] = array(
+ $dateParamArray, // [0] Array of params (incl. TZID)
+ $anEvent[$dtkey], // [1] ICalDateTime string w/o TZID
+ $recurringDatetime->getTimestamp(), // [2] Unix Timestamp
+ "{$tzidPrefix}{$anEvent[$dtkey]}", // [3] Full ICalDateTime string
+ );
+
+ if ($dtkey !== 'DTEND') {
+ $recurringDatetime->modify("{$eventLength} seconds");
+ }
+ }
+
+ return $anEvent;
+ },
+ $eventRecurrences
+ );
+
+ $allEventRecurrences = array_merge($allEventRecurrences, $eventRecurrences);
+ }
+
+ // Nullify the initial events that are also EXDATEs
+ foreach ($eventKeysToRemove as $eventKeyToRemove) {
+ $events[$eventKeyToRemove] = null;
+ }
+
+ $events = array_merge($events, $allEventRecurrences);
+
+ $this->cal['VEVENT'] = $events;
+ }
+
+ /**
+ * Resolves values from indices of the range 1 -> $limit.
+ *
+ * For instance, if passed [1, 4, -16] and 28, this will return [1, 4, 13].
+ *
+ * @param array $indexes
+ * @param integer $limit
+ * @return array
+ */
+ protected function resolveIndicesOfRange(array $indexes, $limit)
+ {
+ $matching = array();
+ foreach ($indexes as $index) {
+ if ($index > 0 && $index <= $limit) {
+ $matching[] = $index;
+ } elseif ($index < 0 && -$index <= $limit) {
+ $matching[] = $index + $limit + 1;
+ }
+ }
+
+ sort($matching);
+
+ return $matching;
+ }
+
+ /**
+ * Find all days of a month that match the BYDAY stanza of an RRULE.
+ *
+ * With no {ordwk}, then return the day number of every {weekday}
+ * within the month.
+ *
+ * With a +ve {ordwk}, then return the {ordwk} {weekday} within the
+ * month.
+ *
+ * With a -ve {ordwk}, then return the {ordwk}-to-last {weekday}
+ * within the month.
+ *
+ * RRule Syntax:
+ * BYDAY={bywdaylist}
+ *
+ * Where:
+ * bywdaylist = {weekdaynum}[,{weekdaynum}...]
+ * weekdaynum = [[+]{ordwk} || -{ordwk}]{weekday}
+ * ordwk = 1 to 53
+ * weekday = SU || MO || TU || WE || TH || FR || SA
+ *
+ * @param array $byDays
+ * @param \DateTime $initialDateTime
+ * @return array
+ */
+ protected function getDaysOfMonthMatchingByDayRRule(array $byDays, $initialDateTime)
+ {
+ $matchingDays = array();
+ $currentMonth = $initialDateTime->format('n');
+
+ foreach ($byDays as $weekday) {
+ $bydayDateTime = clone $initialDateTime;
+
+ $ordwk = intval(substr($weekday, 0, -2));
+
+ // Quantise the date to the first instance of the requested day in a month
+ // (Or last if we have a -ve {ordwk})
+ $bydayDateTime->modify(
+ (($ordwk < 0) ? 'Last' : 'First') .
+ ' ' .
+ $this->weekdays[substr($weekday, -2)] . // e.g. "Monday"
+ ' of ' .
+ $initialDateTime->format('F') // e.g. "June"
+ );
+
+ if ($ordwk < 0) { // -ve {ordwk}
+ $bydayDateTime->modify((++$ordwk) . ' week');
+ if ($bydayDateTime->format('n') === $currentMonth) {
+ $matchingDays[] = $bydayDateTime->format('j');
+ }
+ } elseif ($ordwk > 0) { // +ve {ordwk}
+ $bydayDateTime->modify((--$ordwk) . ' week');
+ if ($bydayDateTime->format('n') === $currentMonth) {
+ $matchingDays[] = $bydayDateTime->format('j');
+ }
+ } else { // No {ordwk}
+ while ($bydayDateTime->format('n') === $initialDateTime->format('n')) {
+ $matchingDays[] = $bydayDateTime->format('j');
+ $bydayDateTime->modify('+1 week');
+ }
+ }
+ }
+
+ // Sort into ascending order
+ sort($matchingDays);
+
+ return $matchingDays;
+ }
+
+ /**
+ * Find all days of a month that match the BYMONTHDAY stanza of an RRULE.
+ *
+ * RRUle Syntax:
+ * BYMONTHDAY={bymodaylist}
+ *
+ * Where:
+ * bymodaylist = {monthdaynum}[,{monthdaynum}...]
+ * monthdaynum = ([+] || -) {ordmoday}
+ * ordmoday = 1 to 31
+ *
+ * @param array $byMonthDays
+ * @param \DateTime $initialDateTime
+ * @return array
+ */
+ protected function getDaysOfMonthMatchingByMonthDayRRule(array $byMonthDays, $initialDateTime)
+ {
+ return $this->resolveIndicesOfRange($byMonthDays, (int) $initialDateTime->format('t'));
+ }
+
+ /**
+ * Find all days of a year that match the BYDAY stanza of an RRULE.
+ *
+ * With no {ordwk}, then return the day number of every {weekday}
+ * within the year.
+ *
+ * With a +ve {ordwk}, then return the {ordwk} {weekday} within the
+ * year.
+ *
+ * With a -ve {ordwk}, then return the {ordwk}-to-last {weekday}
+ * within the year.
+ *
+ * RRule Syntax:
+ * BYDAY={bywdaylist}
+ *
+ * Where:
+ * bywdaylist = {weekdaynum}[,{weekdaynum}...]
+ * weekdaynum = [[+]{ordwk} || -{ordwk}]{weekday}
+ * ordwk = 1 to 53
+ * weekday = SU || MO || TU || WE || TH || FR || SA
+ *
+ * @param array $byDays
+ * @param \DateTime $initialDateTime
+ * @return array
+ */
+ protected function getDaysOfYearMatchingByDayRRule(array $byDays, $initialDateTime)
+ {
+ $matchingDays = array();
+
+ foreach ($byDays as $weekday) {
+ $bydayDateTime = clone $initialDateTime;
+
+ $ordwk = intval(substr($weekday, 0, -2));
+
+ // Quantise the date to the first instance of the requested day in a year
+ // (Or last if we have a -ve {ordwk})
+ $bydayDateTime->modify(
+ (($ordwk < 0) ? 'Last' : 'First') .
+ ' ' .
+ $this->weekdays[substr($weekday, -2)] . // e.g. "Monday"
+ ' of ' . (($ordwk < 0) ? 'December' : 'January') .
+ ' ' . $initialDateTime->format('Y') // e.g. "2018"
+ );
+
+ if ($ordwk < 0) { // -ve {ordwk}
+ $bydayDateTime->modify((++$ordwk) . ' week');
+ $matchingDays[] = $bydayDateTime->format('z') + 1;
+ } elseif ($ordwk > 0) { // +ve {ordwk}
+ $bydayDateTime->modify((--$ordwk) . ' week');
+ $matchingDays[] = $bydayDateTime->format('z') + 1;
+ } else { // No {ordwk}
+ while ($bydayDateTime->format('Y') === $initialDateTime->format('Y')) {
+ $matchingDays[] = $bydayDateTime->format('z') + 1;
+ $bydayDateTime->modify('+1 week');
+ }
+ }
+ }
+
+ // Sort into ascending order
+ sort($matchingDays);
+
+ return $matchingDays;
+ }
+
+ /**
+ * Find all days of a year that match the BYYEARDAY stanza of an RRULE.
+ *
+ * RRUle Syntax:
+ * BYYEARDAY={byyrdaylist}
+ *
+ * Where:
+ * byyrdaylist = {yeardaynum}[,{yeardaynum}...]
+ * yeardaynum = ([+] || -) {ordyrday}
+ * ordyrday = 1 to 366
+ *
+ * @param array $byYearDays
+ * @param \DateTime $initialDateTime
+ * @return array
+ */
+ protected function getDaysOfYearMatchingByYearDayRRule(array $byYearDays, $initialDateTime)
+ {
+ // `\DateTime::format('L')` returns 1 if leap year, 0 if not.
+ $daysInThisYear = $initialDateTime->format('L') ? 366 : 365;
+
+ return $this->resolveIndicesOfRange($byYearDays, $daysInThisYear);
+ }
+
+ /**
+ * Find all days of a year that match the BYWEEKNO stanza of an RRULE.
+ *
+ * Unfortunately, the RFC5545 specification does not specify exactly
+ * how BYWEEKNO should expand on the initial DTSTART when provided
+ * without any other stanzas.
+ *
+ * A comparison of expansions used by other ics parsers may be found
+ * at https://github.com/s0600204/ics-parser-1/wiki/byweekno
+ *
+ * This method uses the same expansion as the python-dateutil module.
+ *
+ * RRUle Syntax:
+ * BYWEEKNO={bywknolist}
+ *
+ * Where:
+ * bywknolist = {weeknum}[,{weeknum}...]
+ * weeknum = ([+] || -) {ordwk}
+ * ordwk = 1 to 53
+ *
+ * @param array $byWeekNums
+ * @param \DateTime $initialDateTime
+ * @return array
+ */
+ protected function getDaysOfYearMatchingByWeekNoRRule(array $byWeekNums, $initialDateTime)
+ {
+ // `\DateTime::format('L')` returns 1 if leap year, 0 if not.
+ $isLeapYear = $initialDateTime->format('L');
+ $firstDayOfTheYear = date_create("first day of January {$initialDateTime->format('Y')}")->format('D');
+ $weeksInThisYear = ($firstDayOfTheYear === 'Thu' || $isLeapYear && $firstDayOfTheYear === 'Wed') ? 53 : 52;
+
+ $matchingWeeks = $this->resolveIndicesOfRange($byWeekNums, $weeksInThisYear);
+ $matchingDays = array();
+ $byweekDateTime = clone $initialDateTime;
+ foreach ($matchingWeeks as $weekNum) {
+ $dayNum = $byweekDateTime->setISODate(
+ (int) $initialDateTime->format('Y'),
+ $weekNum,
+ 1
+ )->format('z') + 1;
+ for ($x = 0; $x < 7; ++$x) {
+ $matchingDays[] = $x + $dayNum;
+ }
+ }
+
+ sort($matchingDays);
+
+ return $matchingDays;
+ }
+
+ /**
+ * Find all days of a year that match the BYMONTHDAY stanza of an RRULE.
+ *
+ * RRule Syntax:
+ * BYMONTHDAY={bymodaylist}
+ *
+ * Where:
+ * bymodaylist = {monthdaynum}[,{monthdaynum}...]
+ * monthdaynum = ([+] || -) {ordmoday}
+ * ordmoday = 1 to 31
+ *
+ * @param array $byMonthDays
+ * @param \DateTime $initialDateTime
+ * @return array
+ */
+ protected function getDaysOfYearMatchingByMonthDayRRule(array $byMonthDays, $initialDateTime)
+ {
+ $matchingDays = array();
+ $monthDateTime = clone $initialDateTime;
+ for ($month = 1; $month < 13; $month++) {
+ $monthDateTime->setDate(
+ (int) $initialDateTime->format('Y'),
+ $month,
+ 1
+ );
+
+ $monthDays = $this->getDaysOfMonthMatchingByMonthDayRRule($byMonthDays, $monthDateTime);
+ foreach ($monthDays as $day) {
+ $matchingDays[] = $monthDateTime->setDate(
+ (int) $initialDateTime->format('Y'),
+ (int) $monthDateTime->format('m'),
+ $day
+ )->format('z') + 1;
+ }
+ }
+
+ return $matchingDays;
+ }
+
+ /**
+ * Filters a provided values-list by applying a BYSETPOS RRule.
+ *
+ * Where a +ve {daynum} is provided, the {ordday} position'd value as
+ * measured from the start of the list of values should be retained.
+ *
+ * Where a -ve {daynum} is provided, the {ordday} position'd value as
+ * measured from the end of the list of values should be retained.
+ *
+ * RRule Syntax:
+ * BYSETPOS={bysplist}
+ *
+ * Where:
+ * bysplist = {setposday}[,{setposday}...]
+ * setposday = {daynum}
+ * daynum = [+ || -] {ordday}
+ * ordday = 1 to 366
+ *
+ * @param array $bySetPos
+ * @param array $valuesList
+ * @return array
+ */
+ protected function filterValuesUsingBySetPosRRule(array $bySetPos, array $valuesList)
+ {
+ $filteredMatches = array();
+
+ foreach ($bySetPos as $setPosition) {
+ if ($setPosition < 0) {
+ $setPosition = count($valuesList) + ++$setPosition;
+ }
+
+ // Positioning starts at 1, array indexes start at 0
+ if (isset($valuesList[$setPosition - 1])) {
+ $filteredMatches[] = $valuesList[$setPosition - 1];
+ }
+ }
+
+ return $filteredMatches;
+ }
+
+ /**
+ * Processes date conversions using the time zone
+ *
+ * Add keys `DTSTART_tz` and `DTEND_tz` to each Event
+ * These keys contain dates adapted to the calendar
+ * time zone depending on the event `TZID`.
+ *
+ * @return void
+ * @throws \Exception
+ */
+ protected function processDateConversions()
+ {
+ $events = (isset($this->cal['VEVENT'])) ? $this->cal['VEVENT'] : array();
+
+ if (!empty($events)) {
+ foreach ($events as $key => $anEvent) {
+ if (is_null($anEvent) || !$this->isValidDate($anEvent['DTSTART'])) {
+ unset($events[$key]);
+ $this->eventCount--;
+
+ continue;
+ }
+
+ $events[$key]['DTSTART_tz'] = $this->iCalDateWithTimeZone($anEvent, 'DTSTART');
+
+ if ($this->iCalDateWithTimeZone($anEvent, 'DTEND')) {
+ $events[$key]['DTEND_tz'] = $this->iCalDateWithTimeZone($anEvent, 'DTEND');
+ } elseif ($this->iCalDateWithTimeZone($anEvent, 'DURATION')) {
+ $events[$key]['DTEND_tz'] = $this->iCalDateWithTimeZone($anEvent, 'DURATION');
+ } else {
+ $events[$key]['DTEND_tz'] = $events[$key]['DTSTART_tz'];
+ }
+ }
+
+ $this->cal['VEVENT'] = $events;
+ }
+ }
+
+ /**
+ * Returns an array of Events.
+ * Every event is a class with the event
+ * details being properties within it.
+ *
+ * @return array
+ */
+ public function events()
+ {
+ $array = $this->cal;
+ $array = isset($array['VEVENT']) ? $array['VEVENT'] : array();
+
+ $events = array();
+
+ if (!empty($array)) {
+ foreach ($array as $event) {
+ $events[] = new Event($event);
+ }
+ }
+
+ return $events;
+ }
+
+ /**
+ * Returns the calendar name
+ *
+ * @return string
+ */
+ public function calendarName()
+ {
+ return isset($this->cal['VCALENDAR']['X-WR-CALNAME']) ? $this->cal['VCALENDAR']['X-WR-CALNAME'] : '';
+ }
+
+ /**
+ * Returns the calendar description
+ *
+ * @return string
+ */
+ public function calendarDescription()
+ {
+ return isset($this->cal['VCALENDAR']['X-WR-CALDESC']) ? $this->cal['VCALENDAR']['X-WR-CALDESC'] : '';
+ }
+
+ /**
+ * Returns the calendar time zone
+ *
+ * @param boolean $ignoreUtc
+ * @return string|null
+ */
+ public function calendarTimeZone($ignoreUtc = false)
+ {
+ if (isset($this->cal['VCALENDAR']['X-WR-TIMEZONE'])) {
+ $timeZone = $this->cal['VCALENDAR']['X-WR-TIMEZONE'];
+ } elseif (isset($this->cal['VTIMEZONE']['TZID'])) {
+ $timeZone = $this->cal['VTIMEZONE']['TZID'];
+ } else {
+ $timeZone = $this->defaultTimeZone;
+ }
+
+ // Validate the time zone, falling back to the time zone set in the PHP environment.
+ $timeZone = $this->timeZoneStringToDateTimeZone($timeZone)->getName();
+
+ if ($ignoreUtc && strtoupper($timeZone) === self::TIME_ZONE_UTC) {
+ return null;
+ }
+
+ return $timeZone;
+ }
+
+ /**
+ * Returns an array of arrays with all free/busy events.
+ * Every event is an associative array and each property
+ * is an element it.
+ *
+ * @return array
+ */
+ public function freeBusyEvents()
+ {
+ $array = $this->cal;
+
+ return isset($array['VFREEBUSY']) ? $array['VFREEBUSY'] : array();
+ }
+
+ /**
+ * Returns a boolean value whether the
+ * current calendar has events or not
+ *
+ * @return boolean
+ */
+ public function hasEvents()
+ {
+ return ($this->events() !== array()) ?: false;
+ }
+
+ /**
+ * Returns a sorted array of the events in a given range,
+ * or an empty array if no events exist in the range.
+ *
+ * Events will be returned if the start or end date is contained within the
+ * range (inclusive), or if the event starts before and end after the range.
+ *
+ * If a start date is not specified or of a valid format, then the start
+ * of the range will default to the current time and date of the server.
+ *
+ * If an end date is not specified or of a valid format, then the end of
+ * the range will default to the current time and date of the server,
+ * plus 20 years.
+ *
+ * Note that this function makes use of Unix timestamps. This might be a
+ * problem for events on, during, or after 29 Jan 2038.
+ * See https://en.wikipedia.org/wiki/Unix_time#Representing_the_number
+ *
+ * @param string|null $rangeStart
+ * @param string|null $rangeEnd
+ * @return array
+ * @throws \Exception
+ */
+ public function eventsFromRange($rangeStart = null, $rangeEnd = null)
+ {
+ // Sort events before processing range
+ $events = $this->sortEventsWithOrder($this->events());
+
+ if (empty($events)) {
+ return array();
+ }
+
+ $extendedEvents = array();
+
+ if (!is_null($rangeStart)) {
+ try {
+ $rangeStart = new \DateTime($rangeStart, new \DateTimeZone($this->defaultTimeZone));
+ } catch (\Exception $exception) {
+ error_log("ICal::eventsFromRange: Invalid date passed ({$rangeStart})");
+ $rangeStart = false;
+ }
+ } else {
+ $rangeStart = new \DateTime('now', new \DateTimeZone($this->defaultTimeZone));
+ }
+
+ if (!is_null($rangeEnd)) {
+ try {
+ $rangeEnd = new \DateTime($rangeEnd, new \DateTimeZone($this->defaultTimeZone));
+ } catch (\Exception $exception) {
+ error_log("ICal::eventsFromRange: Invalid date passed ({$rangeEnd})");
+ $rangeEnd = false;
+ }
+ } else {
+ $rangeEnd = new \DateTime('now', new \DateTimeZone($this->defaultTimeZone));
+ $rangeEnd->modify('+20 years');
+ }
+
+ // If start and end are identical and are dates with no times...
+ if ($rangeEnd->format('His') == 0 && $rangeStart->getTimestamp() === $rangeEnd->getTimestamp()) {
+ $rangeEnd->modify('+1 day');
+ }
+
+ $rangeStart = $rangeStart->getTimestamp();
+ $rangeEnd = $rangeEnd->getTimestamp();
+
+ foreach ($events as $anEvent) {
+ $eventStart = $anEvent->dtstart_array[2];
+ $eventEnd = (isset($anEvent->dtend_array[2])) ? $anEvent->dtend_array[2] : null;
+
+ if (
+ ($eventStart >= $rangeStart && $eventStart < $rangeEnd) // Event start date contained in the range
+ || (
+ $eventEnd !== null
+ && (
+ ($eventEnd > $rangeStart && $eventEnd <= $rangeEnd) // Event end date contained in the range
+ || ($eventStart < $rangeStart && $eventEnd > $rangeEnd) // Event starts before and finishes after range
+ )
+ )
+ ) {
+ $extendedEvents[] = $anEvent;
+ }
+ }
+
+ if ($extendedEvents === array()) {
+ return array();
+ }
+
+ return $extendedEvents;
+ }
+
+ /**
+ * Returns a sorted array of the events following a given string
+ *
+ * @param string $interval
+ * @return array
+ */
+ public function eventsFromInterval($interval)
+ {
+ $rangeStart = new \DateTime('now', new \DateTimeZone($this->defaultTimeZone));
+ $rangeEnd = new \DateTime('now', new \DateTimeZone($this->defaultTimeZone));
+
+ $dateInterval = \DateInterval::createFromDateString($interval);
+ $rangeEnd->add($dateInterval);
+
+ return $this->eventsFromRange($rangeStart->format('Y-m-d'), $rangeEnd->format('Y-m-d'));
+ }
+
+ /**
+ * Sorts events based on a given sort order
+ *
+ * @param array $events
+ * @param integer $sortOrder Either SORT_ASC, SORT_DESC, SORT_REGULAR, SORT_NUMERIC, SORT_STRING
+ * @return array
+ */
+ public function sortEventsWithOrder(array $events, $sortOrder = SORT_ASC)
+ {
+ $extendedEvents = array();
+ $timestamp = array();
+
+ foreach ($events as $key => $anEvent) {
+ $extendedEvents[] = $anEvent;
+ $timestamp[$key] = $anEvent->dtstart_array[2];
+ }
+
+ array_multisort($timestamp, $sortOrder, $extendedEvents);
+
+ return $extendedEvents;
+ }
+
+ /**
+ * Checks if a time zone is valid (IANA, CLDR, or Windows)
+ *
+ * @param string $timeZone
+ * @return boolean
+ */
+ protected function isValidTimeZoneId($timeZone)
+ {
+ return $this->isValidIanaTimeZoneId($timeZone) !== false
+ || $this->isValidCldrTimeZoneId($timeZone) !== false
+ || $this->isValidWindowsTimeZoneId($timeZone) !== false;
+ }
+
+ /**
+ * Checks if a time zone is a valid IANA time zone
+ *
+ * @param string $timeZone
+ * @return boolean
+ */
+ protected function isValidIanaTimeZoneId($timeZone)
+ {
+ if (in_array($timeZone, $this->validIanaTimeZones)) {
+ return true;
+ }
+
+ $valid = array();
+ $tza = timezone_abbreviations_list();
+
+ foreach ($tza as $zone) {
+ foreach ($zone as $item) {
+ $valid[$item['timezone_id']] = true;
+ }
+ }
+
+ unset($valid['']);
+
+ if (isset($valid[$timeZone]) || in_array($timeZone, timezone_identifiers_list(\DateTimeZone::ALL_WITH_BC))) {
+ $this->validIanaTimeZones[] = $timeZone;
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks if a time zone is a valid CLDR time zone
+ *
+ * @param string $timeZone
+ * @return boolean
+ */
+ public function isValidCldrTimeZoneId($timeZone)
+ {
+ return array_key_exists(html_entity_decode($timeZone), self::$cldrTimeZonesMap);
+ }
+
+ /**
+ * Checks if a time zone is a recognised Windows (non-CLDR) time zone
+ *
+ * @param string $timeZone
+ * @return boolean
+ */
+ public function isValidWindowsTimeZoneId($timeZone)
+ {
+ return array_key_exists(html_entity_decode($timeZone), self::$windowsTimeZonesMap);
+ }
+
+ /**
+ * Parses a duration and applies it to a date
+ *
+ * @param string $date
+ * @param \DateInterval $duration
+ * @param string|null $format
+ * @return integer|\DateTime
+ */
+ protected function parseDuration($date, $duration, $format = self::UNIX_FORMAT)
+ {
+ $dateTime = date_create($date);
+ $dateTime->modify("{$duration->y} year");
+ $dateTime->modify("{$duration->m} month");
+ $dateTime->modify("{$duration->d} day");
+ $dateTime->modify("{$duration->h} hour");
+ $dateTime->modify("{$duration->i} minute");
+ $dateTime->modify("{$duration->s} second");
+
+ if (is_null($format)) {
+ $output = $dateTime;
+ } elseif ($format === self::UNIX_FORMAT) {
+ $output = $dateTime->getTimestamp();
+ } else {
+ $output = $dateTime->format($format);
+ }
+
+ return $output;
+ }
+
+ /**
+ * Removes unprintable ASCII and UTF-8 characters
+ *
+ * @param string $data
+ * @return string
+ */
+ protected function removeUnprintableChars($data)
+ {
+ return preg_replace('/[\x00-\x1F\x7F\xA0]/u', '', $data);
+ }
+
+ /**
+ * Provides a polyfill for PHP 7.2's `mb_chr()`, which is a multibyte safe version of `chr()`.
+ * Multibyte safe.
+ *
+ * @param integer $code
+ * @return string
+ */
+ protected function mb_chr($code) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
+ {
+ if (function_exists('mb_chr')) {
+ return mb_chr($code);
+ } else {
+ if (($code %= 0x200000) < 0x80) {
+ $s = chr($code);
+ } elseif ($code < 0x800) {
+ $s = chr(0xc0 | $code >> 6) . chr(0x80 | $code & 0x3f);
+ } elseif ($code < 0x10000) {
+ $s = chr(0xe0 | $code >> 12) . chr(0x80 | $code >> 6 & 0x3f) . chr(0x80 | $code & 0x3f);
+ } else {
+ $s = chr(0xf0 | $code >> 18) . chr(0x80 | $code >> 12 & 0x3f) . chr(0x80 | $code >> 6 & 0x3f) . chr(0x80 | $code & 0x3f);
+ }
+
+ return $s;
+ }
+ }
+
+ /**
+ * Places double-quotes around texts that have characters not permitted
+ * in parameter-texts, but are permitted in quoted-texts.
+ *
+ * @param string $candidateText
+ * @return string
+ */
+ protected function escapeParamText($candidateText)
+ {
+ if (strpbrk($candidateText, ':;,') !== false) {
+ return '"' . $candidateText . '"';
+ }
+
+ return $candidateText;
+ }
+
+ /**
+ * Replace curly quotes and other special characters with their standard equivalents
+ * @see https://utf8-chartable.de/unicode-utf8-table.pl?start=8211&utf8=string-literal
+ *
+ * @param string $input
+ * @return string
+ */
+ protected function cleanCharacters($input)
+ {
+ return strtr(
+ $input,
+ array(
+ "\xe2\x80\x98" => "'", // ‘
+ "\xe2\x80\x99" => "'", // ’
+ "\xe2\x80\x9a" => "'", // ‚
+ "\xe2\x80\x9b" => "'", // ‛
+ "\xe2\x80\x9c" => '"', // “
+ "\xe2\x80\x9d" => '"', // ”
+ "\xe2\x80\x9e" => '"', // „
+ "\xe2\x80\x9f" => '"', // ‟
+ "\xe2\x80\x93" => '-', // –
+ "\xe2\x80\x94" => '--', // —
+ "\xe2\x80\xa6" => '...', // …
+ $this->mb_chr(145) => "'", // ‘
+ $this->mb_chr(146) => "'", // ’
+ $this->mb_chr(147) => '"', // “
+ $this->mb_chr(148) => '"', // ”
+ $this->mb_chr(150) => '-', // –
+ $this->mb_chr(151) => '--', // —
+ $this->mb_chr(133) => '...', // …
+ )
+ );
+ }
+
+ /**
+ * Parses a list of excluded dates
+ * to be applied to an Event
+ *
+ * @param array $event
+ * @return array
+ */
+ public function parseExdates(array $event)
+ {
+ if (empty($event['EXDATE_array'])) {
+ return array();
+ } else {
+ $exdates = $event['EXDATE_array'];
+ }
+
+ $output = array();
+ $currentTimeZone = new \DateTimeZone($this->defaultTimeZone);
+
+ foreach ($exdates as $subArray) {
+ end($subArray);
+ $finalKey = key($subArray);
+
+ foreach (array_keys($subArray) as $key) {
+ if ($key === 'TZID') {
+ $currentTimeZone = $this->timeZoneStringToDateTimeZone($subArray[$key]);
+ } elseif (is_numeric($key)) {
+ $icalDate = $subArray[$key];
+
+ if (substr($icalDate, -1) === 'Z') {
+ $currentTimeZone = new \DateTimeZone(self::TIME_ZONE_UTC);
+ }
+
+ $output[] = new \DateTime($icalDate, $currentTimeZone);
+
+ if ($key === $finalKey) {
+ // Reset to default
+ $currentTimeZone = new \DateTimeZone($this->defaultTimeZone);
+ }
+ }
+ }
+ }
+
+ return $output;
+ }
+
+ /**
+ * Checks if a date string is a valid date
+ *
+ * @param string $value
+ * @return boolean
+ * @throws \Exception
+ */
+ public function isValidDate($value)
+ {
+ if (!$value) {
+ return false;
+ }
+
+ try {
+ new \DateTime($value);
+
+ return true;
+ } catch (\Exception $exception) {
+ return false;
+ }
+ }
+
+ /**
+ * Checks if a filename exists as a file or URL
+ *
+ * @param string $filename
+ * @return boolean
+ */
+ protected function isFileOrUrl($filename)
+ {
+ return (file_exists($filename) || filter_var($filename, FILTER_VALIDATE_URL)) ?: false;
+ }
+
+ /**
+ * Reads an entire file or URL into an array
+ *
+ * @param string $filename
+ * @return array
+ * @throws \Exception
+ */
+ protected function fileOrUrl($filename)
+ {
+ $options = array();
+ $options['http'] = array();
+ $options['http']['header'] = array();
+
+ if (!empty($this->httpBasicAuth) || !empty($this->httpUserAgent) || !empty($this->httpAcceptLanguage)) {
+ if (!empty($this->httpBasicAuth)) {
+ $username = $this->httpBasicAuth['username'];
+ $password = $this->httpBasicAuth['password'];
+ $basicAuth = base64_encode("{$username}:{$password}");
+
+ $options['http']['header'][] = "Authorization: Basic {$basicAuth}";
+ }
+
+ if (!empty($this->httpUserAgent)) {
+ $options['http']['header'][] = "User-Agent: {$this->httpUserAgent}";
+ }
+
+ if (!empty($this->httpAcceptLanguage)) {
+ $options['http']['header'][] = "Accept-language: {$this->httpAcceptLanguage}";
+ }
+ }
+
+ if (empty($this->httpUserAgent)) {
+ if (mb_stripos($filename, 'outlook.office365.com') !== false) {
+ $options['http']['header'][] = 'User-Agent: A User Agent';
+ }
+ }
+
+ if (!empty($this->httpProtocolVersion)) {
+ $options['http']['protocol_version'] = $this->httpProtocolVersion;
+ } else {
+ $options['http']['protocol_version'] = '1.1';
+ }
+
+ $options['http']['header'][] = 'Connection: close';
+
+ $context = stream_context_create($options);
+
+ // phpcs:ignore CustomPHPCS.ControlStructures.AssignmentInCondition
+ if (($lines = file($filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES, $context)) === false) {
+ throw new \Exception("The file path or URL '{$filename}' does not exist.");
+ }
+
+ return $lines;
+ }
+
+ /**
+ * Returns a `DateTimeZone` object based on a string containing a time zone name.
+ * Falls back to the default time zone if string passed not a recognised time zone.
+ *
+ * @param string $timeZoneString
+ * @return \DateTimeZone
+ */
+ public function timeZoneStringToDateTimeZone($timeZoneString)
+ {
+ // Some time zones contain characters that are not permitted in param-texts,
+ // but are within quoted texts. We need to remove the quotes as they're not
+ // actually part of the time zone.
+ $timeZoneString = trim($timeZoneString, '"');
+ $timeZoneString = html_entity_decode($timeZoneString);
+
+ if ($this->isValidIanaTimeZoneId($timeZoneString)) {
+ return new \DateTimeZone($timeZoneString);
+ }
+
+ if ($this->isValidCldrTimeZoneId($timeZoneString)) {
+ return new \DateTimeZone(self::$cldrTimeZonesMap[$timeZoneString]);
+ }
+
+ if ($this->isValidWindowsTimeZoneId($timeZoneString)) {
+ return new \DateTimeZone(self::$windowsTimeZonesMap[$timeZoneString]);
+ }
+
+ return new \DateTimeZone($this->defaultTimeZone);
+ }
+}
diff --git a/vendor/johngrogg/ics-parser/tests/CleanCharacterTest.php b/vendor/johngrogg/ics-parser/tests/CleanCharacterTest.php
new file mode 100644
index 0000000..8ceca1b
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/tests/CleanCharacterTest.php
@@ -0,0 +1,32 @@
+getMethod($name);
+
+ // < PHP 8.1.0
+ $method->setAccessible(true);
+
+ return $method;
+ }
+
+ public function testCleanCharacters()
+ {
+ $ical = new ICal();
+ $input = 'Test with emoji 🔴👍🏻';
+
+ self::assertSame(
+ self::getMethod('cleanCharacters')->invokeArgs($ical, array($input)),
+ $input
+ );
+ }
+}
diff --git a/vendor/johngrogg/ics-parser/tests/DynamicPropertiesTest.php b/vendor/johngrogg/ics-parser/tests/DynamicPropertiesTest.php
new file mode 100644
index 0000000..c683642
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/tests/DynamicPropertiesTest.php
@@ -0,0 +1,24 @@
+events() as $event) {
+ $this->assertTrue(isset($event->dtstart_array));
+ $this->assertTrue(isset($event->dtend_array));
+ $this->assertTrue(isset($event->dtstamp_array));
+ $this->assertTrue(isset($event->uid_array));
+ $this->assertTrue(isset($event->created_array));
+ $this->assertTrue(isset($event->last_modified_array));
+ $this->assertTrue(isset($event->summary_array));
+ }
+ }
+}
diff --git a/vendor/johngrogg/ics-parser/tests/KeyValueTest.php b/vendor/johngrogg/ics-parser/tests/KeyValueTest.php
new file mode 100644
index 0000000..6bbbe6e
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/tests/KeyValueTest.php
@@ -0,0 +1,86 @@
+ 'ATTENDEE',
+ 1 => array(
+ 0 => 'mailto:julien@ag.com',
+ 1 => array(
+ 'PARTSTAT' => 'TENTATIVE',
+ 'CN' => 'ju: @ag.com = Ju ; ',
+ ),
+ ),
+ );
+
+ $this->assertLines(
+ 'ATTENDEE;PARTSTAT=TENTATIVE;CN="ju: @ag.com = Ju ; ":mailto:julien@ag.com',
+ $checks
+ );
+ }
+
+ public function testUtf8Characters()
+ {
+ $checks = array(
+ 0 => 'ATTENDEE',
+ 1 => array(
+ 0 => 'mailto:juëǯ@ag.com',
+ 1 => array(
+ 'PARTSTAT' => 'TENTATIVE',
+ 'CN' => 'juëǯĻ',
+ ),
+ ),
+ );
+
+ $this->assertLines(
+ 'ATTENDEE;PARTSTAT=TENTATIVE;CN=juëǯĻ:mailto:juëǯ@ag.com',
+ $checks
+ );
+
+ $checks = array(
+ 0 => 'SUMMARY',
+ 1 => ' I love emojis 😀😁😁 ë, ǯ, Ļ',
+ );
+
+ $this->assertLines(
+ 'SUMMARY: I love emojis 😀😁😁 ë, ǯ, Ļ',
+ $checks
+ );
+ }
+
+ public function testParametersOfKeysWithMultipleValues()
+ {
+ $checks = array(
+ 0 => 'ATTENDEE',
+ 1 => array(
+ 0 => 'mailto:jsmith@example.com',
+ 1 => array(
+ 'DELEGATED-TO' => array(
+ 0 => 'mailto:jdoe@example.com',
+ 1 => 'mailto:jqpublic@example.com',
+ ),
+ ),
+ ),
+ );
+
+ $this->assertLines(
+ 'ATTENDEE;DELEGATED-TO="mailto:jdoe@example.com","mailto:jqpublic@example.com":mailto:jsmith@example.com',
+ $checks
+ );
+ }
+
+ private function assertLines($lines, array $checks)
+ {
+ $ical = new ICal();
+
+ self::assertSame($ical->keyValueFromString($lines), $checks);
+ }
+}
diff --git a/vendor/johngrogg/ics-parser/tests/RecurrencesTest.php b/vendor/johngrogg/ics-parser/tests/RecurrencesTest.php
new file mode 100644
index 0000000..add13eb
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/tests/RecurrencesTest.php
@@ -0,0 +1,580 @@
+originalTimeZone = date_default_timezone_get();
+ }
+
+ /**
+ * @after
+ */
+ public function tearDownFixtures()
+ {
+ date_default_timezone_set($this->originalTimeZone);
+ }
+
+ public function testYearlyFullDayTimeZoneBerlin()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20000301', 'message' => '1st event, CET: '),
+ array('index' => 1, 'dateString' => '20010301T000000', 'message' => '2nd event, CET: '),
+ array('index' => 2, 'dateString' => '20020301T000000', 'message' => '3rd event, CET: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ array(
+ 'DTSTART;VALUE=DATE:20000301',
+ 'DTEND;VALUE=DATE:20000302',
+ 'RRULE:FREQ=YEARLY;WKST=SU;COUNT=3',
+ ),
+ 3,
+ $checks
+ );
+ }
+
+ public function testMonthlyFullDayTimeZoneBerlin()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20000301', 'message' => '1st event, CET: '),
+ array('index' => 1, 'dateString' => '20000401T000000', 'message' => '2nd event, CEST: '),
+ array('index' => 2, 'dateString' => '20000501T000000', 'message' => '3rd event, CEST: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ array(
+ 'DTSTART;VALUE=DATE:20000301',
+ 'DTEND;VALUE=DATE:20000302',
+ 'RRULE:FREQ=MONTHLY;BYMONTHDAY=1;WKST=SU;COUNT=3',
+ ),
+ 3,
+ $checks
+ );
+ }
+
+ public function testMonthlyFullDayTimeZoneBerlinSummerTime()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20180701', 'message' => '1st event, CEST: '),
+ array('index' => 1, 'dateString' => '20180801T000000', 'message' => '2nd event, CEST: '),
+ array('index' => 2, 'dateString' => '20180901T000000', 'message' => '3rd event, CEST: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ array(
+ 'DTSTART;VALUE=DATE:20180701',
+ 'DTEND;VALUE=DATE:20180702',
+ 'RRULE:FREQ=MONTHLY;WKST=SU;COUNT=3',
+ ),
+ 3,
+ $checks
+ );
+ }
+
+ public function testMonthlyFullDayTimeZoneBerlinFromFile()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20180701', 'message' => '1st event, CEST: '),
+ array('index' => 1, 'dateString' => '20180801T000000', 'message' => '2nd event, CEST: '),
+ array('index' => 2, 'dateString' => '20180901T000000', 'message' => '3rd event, CEST: '),
+ );
+ $this->assertEventFile(
+ 'Europe/Berlin',
+ './tests/ical/ical-monthly.ics',
+ 25,
+ $checks
+ );
+ }
+
+ public function testIssue196FromFile()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20191105T190000', 'timezone' => 'Europe/Berlin', 'message' => '1st event, CEST: '),
+ array('index' => 1, 'dateString' => '20191106T190000', 'timezone' => 'Europe/Berlin', 'message' => '2nd event, CEST: '),
+ array('index' => 2, 'dateString' => '20191107T190000', 'timezone' => 'Europe/Berlin', 'message' => '3rd event, CEST: '),
+ array('index' => 3, 'dateString' => '20191108T190000', 'timezone' => 'Europe/Berlin', 'message' => '4th event, CEST: '),
+ array('index' => 4, 'dateString' => '20191109T170000', 'timezone' => 'Europe/Berlin', 'message' => '5th event, CEST: '),
+ array('index' => 5, 'dateString' => '20191110T180000', 'timezone' => 'Europe/Berlin', 'message' => '6th event, CEST: '),
+ );
+ $this->assertEventFile(
+ 'UTC',
+ './tests/ical/issue-196.ics',
+ 7,
+ $checks
+ );
+ }
+
+ public function testWeeklyFullDayTimeZoneBerlin()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20000301', 'message' => '1st event, CET: '),
+ array('index' => 1, 'dateString' => '20000308T000000', 'message' => '2nd event, CET: '),
+ array('index' => 2, 'dateString' => '20000315T000000', 'message' => '3rd event, CET: '),
+ array('index' => 3, 'dateString' => '20000322T000000', 'message' => '4th event, CET: '),
+ array('index' => 4, 'dateString' => '20000329T000000', 'message' => '5th event, CEST: '),
+ array('index' => 5, 'dateString' => '20000405T000000', 'message' => '6th event, CEST: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ array(
+ 'DTSTART;VALUE=DATE:20000301',
+ 'DTEND;VALUE=DATE:20000302',
+ 'RRULE:FREQ=WEEKLY;WKST=SU;COUNT=6',
+ ),
+ 6,
+ $checks
+ );
+ }
+
+ public function testDailyFullDayTimeZoneBerlin()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20000301', 'message' => '1st event, CET: '),
+ array('index' => 1, 'dateString' => '20000302T000000', 'message' => '2nd event, CET: '),
+ array('index' => 30, 'dateString' => '20000331T000000', 'message' => '31st event, CEST: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ array(
+ 'DTSTART;VALUE=DATE:20000301',
+ 'DTEND;VALUE=DATE:20000302',
+ 'RRULE:FREQ=DAILY;WKST=SU;COUNT=31',
+ ),
+ 31,
+ $checks
+ );
+ }
+
+ public function testWeeklyFullDayTimeZoneBerlinLocal()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20000301T000000', 'message' => '1st event, CET: '),
+ array('index' => 1, 'dateString' => '20000308T000000', 'message' => '2nd event, CET: '),
+ array('index' => 2, 'dateString' => '20000315T000000', 'message' => '3rd event, CET: '),
+ array('index' => 3, 'dateString' => '20000322T000000', 'message' => '4th event, CET: '),
+ array('index' => 4, 'dateString' => '20000329T000000', 'message' => '5th event, CEST: '),
+ array('index' => 5, 'dateString' => '20000405T000000', 'message' => '6th event, CEST: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ array(
+ 'DTSTART;TZID=Europe/Berlin:20000301T000000',
+ 'DTEND;TZID=Europe/Berlin:20000302T000000',
+ 'RRULE:FREQ=WEEKLY;WKST=SU;COUNT=6',
+ ),
+ 6,
+ $checks
+ );
+ }
+
+ public function testRFCDaily10NewYork()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'timezone' => 'America/New_York', 'message' => '1st event, EDT: '),
+ array('index' => 1, 'dateString' => '19970903T090000', 'timezone' => 'America/New_York', 'message' => '2nd event, EDT: '),
+ array('index' => 9, 'dateString' => '19970911T090000', 'timezone' => 'America/New_York', 'message' => '10th event, EDT: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=DAILY;COUNT=10',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ public function testRFCDaily10Berlin()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'timezone' => 'Europe/Berlin', 'message' => '1st event, CEST: '),
+ array('index' => 1, 'dateString' => '19970903T090000', 'timezone' => 'Europe/Berlin', 'message' => '2nd event, CEST: '),
+ array('index' => 9, 'dateString' => '19970911T090000', 'timezone' => 'Europe/Berlin', 'message' => '10th event, CEST: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ array(
+ 'DTSTART;TZID=Europe/Berlin:19970902T090000',
+ 'RRULE:FREQ=DAILY;COUNT=10',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ public function testStartDateIsExdateUsingUntil()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20190918T095000', 'timezone' => 'Europe/London', 'message' => '1st event: '),
+ array('index' => 1, 'dateString' => '20191002T095000', 'timezone' => 'Europe/London', 'message' => '2nd event: '),
+ array('index' => 2, 'dateString' => '20191016T095000', 'timezone' => 'Europe/London', 'message' => '3rd event: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/London',
+ array(
+ 'DTSTART;TZID=Europe/London:20190911T095000',
+ 'RRULE:FREQ=WEEKLY;WKST=SU;UNTIL=20191027T235959Z;BYDAY=WE',
+ 'EXDATE;TZID=Europe/London:20191023T095000',
+ 'EXDATE;TZID=Europe/London:20191009T095000',
+ 'EXDATE;TZID=Europe/London:20190925T095000',
+ 'EXDATE;TZID=Europe/London:20190911T095000',
+ ),
+ 3,
+ $checks
+ );
+ }
+
+ public function testStartDateIsExdateUsingCount()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20190918T095000', 'timezone' => 'Europe/London', 'message' => '1st event: '),
+ array('index' => 1, 'dateString' => '20191002T095000', 'timezone' => 'Europe/London', 'message' => '2nd event: '),
+ array('index' => 2, 'dateString' => '20191016T095000', 'timezone' => 'Europe/London', 'message' => '3rd event: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/London',
+ array(
+ 'DTSTART;TZID=Europe/London:20190911T095000',
+ 'RRULE:FREQ=WEEKLY;WKST=SU;COUNT=7;BYDAY=WE',
+ 'EXDATE;TZID=Europe/London:20191023T095000',
+ 'EXDATE;TZID=Europe/London:20191009T095000',
+ 'EXDATE;TZID=Europe/London:20190925T095000',
+ 'EXDATE;TZID=Europe/London:20190911T095000',
+ ),
+ 3,
+ $checks
+ );
+ }
+
+ public function testCountWithExdate()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20200323T050000', 'timezone' => 'Europe/Paris', 'message' => '1st event: '),
+ array('index' => 1, 'dateString' => '20200324T050000', 'timezone' => 'Europe/Paris', 'message' => '2nd event: '),
+ array('index' => 2, 'dateString' => '20200327T050000', 'timezone' => 'Europe/Paris', 'message' => '3rd event: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/London',
+ array(
+ 'DTSTART;TZID=Europe/Paris:20200323T050000',
+ 'DTEND;TZID=Europe/Paris:20200323T070000',
+ 'RRULE:FREQ=DAILY;COUNT=5',
+ 'EXDATE;TZID=Europe/Paris:20200326T050000',
+ 'EXDATE;TZID=Europe/Paris:20200325T050000',
+ 'DTSTAMP:20200318T141057Z',
+ ),
+ 3,
+ $checks
+ );
+ }
+
+ public function testRFCDaily10BerlinFromNewYork()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'timezone' => 'Europe/Berlin', 'message' => '1st event, CEST: '),
+ array('index' => 1, 'dateString' => '19970903T090000', 'timezone' => 'Europe/Berlin', 'message' => '2nd event, CEST: '),
+ array('index' => 9, 'dateString' => '19970911T090000', 'timezone' => 'Europe/Berlin', 'message' => '10th event, CEST: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=Europe/Berlin:19970902T090000',
+ 'RRULE:FREQ=DAILY;COUNT=10',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ public function testExdatesInDifferentTimezone()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20170503T190000', 'message' => '1st event: '),
+ array('index' => 1, 'dateString' => '20170510T190000', 'message' => '2nd event: '),
+ array('index' => 9, 'dateString' => '20170712T190000', 'message' => '10th event: '),
+ array('index' => 19, 'dateString' => '20171004T190000', 'message' => '20th event: '),
+ );
+ $this->assertVEVENT(
+ 'America/Chicago',
+ array(
+ 'DTSTART;TZID=America/Chicago:20170503T190000',
+ 'RRULE:FREQ=WEEKLY;BYDAY=WE;WKST=SU;UNTIL=20180101',
+ 'EXDATE:20170601T000000Z',
+ 'EXDATE:20170803T000000Z',
+ 'EXDATE:20170824T000000Z',
+ 'EXDATE:20171026T000000Z',
+ 'EXDATE:20171102T000000Z',
+ 'EXDATE:20171123T010000Z',
+ 'EXDATE:20171221T010000Z',
+ ),
+ 28,
+ $checks
+ );
+ }
+
+ public function testYearlyWithBySetPos()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970306T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970313T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970325T090000', 'message' => '3rd occurrence: '),
+ array('index' => 3, 'dateString' => '19980305T090000', 'message' => '4th occurrence: '),
+ array('index' => 4, 'dateString' => '19980312T090000', 'message' => '5th occurrence: '),
+ array('index' => 5, 'dateString' => '19980326T090000', 'message' => '6th occurrence: '),
+ array('index' => 9, 'dateString' => '20000307T090000', 'message' => '10th occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970306T090000',
+ 'RRULE:FREQ=YEARLY;COUNT=10;BYMONTH=3;BYDAY=TU,TH;BYSETPOS=2,4,-2',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ public function testDailyWithByMonthDay()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20000206T120000', 'message' => '1st event: '),
+ array('index' => 1, 'dateString' => '20000211T120000', 'message' => '2nd event: '),
+ array('index' => 2, 'dateString' => '20000216T120000', 'message' => '3rd event: '),
+ array('index' => 4, 'dateString' => '20000226T120000', 'message' => '5th event, transition from February to March: '),
+ array('index' => 5, 'dateString' => '20000301T120000', 'message' => '6th event, transition to March from February: '),
+ array('index' => 11, 'dateString' => '20000331T120000', 'message' => '12th event, transition from March to April: '),
+ array('index' => 12, 'dateString' => '20000401T120000', 'message' => '13th event, transition to April from March: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ array(
+ 'DTSTART:20000206T120000',
+ 'DTEND:20000206T130000',
+ 'RRULE:FREQ=DAILY;BYMONTHDAY=1,6,11,16,21,26,31;COUNT=16',
+ ),
+ 16,
+ $checks
+ );
+ }
+
+ public function testYearlyWithByMonthDay()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20001214T120000', 'message' => '1st event: '),
+ array('index' => 1, 'dateString' => '20001221T120000', 'message' => '2nd event: '),
+ array('index' => 2, 'dateString' => '20010107T120000', 'message' => '3rd event: '),
+ array('index' => 3, 'dateString' => '20010114T120000', 'message' => '4th event: '),
+ array('index' => 6, 'dateString' => '20010214T120000', 'message' => '7th event: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ array(
+ 'DTSTART:20001214T120000',
+ 'DTEND:20001214T130000',
+ 'RRULE:FREQ=YEARLY;BYMONTHDAY=7,14,21;COUNT=8',
+ ),
+ 8,
+ $checks
+ );
+ }
+
+ public function testYearlyWithByMonthDayAndByDay()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20001214T120000', 'message' => '1st event: '),
+ array('index' => 1, 'dateString' => '20001221T120000', 'message' => '2nd event: '),
+ array('index' => 2, 'dateString' => '20010607T120000', 'message' => '3rd event: '),
+ array('index' => 3, 'dateString' => '20010614T120000', 'message' => '4th event: '),
+ array('index' => 6, 'dateString' => '20020214T120000', 'message' => '7th event: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ array(
+ 'DTSTART:20001214T120000',
+ 'DTEND:20001214T130000',
+ 'RRULE:FREQ=YEARLY;BYMONTHDAY=7,14,21;BYDAY=TH;COUNT=8',
+ ),
+ 8,
+ $checks
+ );
+ }
+
+ public function testYearlyWithByMonthAndByMonthDay()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20001214T120000', 'message' => '1st event: '),
+ array('index' => 1, 'dateString' => '20001221T120000', 'message' => '2nd event: '),
+ array('index' => 2, 'dateString' => '20010607T120000', 'message' => '3rd event: '),
+ array('index' => 3, 'dateString' => '20010614T120000', 'message' => '4th event: '),
+ array('index' => 6, 'dateString' => '20011214T120000', 'message' => '7th event: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ array(
+ 'DTSTART:20001214T120000',
+ 'DTEND:20001214T130000',
+ 'RRULE:FREQ=YEARLY;BYMONTH=12,6;BYMONTHDAY=7,14,21;COUNT=8',
+ ),
+ 8,
+ $checks
+ );
+ }
+
+ public function testCountIsOne()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20211201T090000', 'message' => '1st and only expected event: '),
+ );
+ $this->assertVEVENT(
+ 'UTC',
+ array(
+ 'DTSTART:20211201T090000',
+ 'DTEND:20211201T100000',
+ 'RRULE:FREQ=DAILY;COUNT=1',
+ ),
+ 1,
+ $checks
+ );
+ }
+
+ public function test5thByDayOfMonth()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20200103T090000', 'message' => '1st event: '),
+ array('index' => 1, 'dateString' => '20200129T090000', 'message' => '2nd event: '),
+ array('index' => 2, 'dateString' => '20200429T090000', 'message' => '3rd event: '),
+ array('index' => 3, 'dateString' => '20200501T090000', 'message' => '4th event: '),
+ array('index' => 4, 'dateString' => '20200703T090000', 'message' => '5th event: '),
+ array('index' => 5, 'dateString' => '20200729T090000', 'message' => '6th event: '),
+ array('index' => 6, 'dateString' => '20200930T090000', 'message' => '7th event: '),
+ array('index' => 7, 'dateString' => '20201002T090000', 'message' => '8th event: '),
+ array('index' => 8, 'dateString' => '20201230T090000', 'message' => '9th event: '),
+ array('index' => 9, 'dateString' => '20210101T090000', 'message' => '10th and last event: '),
+ );
+ $this->assertVEVENT(
+ 'UTC',
+ array(
+ 'DTSTART:20200103T090000',
+ 'DTEND:20200103T100000',
+ 'RRULE:FREQ=MONTHLY;BYDAY=5WE,-5FR;UNTIL=20210102T090000',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ public function assertVEVENT($defaultTimezone, $veventParts, $count, $checks)
+ {
+ $options = $this->getOptions($defaultTimezone);
+
+ $testIcal = implode(PHP_EOL, $this->getIcalHeader());
+ $testIcal .= PHP_EOL;
+ $testIcal .= implode(PHP_EOL, $this->formatIcalEvent($veventParts));
+ $testIcal .= PHP_EOL;
+ $testIcal .= implode(PHP_EOL, $this->getIcalFooter());
+
+ $ical = new ICal(false, $options);
+ $ical->initString($testIcal);
+
+ $events = $ical->events();
+
+ $this->assertCount($count, $events);
+
+ foreach ($checks as $check) {
+ $this->assertEvent($events[$check['index']], $check['dateString'], $check['message'], isset($check['timezone']) ? $check['timezone'] : $defaultTimezone);
+ }
+ }
+
+ public function assertEventFile($defaultTimezone, $file, $count, $checks)
+ {
+ $options = $this->getOptions($defaultTimezone);
+
+ $ical = new ICal($file, $options);
+
+ $events = $ical->events();
+
+ $this->assertCount($count, $events);
+
+ $events = $ical->sortEventsWithOrder($events);
+
+ foreach ($checks as $check) {
+ $this->assertEvent($events[$check['index']], $check['dateString'], $check['message'], isset($check['timezone']) ? $check['timezone'] : $defaultTimezone);
+ }
+ }
+
+ public function assertEvent($event, $expectedDateString, $message, $timeZone = null)
+ {
+ if (!is_null($timeZone)) {
+ date_default_timezone_set($timeZone);
+ }
+
+ $expectedTimeStamp = strtotime($expectedDateString);
+
+ $this->assertSame($expectedTimeStamp, $event->dtstart_array[2], $message . 'timestamp mismatch (expected ' . $expectedDateString . ' vs actual ' . $event->dtstart . ')');
+ $this->assertSame($expectedDateString, $event->dtstart, $message . 'dtstart mismatch (timestamp is okay)');
+ }
+
+ public function getOptions($defaultTimezone)
+ {
+ $options = array(
+ 'defaultSpan' => 2, // Default value
+ 'defaultTimeZone' => $defaultTimezone, // Default value: UTC
+ 'defaultWeekStart' => 'MO', // Default value
+ 'disableCharacterReplacement' => false, // Default value
+ 'filterDaysAfter' => null, // Default value
+ 'filterDaysBefore' => null, // Default value
+ 'httpUserAgent' => null, // Default value
+ 'skipRecurrence' => false, // Default value
+ );
+
+ return $options;
+ }
+
+ public function formatIcalEvent($veventParts)
+ {
+ return array_merge(
+ array(
+ 'BEGIN:VEVENT',
+ 'CREATED:' . gmdate('Ymd\THis\Z'),
+ 'UID:M2CD-1-1-5FB000FB-BBE4-4F3F-9E7E-217F1FF97209',
+ ),
+ $veventParts,
+ array(
+ 'SUMMARY:test',
+ 'LAST-MODIFIED:' . gmdate('Ymd\THis\Z', filemtime(__FILE__)),
+ 'END:VEVENT',
+ )
+ );
+ }
+
+ public function getIcalHeader()
+ {
+ return array(
+ 'BEGIN:VCALENDAR',
+ 'VERSION:2.0',
+ 'PRODID:-//Google Inc//Google Calendar 70.9054//EN',
+ 'X-WR-CALNAME:Private',
+ 'X-APPLE-CALENDAR-COLOR:#FF2968',
+ 'X-WR-CALDESC:',
+ );
+ }
+
+ public function getIcalFooter()
+ {
+ return array('END:VCALENDAR');
+ }
+}
diff --git a/vendor/johngrogg/ics-parser/tests/Rfc5545RecurrenceTest.php b/vendor/johngrogg/ics-parser/tests/Rfc5545RecurrenceTest.php
new file mode 100644
index 0000000..a617445
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/tests/Rfc5545RecurrenceTest.php
@@ -0,0 +1,1059 @@
+originalTimeZone = date_default_timezone_get();
+ }
+
+ /**
+ * @after
+ */
+ public function tearDownFixtures()
+ {
+ date_default_timezone_set($this->originalTimeZone);
+ }
+
+ // Page 123, Test 1 :: Daily, 10 Occurrences
+ public function test_page123_test1()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970903T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970904T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=DAILY;COUNT=10',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ // Page 123, Test 2 :: Daily, until December 24th
+ public function test_page123_test2()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970903T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970904T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=DAILY;UNTIL=19971224T000000Z',
+ ),
+ 113,
+ $checks
+ );
+ }
+
+ // Page 123, Test 3 :: Daily, until December 24th, with trailing semicolon
+ public function test_page123_test3()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970903T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970904T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=DAILY;UNTIL=19971224T000000Z;',
+ ),
+ 113,
+ $checks
+ );
+ }
+
+ // Page 124, Test 1 :: Daily, every other day, Forever
+ //
+ // UNTIL rule does not exist in original example
+ public function test_page124_test1()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970904T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970906T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=DAILY;INTERVAL=2;UNTIL=19971201Z',
+ ),
+ 45,
+ $checks
+ );
+ }
+
+ // Page 124, Test 2 :: Daily, 10-day intervals, 5 occurrences
+ public function test_page124_test2()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970912T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970922T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5',
+ ),
+ 5,
+ $checks
+ );
+ }
+
+ // Page 124, Test 3a :: Every January day, for 3 years (Variant A)
+ public function test_page124_test3a()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19980101T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19980102T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19980103T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19980101T090000',
+ 'RRULE:FREQ=YEARLY;UNTIL=20000131T140000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA',
+ ),
+ 93,
+ $checks
+ );
+ }
+
+ /* Requires support for BYMONTH under DAILY [No ticket]
+ *
+ // Page 124, Test 3b :: Every January day, for 3 years (Variant B)
+ public function test_page124_test3b()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19980101T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19980102T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19980103T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19980101T090000',
+ 'RRULE:FREQ=DAILY;UNTIL=20000131T140000Z;BYMONTH=1',
+ ),
+ 93,
+ $checks
+ );
+ }
+ */
+
+ // Page 124, Test 4 :: Weekly, 10 occurrences
+ public function test_page124_test4()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970909T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970916T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=WEEKLY;COUNT=10',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ // Page 125, Test 1 :: Weekly, until December 24th
+ public function test_page125_test1()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970909T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970916T090000', 'message' => '3rd occurrence: '),
+ array('index' => 16, 'dateString' => '19971223T090000', 'message' => 'last occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=WEEKLY;UNTIL=19971224T000000Z',
+ ),
+ 17,
+ $checks
+ );
+ }
+
+ // Page 125, Test 2 :: Every other week, forever
+ //
+ // UNTIL rule does not exist in original example
+ public function test_page125_test2()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970916T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970930T090000', 'message' => '3rd occurrence: '),
+ array('index' => 3, 'dateString' => '19971014T090000', 'message' => '4th occurrence: '),
+ array('index' => 4, 'dateString' => '19971028T090000', 'message' => '5th occurrence: '),
+ array('index' => 5, 'dateString' => '19971111T090000', 'message' => '6th occurrence: '),
+ array('index' => 6, 'dateString' => '19971125T090000', 'message' => '7th occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=WEEKLY;INTERVAL=2;WKST=SU;UNTIL=19971201Z',
+ ),
+ 7,
+ $checks
+ );
+ }
+
+ // Page 125, Test 3a :: Tuesday & Thursday every week, for five weeks (Variant A)
+ public function test_page125_test3a()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970904T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970909T090000', 'message' => '3rd occurrence: '),
+ array('index' => 9, 'dateString' => '19971002T090000', 'message' => 'final occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ // Page 125, Test 3b :: Tuesday & Thursday every week, for five weeks (Variant B)
+ public function test_page125_test3b()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970904T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970909T090000', 'message' => '3rd occurrence: '),
+ array('index' => 9, 'dateString' => '19971002T090000', 'message' => 'final occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=WEEKLY;COUNT=10;WKST=SU;BYDAY=TU,TH',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ // Page 125, Test 4 :: Monday, Wednesday & Friday of every other week until December 24th
+ public function test_page125_test4()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970901T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970903T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970905T090000', 'message' => '3rd occurrence: '),
+ array('index' => 24, 'dateString' => '19971222T090000', 'message' => 'final occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970901T090000',
+ 'RRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR',
+ ),
+ 25,
+ $checks
+ );
+ }
+
+ // Page 126, Test 1 :: Tuesday & Thursday, every other week, for 8 occurrences
+ public function test_page126_test1()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970904T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970916T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH',
+ ),
+ 8,
+ $checks
+ );
+ }
+
+ // Page 126, Test 2 :: First Friday of the Month, for 10 occurrences
+ public function test_page126_test2()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970905T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19971003T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19971107T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970905T090000',
+ 'RRULE:FREQ=MONTHLY;COUNT=10;BYDAY=1FR',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ // Page 126, Test 3 :: First Friday of the Month, until 24th December
+ public function test_page126_test3()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970905T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19971003T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19971107T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970905T090000',
+ 'RRULE:FREQ=MONTHLY;UNTIL=19971224T000000Z;BYDAY=1FR',
+ ),
+ 4,
+ $checks
+ );
+ }
+
+ // Page 126, Test 4 :: First and last Sunday, every other Month, for 10 occurrences
+ public function test_page126_test4()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970907T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970928T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19971102T090000', 'message' => '3rd occurrence: '),
+ array('index' => 3, 'dateString' => '19971130T090000', 'message' => '4th occurrence: '),
+ array('index' => 4, 'dateString' => '19980104T090000', 'message' => '5th occurrence: '),
+ array('index' => 5, 'dateString' => '19980125T090000', 'message' => '6th occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970907T090000',
+ 'RRULE:FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ // Page 126, Test 5 :: Second-to-last Monday of the Month, for six months
+ public function test_page126_test5()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970922T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19971020T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19971117T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970922T090000',
+ 'RRULE:FREQ=MONTHLY;COUNT=6;BYDAY=-2MO',
+ ),
+ 6,
+ $checks
+ );
+ }
+
+ // Page 127, Test 1 :: Third-to-last day of the month, forever
+ //
+ // UNTIL rule does not exist in original example.
+ public function test_page127_test1()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970928T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19971029T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19971128T090000', 'message' => '3rd occurrence: '),
+ array('index' => 3, 'dateString' => '19971229T090000', 'message' => '4th occurrence: '),
+ array('index' => 4, 'dateString' => '19980129T090000', 'message' => '5th occurrence: '),
+ array('index' => 5, 'dateString' => '19980226T090000', 'message' => '6th occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970928T090000',
+ 'RRULE:FREQ=MONTHLY;BYMONTHDAY=-3;UNTIL=19980401',
+ ),
+ 7,
+ $checks
+ );
+ }
+
+ // Page 127, Test 2 :: 2nd and 15th of each Month, for 10 occurrences
+ public function test_page127_test2()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970915T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19971002T090000', 'message' => '3rd occurrence: '),
+ array('index' => 3, 'dateString' => '19971015T090000', 'message' => '4th occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ // Page 127, Test 3 :: First and last day of the month, for 10 occurrences
+ public function test_page127_test3()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970930T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19971001T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19971031T090000', 'message' => '3rd occurrence: '),
+ array('index' => 3, 'dateString' => '19971101T090000', 'message' => '4th occurrence: '),
+ array('index' => 4, 'dateString' => '19971130T090000', 'message' => '5th occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970930T090000',
+ 'RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ // Page 127, Test 4 :: 10th through 15th, every 18 months, for 10 occurrences
+ public function test_page127_test4()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970910T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970911T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970912T090000', 'message' => '3rd occurrence: '),
+ array('index' => 6, 'dateString' => '19990310T090000', 'message' => '7th occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970910T090000',
+ 'RRULE:FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ // Page 127, Test 5 :: Every Tuesday, every other Month, forever
+ //
+ // UNTIL rule does not exist in original example.
+ public function test_page127_test5()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970909T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970916T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=MONTHLY;INTERVAL=2;BYDAY=TU;UNTIL=19980101',
+ ),
+ 9,
+ $checks
+ );
+ }
+
+ // Page 128, Test 1 :: June & July of each Year, for 10 occurrences
+ public function test_page128_test1()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970610T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970710T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19980610T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970610T090000',
+ 'RRULE:FREQ=YEARLY;COUNT=10;BYMONTH=6,7',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ // Page 128, Test 2 :: January, February, & March, every other Year, for 10 occurrences
+ public function test_page128_test2()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970310T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19990110T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19990210T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970310T090000',
+ 'RRULE:FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ // Page 128, Test 3 :: Every third Year on the 1st, 100th, & 200th day for 10 occurrences
+ public function test_page128_test3()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970101T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970410T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970719T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970101T090000',
+ 'RRULE:FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ // Page 128, Test 4 :: 20th Monday of a Year, forever
+ //
+ // COUNT rule does not exist in original example.
+ public function test_page128_test4()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970519T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19980518T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19990517T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970519T090000',
+ 'RRULE:FREQ=YEARLY;BYDAY=20MO;COUNT=4',
+ ),
+ 4,
+ $checks
+ );
+ }
+
+ // Page 129, Test 1 :: Monday of Week 20, where the default start of the week is Monday, forever
+ //
+ // COUNT rule does not exist in original example.
+ public function test_page129_test1()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970512T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19980511T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19990517T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970512T090000',
+ 'RRULE:FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO;COUNT=4',
+ ),
+ 4,
+ $checks
+ );
+ }
+
+ // Page 129, Test 2 :: Every Thursday in March, forever
+ //
+ // UNTIL rule does not exist in original example.
+ public function test_page129_test2()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970313T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970320T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970327T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970313T090000',
+ 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=TH;UNTIL=19990401Z',
+ ),
+ 11,
+ $checks
+ );
+ }
+
+ // Page 129, Test 3 :: Every Thursday in June, July, & August, forever
+ //
+ // UNTIL rule does not exist in original example.
+ public function test_page129_test3()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970605T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970612T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970619T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970605T090000',
+ 'RRULE:FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8;UNTIL=19970901Z',
+ ),
+ 13,
+ $checks
+ );
+ }
+
+ /* Requires support for BYMONTHDAY and BYDAY in the same MONTHLY RRULE [No ticket]
+ *
+ // Page 129, Test 4 :: Every Friday 13th, forever
+ //
+ // COUNT rule does not exist in original example.
+ public function test_page129_test4()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19980213T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19980313T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19981113T090000', 'message' => '3rd occurrence: '),
+ array('index' => 3, 'dateString' => '19990813T090000', 'message' => '4th occurrence: '),
+ array('index' => 4, 'dateString' => '20001013T090000', 'message' => '5th occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'EXDATE;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13;COUNT=5',
+ ),
+ 5,
+ $checks
+ );
+ }
+ */
+
+ // Page 130, Test 1 :: The first Saturday that follows the first Sunday of the month, forever:
+ //
+ // COUNT rule does not exist in original example.
+ public function test_page130_test1()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970913T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19971011T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19971108T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970913T090000',
+ 'RRULE:FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13;COUNT=7',
+ ),
+ 7,
+ $checks
+ );
+ }
+
+ // Page 130, Test 2 :: The first Tuesday after a Monday in November, every 4 Years (U.S. Presidential Election Day), forever
+ //
+ // COUNT rule does not exist in original example.
+ public function test_page130_test2()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19961105T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '20001107T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '20041102T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19961105T090000',
+ 'RRULE:FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8;COUNT=4',
+ ),
+ 4,
+ $checks
+ );
+ }
+
+ // Page 130, Test 3 :: Third instance of either a Tuesday, Wednesday, or Thursday of a Month, for 3 months.
+ public function test_page130_test3()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970904T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19971007T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19971106T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970904T090000',
+ 'RRULE:FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3',
+ ),
+ 3,
+ $checks
+ );
+ }
+
+ // Page 130, Test 4 :: Second-to-last weekday of the month, indefinitely
+ //
+ // UNTIL rule does not exist in original example.
+ public function test_page130_test4()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970929T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19971030T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19971127T090000', 'message' => '3rd occurrence: '),
+ array('index' => 3, 'dateString' => '19971230T090000', 'message' => '4th occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970929T090000',
+ 'RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2;UNTIL=19980101',
+ ),
+ 4,
+ $checks
+ );
+ }
+
+ /* Requires support of HOURLY frequency [#101]
+ *
+ // Page 131, Test 1 :: Every 3 hours from 09:00 to 17:00 on a specific day
+ public function test_page131_test1()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970902T120000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970902T150000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T170000Z',
+ ),
+ 3,
+ $checks
+ );
+ }
+ */
+
+ /* Requires support of MINUTELY frequency [#101]
+ *
+ // Page 131, Test 2 :: Every 15 minutes for 6 occurrences
+ public function test_page131_test2()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970902T091500', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970902T093000', 'message' => '3rd occurrence: '),
+ array('index' => 3, 'dateString' => '19970902T094500', 'message' => '4th occurrence: '),
+ array('index' => 4, 'dateString' => '19970902T100000', 'message' => '5th occurrence: '),
+ array('index' => 5, 'dateString' => '19970902T101500', 'message' => '6th occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=MINUTELY;INTERVAL=15;COUNT=6',
+ ),
+ 6,
+ $checks
+ );
+ }
+ */
+
+ /* Requires support of MINUTELY frequency [#101]
+ *
+ // Page 131, Test 3 :: Every hour and a half for 4 occurrences
+ public function test_page131_test3()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970902T103000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970902T120000', 'message' => '3rd occurrence: '),
+ array('index' => 3, 'dateString' => '19970902T133000', 'message' => '4th occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=MINUTELY;INTERVAL=90;COUNT=4',
+ ),
+ 4,
+ $checks
+ );
+ }
+ */
+
+ /* Requires support of BYHOUR and BYMINUTE under DAILY [#11]
+ *
+ // Page 131, Test 4a :: Every 20 minutes from 9:00 to 16:40 every day, using DAILY
+ //
+ // UNTIL rule does not exist in original example
+ public function test_page131_test4a()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence, Day 1: '),
+ array('index' => 1, 'dateString' => '19970902T092000', 'message' => '2nd occurrence, Day 1: '),
+ array('index' => 2, 'dateString' => '19970902T094000', 'message' => '3rd occurrence, Day 1: '),
+ array('index' => 3, 'dateString' => '19970902T100000', 'message' => '4th occurrence, Day 1: '),
+ array('index' => 20, 'dateString' => '19970902T164000', 'message' => 'Last occurrence, Day 1: '),
+ array('index' => 21, 'dateString' => '19970903T090000', 'message' => '1st occurrence, Day 2: '),
+ array('index' => 41, 'dateString' => '19970903T164000', 'message' => 'Last occurrence, Day 2: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40;UNTIL=19970904T000000Z',
+ ),
+ 42,
+ $checks
+ );
+ }
+ */
+
+ /* Requires support of MINUTELY frequency [#101]
+ *
+ // Page 131, Test 4b :: Every 20 minutes from 9:00 to 16:40 every day, using MINUTELY
+ //
+ // UNTIL rule does not exist in original example
+ public function test_page131_test4b()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence, Day 1: '),
+ array('index' => 1, 'dateString' => '19970902T092000', 'message' => '2nd occurrence, Day 1: '),
+ array('index' => 2, 'dateString' => '19970902T094000', 'message' => '3rd occurrence, Day 1: '),
+ array('index' => 3, 'dateString' => '19970902T100000', 'message' => '4th occurrence, Day 1: '),
+ array('index' => 20, 'dateString' => '19970902T164000', 'message' => 'Last occurrence, Day 1: '),
+ array('index' => 21, 'dateString' => '19970903T090000', 'message' => '1st occurrence, Day 2: '),
+ array('index' => 41, 'dateString' => '19970903T164000', 'message' => 'Last occurrence, Day 2: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=MINUTELY;INTERVAL=20;BYHOUR=9,10,11,12,13,14,15,16;UNTIL=19970904T000000Z',
+ ),
+ 42,
+ $checks
+ );
+ }
+ */
+
+ // Page 131, Test 5a :: Changing the passed WKST rule, before...
+ public function test_page131_test5a()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970805T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970810T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970819T090000', 'message' => '3rd occurrence: '),
+ array('index' => 3, 'dateString' => '19970824T090000', 'message' => '4th occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970805T090000',
+ 'RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO',
+ ),
+ 4,
+ $checks
+ );
+ }
+
+ // Page 131, Test 5b :: ...and after
+ public function test_page131_test5b()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970805T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970817T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970819T090000', 'message' => '3rd occurrence: '),
+ array('index' => 3, 'dateString' => '19970831T090000', 'message' => '4th occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970805T090000',
+ 'RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU',
+ ),
+ 4,
+ $checks
+ );
+ }
+
+ // Page 132, Test 1 :: Automatically ignoring an invalid date (30 February)
+ public function test_page132_test1()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20070115T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '20070130T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '20070215T090000', 'message' => '3rd occurrence: '),
+ array('index' => 3, 'dateString' => '20070315T090000', 'message' => '4th occurrence: '),
+ array('index' => 4, 'dateString' => '20070330T090000', 'message' => '5th occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:20070115T090000',
+ 'RRULE:FREQ=MONTHLY;BYMONTHDAY=15,30;COUNT=5',
+ ),
+ 5,
+ $checks
+ );
+ }
+
+ public function assertVEVENT($defaultTimezone, $veventParts, $count, $checks)
+ {
+ $options = $this->getOptions($defaultTimezone);
+
+ $testIcal = implode(PHP_EOL, $this->getIcalHeader());
+ $testIcal .= PHP_EOL;
+ $testIcal .= implode(PHP_EOL, $this->formatIcalEvent($veventParts));
+ $testIcal .= PHP_EOL;
+ $testIcal .= implode(PHP_EOL, $this->getIcalFooter());
+
+ $ical = new ICal(false, $options);
+ $ical->initString($testIcal);
+
+ $events = $ical->events();
+
+ $this->assertCount($count, $events);
+
+ foreach ($checks as $check) {
+ $this->assertEvent($events[$check['index']], $check['dateString'], $check['message'], isset($check['timezone']) ? $check['timezone'] : $defaultTimezone);
+ }
+ }
+
+ public function assertEvent($event, $expectedDateString, $message, $timeZone = null)
+ {
+ if (!is_null($timeZone)) {
+ date_default_timezone_set($timeZone);
+ }
+
+ $expectedTimeStamp = strtotime($expectedDateString);
+
+ $this->assertSame($expectedTimeStamp, $event->dtstart_array[2], $message . 'timestamp mismatch (expected ' . $expectedDateString . ' vs actual ' . $event->dtstart . ')');
+ $this->assertSame($expectedDateString, $event->dtstart, $message . 'dtstart mismatch (timestamp is okay)');
+ }
+
+ public function getOptions($defaultTimezone)
+ {
+ $options = array(
+ 'defaultSpan' => 2, // Default value: 2
+ 'defaultTimeZone' => $defaultTimezone, // Default value: UTC
+ 'defaultWeekStart' => 'MO', // Default value
+ 'disableCharacterReplacement' => false, // Default value
+ 'filterDaysAfter' => null, // Default value
+ 'filterDaysBefore' => null, // Default value
+ 'httpUserAgent' => null, // Default value
+ 'skipRecurrence' => false, // Default value
+ );
+
+ return $options;
+ }
+
+ public function formatIcalEvent($veventParts)
+ {
+ return array_merge(
+ array(
+ 'BEGIN:VEVENT',
+ 'CREATED:' . gmdate('Ymd\THis\Z'),
+ 'UID:RFC5545-examples-test',
+ ),
+ $veventParts,
+ array(
+ 'SUMMARY:test',
+ 'LAST-MODIFIED:' . gmdate('Ymd\THis\Z', filemtime(__FILE__)),
+ 'END:VEVENT',
+ )
+ );
+ }
+
+ public function getIcalHeader()
+ {
+ return array(
+ 'BEGIN:VCALENDAR',
+ 'VERSION:2.0',
+ 'PRODID:-//Google Inc//Google Calendar 70.9054//EN',
+ 'X-WR-CALNAME:Private',
+ 'X-APPLE-CALENDAR-COLOR:#FF2968',
+ 'X-WR-CALDESC:',
+ );
+ }
+
+ public function getIcalFooter()
+ {
+ return array('END:VCALENDAR');
+ }
+}
diff --git a/vendor/johngrogg/ics-parser/tests/SingleEventsTest.php b/vendor/johngrogg/ics-parser/tests/SingleEventsTest.php
new file mode 100644
index 0000000..fc89c67
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/tests/SingleEventsTest.php
@@ -0,0 +1,509 @@
+originalTimeZone = date_default_timezone_get();
+ }
+
+ /**
+ * @after
+ */
+ public function tearDownFixtures()
+ {
+ date_default_timezone_set($this->originalTimeZone);
+ }
+
+ public function testFullDayTimeZoneBerlin()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20000301', 'message' => '1st event, CET: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ 'DTSTART;VALUE=DATE:20000301',
+ 'DTEND;VALUE=DATE:20000302',
+ 1,
+ $checks
+ );
+ }
+
+ public function testSeveralFullDaysTimeZoneBerlin()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20000301', 'message' => '1st event, CET: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ 'DTSTART;VALUE=DATE:20000301',
+ 'DTEND;VALUE=DATE:20000304',
+ 1,
+ $checks
+ );
+ }
+
+ public function testEventTimeZoneUTC()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20180626T070000Z', 'message' => '1st event, UTC: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ 'DTSTART:20180626T070000Z',
+ 'DTEND:20180626T110000Z',
+ 1,
+ $checks
+ );
+ }
+
+ public function testEventTimeZoneBerlin()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20180626T070000', 'message' => '1st event, CEST: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ 'DTSTART:20180626T070000',
+ 'DTEND:20180626T110000',
+ 1,
+ $checks
+ );
+ }
+
+ public function assertVEVENT($defaultTimezone, $dtstart, $dtend, $count, $checks)
+ {
+ $options = $this->getOptions($defaultTimezone);
+
+ $testIcal = implode(PHP_EOL, $this->getIcalHeader());
+ $testIcal .= PHP_EOL;
+ $testIcal .= implode(PHP_EOL, $this->formatIcalEvent($dtstart, $dtend));
+ $testIcal .= PHP_EOL;
+ $testIcal .= implode(PHP_EOL, $this->getIcalTimezones());
+ $testIcal .= PHP_EOL;
+ $testIcal .= implode(PHP_EOL, $this->getIcalFooter());
+
+ date_default_timezone_set('UTC');
+
+ $ical = new ICal(false, $options);
+ $ical->initString($testIcal);
+
+ $events = $ical->events();
+
+ $this->assertCount($count, $events);
+
+ foreach ($checks as $check) {
+ $this->assertEvent(
+ $events[$check['index']],
+ $check['dateString'],
+ $check['message'],
+ isset($check['timezone']) ? $check['timezone'] : $defaultTimezone
+ );
+ }
+ }
+
+ public function getOptions($defaultTimezone)
+ {
+ $options = array(
+ 'defaultSpan' => 2, // Default value
+ 'defaultTimeZone' => $defaultTimezone, // Default value: UTC
+ 'defaultWeekStart' => 'MO', // Default value
+ 'disableCharacterReplacement' => false, // Default value
+ 'filterDaysAfter' => null, // Default value
+ 'filterDaysBefore' => null, // Default value
+ 'httpUserAgent' => null, // Default value
+ 'skipRecurrence' => false, // Default value
+ );
+
+ return $options;
+ }
+
+ public function getIcalHeader()
+ {
+ return array(
+ 'BEGIN:VCALENDAR',
+ 'VERSION:2.0',
+ 'PRODID:-//Google Inc//Google Calendar 70.9054//EN',
+ 'X-WR-CALNAME:Private',
+ 'X-APPLE-CALENDAR-COLOR:#FF2968',
+ 'X-WR-CALDESC:',
+ );
+ }
+
+ public function formatIcalEvent($dtstart, $dtend)
+ {
+ return array(
+ 'BEGIN:VEVENT',
+ 'CREATED:20090213T195947Z',
+ 'UID:M2CD-1-1-5FB000FB-BBE4-4F3F-9E7E-217F1FF97209',
+ $dtstart,
+ $dtend,
+ 'SUMMARY:test',
+ 'DESCRIPTION;LANGUAGE=en-gb:This is a short description\nwith a new line. Some "special" \'s',
+ ' igns\' may be interesting\, too.',
+ ' And a non-breaking space.',
+ 'LAST-MODIFIED:20110429T222101Z',
+ 'DTSTAMP:20170630T105724Z',
+ 'SEQUENCE:0',
+ 'END:VEVENT',
+ );
+ }
+
+ public function getIcalTimezones()
+ {
+ return array(
+ 'BEGIN:VTIMEZONE',
+ 'TZID:Europe/Berlin',
+ 'X-LIC-LOCATION:Europe/Berlin',
+ 'BEGIN:STANDARD',
+ 'DTSTART:18930401T000000',
+ 'RDATE:18930401T000000',
+ 'TZNAME:CEST',
+ 'TZOFFSETFROM:+005328',
+ 'TZOFFSETTO:+0100',
+ 'END:STANDARD',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19160430T230000',
+ 'RDATE:19160430T230000',
+ 'RDATE:19400401T020000',
+ 'RDATE:19430329T020000',
+ 'RDATE:19460414T020000',
+ 'RDATE:19470406T030000',
+ 'RDATE:19480418T020000',
+ 'RDATE:19490410T020000',
+ 'RDATE:19800406T020000',
+ 'TZNAME:CEST',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0200',
+ 'END:DAYLIGHT',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19161001T010000',
+ 'RDATE:19161001T010000',
+ 'RDATE:19421102T030000',
+ 'RDATE:19431004T030000',
+ 'RDATE:19441002T030000',
+ 'RDATE:19451118T030000',
+ 'RDATE:19461007T030000',
+ 'TZNAME:CET',
+ 'TZOFFSETFROM:+0200',
+ 'TZOFFSETTO:+0100',
+ 'END:STANDARD',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19170416T020000',
+ 'RRULE:FREQ=YEARLY;UNTIL=19180415T010000Z;BYMONTH=4;BYDAY=3MO',
+ 'TZNAME:CEST',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0200',
+ 'END:DAYLIGHT',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19170917T030000',
+ 'RRULE:FREQ=YEARLY;UNTIL=19180916T010000Z;BYMONTH=9;BYDAY=3MO',
+ 'TZNAME:CET',
+ 'TZOFFSETFROM:+0200',
+ 'TZOFFSETTO:+0100',
+ 'END:STANDARD',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19440403T020000',
+ 'RRULE:FREQ=YEARLY;UNTIL=19450402T010000Z;BYMONTH=4;BYDAY=1MO',
+ 'TZNAME:CEST',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0200',
+ 'END:DAYLIGHT',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19450524T020000',
+ 'RDATE:19450524T020000',
+ 'RDATE:19470511T030000',
+ 'TZNAME:CEMT',
+ 'TZOFFSETFROM:+0200',
+ 'TZOFFSETTO:+0300',
+ 'END:DAYLIGHT',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19450924T030000',
+ 'RDATE:19450924T030000',
+ 'RDATE:19470629T030000',
+ 'TZNAME:CEST',
+ 'TZOFFSETFROM:+0300',
+ 'TZOFFSETTO:+0200',
+ 'END:DAYLIGHT',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19460101T000000',
+ 'RDATE:19460101T000000',
+ 'RDATE:19800101T000000',
+ 'TZNAME:CEST',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0100',
+ 'END:STANDARD',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19471005T030000',
+ 'RRULE:FREQ=YEARLY;UNTIL=19491002T010000Z;BYMONTH=10;BYDAY=1SU',
+ 'TZNAME:CET',
+ 'TZOFFSETFROM:+0200',
+ 'TZOFFSETTO:+0100',
+ 'END:STANDARD',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19800928T030000',
+ 'RRULE:FREQ=YEARLY;UNTIL=19950924T010000Z;BYMONTH=9;BYDAY=-1SU',
+ 'TZNAME:CET',
+ 'TZOFFSETFROM:+0200',
+ 'TZOFFSETTO:+0100',
+ 'END:STANDARD',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19810329T020000',
+ 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU',
+ 'TZNAME:CEST',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0200',
+ 'END:DAYLIGHT',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19961027T030000',
+ 'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU',
+ 'TZNAME:CET',
+ 'TZOFFSETFROM:+0200',
+ 'TZOFFSETTO:+0100',
+ 'END:STANDARD',
+ 'END:VTIMEZONE',
+ 'BEGIN:VTIMEZONE',
+ 'TZID:Europe/Paris',
+ 'X-LIC-LOCATION:Europe/Paris',
+ 'BEGIN:STANDARD',
+ 'DTSTART:18910315T000100',
+ 'RDATE:18910315T000100',
+ 'TZNAME:PMT',
+ 'TZOFFSETFROM:+000921',
+ 'TZOFFSETTO:+000921',
+ 'END:STANDARD',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19110311T000100',
+ 'RDATE:19110311T000100',
+ 'TZNAME:WEST',
+ 'TZOFFSETFROM:+000921',
+ 'TZOFFSETTO:+0000',
+ 'END:STANDARD',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19160614T230000',
+ 'RDATE:19160614T230000',
+ 'RDATE:19170324T230000',
+ 'RDATE:19180309T230000',
+ 'RDATE:19190301T230000',
+ 'RDATE:19200214T230000',
+ 'RDATE:19210314T230000',
+ 'RDATE:19220325T230000',
+ 'RDATE:19230526T230000',
+ 'RDATE:19240329T230000',
+ 'RDATE:19250404T230000',
+ 'RDATE:19260417T230000',
+ 'RDATE:19270409T230000',
+ 'RDATE:19280414T230000',
+ 'RDATE:19290420T230000',
+ 'RDATE:19300412T230000',
+ 'RDATE:19310418T230000',
+ 'RDATE:19320402T230000',
+ 'RDATE:19330325T230000',
+ 'RDATE:19340407T230000',
+ 'RDATE:19350330T230000',
+ 'RDATE:19360418T230000',
+ 'RDATE:19370403T230000',
+ 'RDATE:19380326T230000',
+ 'RDATE:19390415T230000',
+ 'RDATE:19400225T020000',
+ 'TZNAME:WEST',
+ 'TZOFFSETFROM:+0000',
+ 'TZOFFSETTO:+0100',
+ 'END:DAYLIGHT',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19161002T000000',
+ 'RRULE:FREQ=YEARLY;UNTIL=19191005T230000Z;BYMONTH=10;BYMONTHDAY=2,3,4,5,6,',
+ ' 7,8;BYDAY=MO',
+ 'TZNAME:WET',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0000',
+ 'END:STANDARD',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19201024T000000',
+ 'RDATE:19201024T000000',
+ 'RDATE:19211026T000000',
+ 'RDATE:19391119T000000',
+ 'TZNAME:WET',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0000',
+ 'END:STANDARD',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19221008T000000',
+ 'RRULE:FREQ=YEARLY;UNTIL=19381001T230000Z;BYMONTH=10;BYMONTHDAY=2,3,4,5,6,',
+ ' 7,8;BYDAY=SU',
+ 'TZNAME:WET',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0000',
+ 'END:STANDARD',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19400614T230000',
+ 'RDATE:19400614T230000',
+ 'TZNAME:CEST',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0200',
+ 'END:STANDARD',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19421102T030000',
+ 'RDATE:19421102T030000',
+ 'RDATE:19431004T030000',
+ 'RDATE:19760926T010000',
+ 'RDATE:19770925T030000',
+ 'RDATE:19781001T030000',
+ 'TZNAME:CET',
+ 'TZOFFSETFROM:+0200',
+ 'TZOFFSETTO:+0100',
+ 'END:STANDARD',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19430329T020000',
+ 'RDATE:19430329T020000',
+ 'RDATE:19440403T020000',
+ 'RDATE:19760328T010000',
+ 'TZNAME:CEST',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0200',
+ 'END:DAYLIGHT',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19440825T000000',
+ 'RDATE:19440825T000000',
+ 'TZNAME:WEST',
+ 'TZOFFSETFROM:+0200',
+ 'TZOFFSETTO:+0200',
+ 'END:STANDARD',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19441008T010000',
+ 'RDATE:19441008T010000',
+ 'TZNAME:WEST',
+ 'TZOFFSETFROM:+0200',
+ 'TZOFFSETTO:+0100',
+ 'END:DAYLIGHT',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19450402T020000',
+ 'RDATE:19450402T020000',
+ 'TZNAME:WEMT',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0200',
+ 'END:DAYLIGHT',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19450916T030000',
+ 'RDATE:19450916T030000',
+ 'TZNAME:CEST',
+ 'TZOFFSETFROM:+0200',
+ 'TZOFFSETTO:+0100',
+ 'END:STANDARD',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19770101T000000',
+ 'RDATE:19770101T000000',
+ 'TZNAME:CEST',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0100',
+ 'END:STANDARD',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19770403T020000',
+ 'RRULE:FREQ=YEARLY;UNTIL=19800406T010000Z;BYMONTH=4;BYDAY=1SU',
+ 'TZNAME:CEST',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0200',
+ 'END:DAYLIGHT',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19790930T030000',
+ 'RRULE:FREQ=YEARLY;UNTIL=19950924T010000Z;BYMONTH=9;BYDAY=-1SU',
+ 'TZNAME:CET',
+ 'TZOFFSETFROM:+0200',
+ 'TZOFFSETTO:+0100',
+ 'END:STANDARD',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19810329T020000',
+ 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU',
+ 'TZNAME:CEST',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0200',
+ 'END:DAYLIGHT',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19961027T030000',
+ 'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU',
+ 'TZNAME:CET',
+ 'TZOFFSETFROM:+0200',
+ 'TZOFFSETTO:+0100',
+ 'END:STANDARD',
+ 'END:VTIMEZONE',
+ 'BEGIN:VTIMEZONE',
+ 'TZID:US-Eastern',
+ 'LAST-MODIFIED:19870101T000000Z',
+ 'TZURL:http://zones.stds_r_us.net/tz/US-Eastern',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19671029T020000',
+ 'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10',
+ 'TZOFFSETFROM:-0400',
+ 'TZOFFSETTO:-0500',
+ 'TZNAME:EST',
+ 'END:STANDARD',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19870405T020000',
+ 'RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4',
+ 'TZOFFSETFROM:-0500',
+ 'TZOFFSETTO:-0400',
+ 'TZNAME:EDT',
+ 'END:DAYLIGHT',
+ 'END:VTIMEZONE',
+ );
+ }
+
+ public function getIcalFooter()
+ {
+ return array('END:VCALENDAR');
+ }
+
+ public function assertEvent($event, $expectedDateString, $message, $timezone = null)
+ {
+ if ($timezone !== null) {
+ date_default_timezone_set($timezone);
+ }
+
+ $expectedTimeStamp = strtotime($expectedDateString);
+
+ $this->assertSame(
+ $expectedTimeStamp,
+ $event->dtstart_array[2],
+ $message . 'timestamp mismatch (expected ' . $expectedDateString . ' vs actual ' . $event->dtstart . ')'
+ );
+ $this->assertSame(
+ $expectedDateString,
+ $event->dtstart,
+ $message . 'dtstart mismatch (timestamp is okay)'
+ );
+ }
+
+ public function assertEventFile($defaultTimezone, $file, $count, $checks)
+ {
+ $options = $this->getOptions($defaultTimezone);
+
+ date_default_timezone_set('UTC');
+
+ $ical = new ICal($file, $options);
+
+ $events = $ical->events();
+
+ $this->assertCount($count, $events);
+
+ foreach ($checks as $check) {
+ $this->assertEvent(
+ $events[$check['index']],
+ $check['dateString'],
+ $check['message'],
+ isset($check['timezone']) ? $check['timezone'] : $defaultTimezone
+ );
+ }
+ }
+}
diff --git a/vendor/johngrogg/ics-parser/tests/ical/ical-monthly.ics b/vendor/johngrogg/ics-parser/tests/ical/ical-monthly.ics
new file mode 100644
index 0000000..d80e221
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/tests/ical/ical-monthly.ics
@@ -0,0 +1,18 @@
+BEGIN:VCALENDAR
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+VERSION:2.0
+X-WR-CALNAME:Private
+X-APPLE-CALENDAR-COLOR:#FF2968
+X-WR-CALDESC:
+BEGIN:VEVENT
+CREATED:20090213T195947Z
+UID:M2CD-1-1-5FB000FB-BBE4-4F3F-9E7E-217F1FF97208
+RRULE:FREQ=MONTHLY;BYMONTHDAY=1;WKST=SU;COUNT=25
+DTSTART;VALUE=DATE:20180701
+DTEND;VALUE=DATE:20180702
+SUMMARY:Monthly
+LAST-MODIFIED:20110429T222101Z
+DTSTAMP:20170630T105724Z
+SEQUENCE:0
+END:VEVENT
+END:VCALENDAR
diff --git a/vendor/johngrogg/ics-parser/tests/ical/issue-196.ics b/vendor/johngrogg/ics-parser/tests/ical/issue-196.ics
new file mode 100644
index 0000000..3ffcb18
--- /dev/null
+++ b/vendor/johngrogg/ics-parser/tests/ical/issue-196.ics
@@ -0,0 +1,64 @@
+BEGIN:VCALENDAR
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+VERSION:2.0
+X-WR-CALNAME:Test-Calendar
+X-WR-TIMEZONE:Europe/Berlin
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20180101T152047Z
+LAST-MODIFIED:20181202T202056Z
+DTSTAMP:20181202T202056Z
+UID:529b1ea3-8de8-484d-b878-c20c7fb72bf5
+SUMMARY:test
+RRULE:FREQ=DAILY;UNTIL=20191111T180000Z
+DTSTART;TZID=Europe/Berlin:20191105T190000
+DTEND;TZID=Europe/Berlin:20191105T220000
+TRANSP:OPAQUE
+SEQUENCE:24
+X-MOZ-GENERATION:37
+END:VEVENT
+BEGIN:VEVENT
+CREATED:20181202T202042Z
+LAST-MODIFIED:20181202T202053Z
+DTSTAMP:20181202T202053Z
+UID:529b1ea3-8de8-484d-b878-c20c7fb72bf5
+SUMMARY:test
+RECURRENCE-ID;TZID=Europe/Berlin:20191109T190000
+DTSTART;TZID=Europe/Berlin:20191109T170000
+DTEND;TZID=Europe/Berlin:20191109T220000
+TRANSP:OPAQUE
+SEQUENCE:25
+X-MOZ-GENERATION:37
+DURATION:PT0S
+END:VEVENT
+BEGIN:VEVENT
+CREATED:20181202T202053Z
+LAST-MODIFIED:20181202T202056Z
+DTSTAMP:20181202T202056Z
+UID:529b1ea3-8de8-484d-b878-c20c7fb72bf5
+SUMMARY:test
+RECURRENCE-ID;TZID=Europe/Berlin:20191110T190000
+DTSTART;TZID=Europe/Berlin:20191110T180000
+DTEND;TZID=Europe/Berlin:20191110T220000
+TRANSP:OPAQUE
+SEQUENCE:25
+X-MOZ-GENERATION:37
+DURATION:PT0S
+END:VEVENT
+END:VCALENDAR