diff --git a/README.md b/README.md
index 73f7b44..bda68a8 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,7 @@ luxtools provides DokuWiki syntax that:
- Embeds file-backed scratchpads with a minimal inline editor (no wiki revisions)
- Links a page to a media folder via a UUID (.pagelink), enabling a `blobs/` alias
- Adds a Page ID download link in the page info area to fetch a `.pagelink` file
+- Renders a basic calendar widget with clickable day links to chronological pages
It also ships a small file-serving endpoint (`lib/plugins/luxtools/file.php`) used
to deliver files and generate cached thumbnails.
@@ -49,6 +50,13 @@ If you install this plugin manually, make sure it is installed in:
If the folder is called differently, DokuWiki will not load it.
+This plugin uses Composer dependencies shipped inside `vendor/`.
+If dependencies are missing in your local checkout, run:
+
+```bash
+php composer.phar install
+```
+
## Project structure (developer notes)
@@ -149,6 +157,20 @@ Key settings:
URL of a local client service used by `{{open>...}}` and directory links.
See luxtools-client.
+- **image_base_path**
+ Base filesystem path used for chronological photo integration.
+ On canonical day pages (`chronological:YYYY:MM:DD`), files that start with
+ `YYYY-MM-DD` are listed automatically.
+ If a yearly subfolder exists (for example `.../2026/`), it is preferred.
+
+- **calendar_ics_files**
+ Local calendar `.ics` files (one absolute file path per line).
+ Events are parsed by `sabre/vobject` and shown on matching chronological day pages.
+ Recurrence and exclusions from the ICS are respected. For timed entries, the
+ page stores the original timestamp and renders the visible time in the
+ browser's local timezone.
+ Multi-day events appear on each overlapping day.
+
- **pagelink_search_depth**
Maximum directory depth for `.pagelink` discovery under each configured root.
`0` means only the root directory itself is checked.
@@ -246,6 +268,38 @@ for example:
{{directory>blobs/&recursive=1}}
```
+### 0.3) Calendar widget
+
+Render a basic monthly calendar that links each day to canonical chronological pages:
+
+```
+{{calendar>}}
+{{calendar>2024-10}}
+```
+
+Notes:
+
+- `{{calendar>}}` renders the current month.
+- `{{calendar>YYYY-MM}}` renders a specific month.
+- Day links target `chronological:YYYY:MM:DD`.
+- Header month/year links target `chronological:YYYY:MM` and `chronological:YYYY`.
+- Prev/next month buttons update the widget in-place without a full page reload.
+- Month switches fetch server-rendered widget HTML via AJAX and replace only the widget node.
+
+### 0.4) Virtual chronological day pages
+
+When a canonical day page (for example `chronological:2026:02:13`) does not yet
+exist, luxtools renders a virtual page in normal show mode instead of the
+default "page does not exist" output.
+
+The virtual page includes:
+
+- a German-formatted heading (for example `Freitag, 13. Februar 2026`)
+- matching local calendar events from configured `.ics` files (when available)
+- matching day photos (via existing `{{images>...}}` rendering) when available
+
+The page is only created once you edit and save actual content.
+
### 1) List files by glob pattern
The `{{directory>...}}` syntax (or `{{files>...}}` for backwards compatibility) can handle both directory listings and glob patterns. When a glob pattern is used, it renders as a table:
diff --git a/_test/ChronoIDTest.php b/_test/ChronoIDTest.php
new file mode 100644
index 0000000..d15df81
--- /dev/null
+++ b/_test/ChronoIDTest.php
@@ -0,0 +1,138 @@
+assertBool(true, ChronoID::isIsoDate($date), 'Expected valid ISO date: ' . $date);
+ }
+ }
+
+ public function testIsIsoDateInvalidCases(): void
+ {
+ $invalid = [
+ '2023-02-29',
+ '2024-13-01',
+ '2024-00-10',
+ '24-10-2024',
+ '2024/10/24',
+ '2024-10-24 12:00:00',
+ '2024-10-24T12:00:00',
+ '0000-01-01',
+ ];
+
+ foreach ($invalid as $date) {
+ $this->assertBool(false, ChronoID::isIsoDate($date), 'Expected invalid ISO date: ' . $date);
+ }
+ }
+
+ public function testDateToDayId(): void
+ {
+ $this->assertStringOrNull('chronological:2024:10:24', ChronoID::dateToDayId('2024-10-24'), 'dateToDayId failed');
+ $this->assertStringOrNull('journal:chrono:2024:10:24', ChronoID::dateToDayId('2024-10-24', 'journal:chrono'), 'dateToDayId with custom namespace failed');
+
+ $this->assertStringOrNull(null, ChronoID::dateToDayId('2024-10-24T12:00:00'), 'datetime should be rejected');
+ $this->assertStringOrNull(null, ChronoID::dateToDayId('2024-13-01'), 'invalid month should be rejected');
+ $this->assertStringOrNull(null, ChronoID::dateToDayId('2024-10-24', ''), 'empty namespace should be rejected');
+ $this->assertStringOrNull(null, ChronoID::dateToDayId('2024-10-24', 'bad namespace!'), 'invalid namespace should be rejected');
+ }
+
+ public function testCanonicalIdChecks(): void
+ {
+ $this->assertBool(true, ChronoID::isDayId('chronological:2024:10:24'), 'valid day ID should be accepted');
+ $this->assertBool(true, ChronoID::isMonthId('chronological:2024:10'), 'valid month ID should be accepted');
+ $this->assertBool(true, ChronoID::isYearId('chronological:2024'), 'valid year ID should be accepted');
+
+ $this->assertBool(false, ChronoID::isDayId('2024:10:24'), 'missing namespace should be rejected as day ID');
+ $this->assertBool(false, ChronoID::isDayId('chronological:2024-10-24'), 'hyphen date in ID should be rejected as day ID');
+ $this->assertBool(false, ChronoID::isDayId('chronological:2023:02:29'), 'invalid Gregorian day should be rejected');
+
+ $this->assertBool(false, ChronoID::isMonthId('chronological:2024:13'), 'invalid month 13 should be rejected');
+ $this->assertBool(false, ChronoID::isMonthId('chronological:2024:00'), 'invalid month 00 should be rejected');
+ $this->assertBool(false, ChronoID::isMonthId('chronological:2024-10'), 'invalid month format should be rejected');
+
+ $this->assertBool(false, ChronoID::isYearId('chronological:0000'), 'year 0000 should be rejected');
+ $this->assertBool(false, ChronoID::isYearId('chronological:24'), 'short year should be rejected');
+ $this->assertBool(false, ChronoID::isYearId('chronological:2024:10'), 'month ID should not pass as year ID');
+ }
+
+ public function testConversions(): void
+ {
+ $this->assertStringOrNull('chronological:2024:10', ChronoID::dayIdToMonthId('chronological:2024:10:24'), 'dayIdToMonthId failed');
+ $this->assertStringOrNull('chronological:2024', ChronoID::monthIdToYearId('chronological:2024:10'), 'monthIdToYearId failed');
+
+ $this->assertStringOrNull(null, ChronoID::dayIdToMonthId('chronological:2024:13:24'), 'invalid day ID should map to null month ID');
+ $this->assertStringOrNull(null, ChronoID::dayIdToMonthId('chronological:2024:10'), 'month ID should not map via dayIdToMonthId');
+ $this->assertStringOrNull(null, ChronoID::monthIdToYearId('chronological:2024:13'), 'invalid month ID should map to null year ID');
+ $this->assertStringOrNull(null, ChronoID::monthIdToYearId('chronological:2024:10:24'), 'day ID should not map via monthIdToYearId');
+ }
+
+ /**
+ * Integration-style smoke test for canonical ID matrix acceptance/rejection.
+ */
+ public function testCanonicalPageIdSmokeMatrix(): void
+ {
+ $accepted = [
+ ['day', 'chronological:2024:10:24'],
+ ['month', 'chronological:2024:10'],
+ ['year', 'chronological:2024'],
+ ];
+
+ foreach ($accepted as [$kind, $id]) {
+ if ($kind === 'day') {
+ $this->assertBool(true, ChronoID::isDayId($id), 'Expected accepted day ID: ' . $id);
+ } elseif ($kind === 'month') {
+ $this->assertBool(true, ChronoID::isMonthId($id), 'Expected accepted month ID: ' . $id);
+ } else {
+ $this->assertBool(true, ChronoID::isYearId($id), 'Expected accepted year ID: ' . $id);
+ }
+ }
+
+ $rejected = [
+ '2024:10:24',
+ 'chronological:2024-10-24',
+ 'chronological:2024:13:01',
+ 'chronological:2024:00',
+ 'chronological:0000',
+ ];
+
+ foreach ($rejected as $id) {
+ $this->assertBool(false, ChronoID::isDayId($id), 'Unexpected day ID acceptance: ' . $id);
+ $this->assertBool(false, ChronoID::isMonthId($id), 'Unexpected month ID acceptance: ' . $id);
+ $this->assertBool(false, ChronoID::isYearId($id), 'Unexpected year ID acceptance: ' . $id);
+ }
+ }
+}
diff --git a/_test/ChronologicalDateAutoLinkerTest.php b/_test/ChronologicalDateAutoLinkerTest.php
new file mode 100644
index 0000000..93744d0
--- /dev/null
+++ b/_test/ChronologicalDateAutoLinkerTest.php
@@ -0,0 +1,42 @@
+Meeting on 2024-10-24
';
+ $out = ChronologicalDateAutoLinker::linkHtml($html);
+
+ $decoded = urldecode($out);
+ if (strpos($decoded, 'chronological:2024:10:24') === false) {
+ throw new \Exception('Expected canonical link target not found');
+ }
+ }
+
+ public function testSkipsCodeContent(): void
+ {
+ $html = 'Outside 2024-10-25
Inside 2024-10-24';
+ $out = ChronologicalDateAutoLinker::linkHtml($html);
+
+ $decoded = urldecode($out);
+ if (strpos($decoded, 'chronological:2024:10:25') === false) {
+ throw new \Exception('Expected outside date link not found');
+ }
+ if (strpos($decoded, 'chronological:2024:10:24') !== false) {
+ throw new \Exception('Date inside code block should not be auto-linked');
+ }
+ }
+}
diff --git a/_test/ChronologicalDayTemplateTest.php b/_test/ChronologicalDayTemplateTest.php
new file mode 100644
index 0000000..9c5ff1a
--- /dev/null
+++ b/_test/ChronologicalDayTemplateTest.php
@@ -0,0 +1,37 @@
+ (string)$e['summary'], $events);
+ if (!in_array('Ganztag Event', $summaries, true)) {
+ throw new \Exception('Missing all-day event summary');
+ }
+ if (!in_array('Termin A', $summaries, true)) {
+ throw new \Exception('Missing timed event summary');
+ }
+
+ $timed = null;
+ foreach ($events as $event) {
+ if ((string)($event['summary'] ?? '') === 'Termin A') {
+ $timed = $event;
+ break;
+ }
+ }
+
+ if (!is_array($timed)) {
+ throw new \Exception('Timed event payload missing');
+ }
+ if (trim((string)($timed['startIso'] ?? '')) === '') {
+ throw new \Exception('Timed event should expose startIso for client-side timezone conversion');
+ }
+ }
+
+ public function testEventsForDateReadsMultipleConfiguredFiles(): void
+ {
+ $dir = TMP_DIR . '/chrono_ics/' . uniqid('multi_', true);
+ @mkdir($dir, 0777, true);
+ $ics1 = $dir . '/one.ics';
+ $ics2 = $dir . '/two.ics';
+
+ @file_put_contents($ics1, "BEGIN:VCALENDAR\nBEGIN:VEVENT\nDTSTART:20260218T100000\nSUMMARY:Eins\nEND:VEVENT\nEND:VCALENDAR\n");
+ @file_put_contents($ics2, "BEGIN:VCALENDAR\nBEGIN:VEVENT\nDTSTART:20260218T110000\nSUMMARY:Zwei\nEND:VEVENT\nEND:VCALENDAR\n");
+
+ $config = $ics1 . "\n" . $ics2;
+ $events = ChronologicalIcsEvents::eventsForDate($config, '2026-02-18');
+
+ if (count($events) !== 2) {
+ throw new \Exception('Expected 2 events from two files, got ' . count($events));
+ }
+ }
+
+ public function testEventsForDateSupportsWeeklyRecurrence(): void
+ {
+ $dir = TMP_DIR . '/chrono_ics/' . uniqid('rrule_', true);
+ @mkdir($dir, 0777, true);
+ $ics = $dir . '/recurring.ics';
+
+ $content = "BEGIN:VCALENDAR\n"
+ . "BEGIN:VEVENT\n"
+ . "UID:weekly-1\n"
+ . "DTSTART:20260205T090000\n"
+ . "RRULE:FREQ=WEEKLY;INTERVAL=1\n"
+ . "SUMMARY:Wiederkehrender Termin\n"
+ . "END:VEVENT\n"
+ . "END:VCALENDAR\n";
+ @file_put_contents($ics, $content);
+
+ $events = ChronologicalIcsEvents::eventsForDate($ics, '2026-02-12');
+ if (count($events) < 1) {
+ throw new \Exception('Expected recurring event on 2026-02-12, got none');
+ }
+
+ $summaries = array_map(static fn(array $e): string => (string)$e['summary'], $events);
+ if (!in_array('Wiederkehrender Termin', $summaries, true)) {
+ throw new \Exception('Recurring summary not found on matching date');
+ }
+ }
+
+ public function testEventsForDateRespectsExdateForRecurringEvent(): void
+ {
+ $dir = TMP_DIR . '/chrono_ics/' . uniqid('exdate_', true);
+ @mkdir($dir, 0777, true);
+ $ics = $dir . '/recurring-exdate.ics';
+
+ $content = "BEGIN:VCALENDAR\n"
+ . "BEGIN:VEVENT\n"
+ . "UID:weekly-2\n"
+ . "DTSTART:20260205T090000\n"
+ . "RRULE:FREQ=WEEKLY;COUNT=4\n"
+ . "EXDATE:20260212T090000\n"
+ . "SUMMARY:Termin mit Ausnahme\n"
+ . "END:VEVENT\n"
+ . "END:VCALENDAR\n";
+ @file_put_contents($ics, $content);
+
+ $events = ChronologicalIcsEvents::eventsForDate($ics, '2026-02-12');
+ $summaries = array_map(static fn(array $e): string => (string)$e['summary'], $events);
+ if (in_array('Termin mit Ausnahme', $summaries, true)) {
+ throw new \Exception('Recurring event with EXDATE should not appear on excluded day');
+ }
+ }
+
+ public function testEventsForDateKeepsUtcDateAndTimeAsIs(): void
+ {
+ $previousTimezone = date_default_timezone_get();
+ date_default_timezone_set('Europe/Berlin');
+
+ try {
+ $dir = TMP_DIR . '/chrono_ics/' . uniqid('tz_', true);
+ @mkdir($dir, 0777, true);
+ $ics = $dir . '/timezone.ics';
+
+ $content = "BEGIN:VCALENDAR\n"
+ . "BEGIN:VEVENT\n"
+ . "UID:utc-shift\n"
+ . "DTSTART:20260216T233000Z\n"
+ . "SUMMARY:UTC Spaet\n"
+ . "END:VEVENT\n"
+ . "END:VCALENDAR\n";
+ @file_put_contents($ics, $content);
+
+ $eventsOn16 = ChronologicalIcsEvents::eventsForDate($ics, '2026-02-16');
+ $eventsOn17 = ChronologicalIcsEvents::eventsForDate($ics, '2026-02-17');
+
+ $summaries16 = array_map(static fn(array $e): string => (string)$e['summary'], $eventsOn16);
+ $summaries17 = array_map(static fn(array $e): string => (string)$e['summary'], $eventsOn17);
+
+ if (!in_array('UTC Spaet', $summaries16, true)) {
+ throw new \Exception('UTC event should stay on its own UTC date');
+ }
+ if (in_array('UTC Spaet', $summaries17, true)) {
+ throw new \Exception('UTC event should not be shifted to next day by server timezone');
+ }
+
+ $utcEvent = null;
+ foreach ($eventsOn16 as $entry) {
+ if ((string)($entry['summary'] ?? '') === 'UTC Spaet') {
+ $utcEvent = $entry;
+ break;
+ }
+ }
+ if (!is_array($utcEvent)) {
+ throw new \Exception('UTC event payload missing after day match');
+ }
+ if ((string)($utcEvent['time'] ?? '') !== '23:30') {
+ throw new \Exception('UTC event time should remain unchanged (expected 23:30)');
+ }
+ } finally {
+ date_default_timezone_set($previousTimezone);
+ }
+ }
+
+ public function testEventsForDateShowsMultiDayAllDayEventOnOverlappingDays(): void
+ {
+ $dir = TMP_DIR . '/chrono_ics/' . uniqid('multiday_', true);
+ @mkdir($dir, 0777, true);
+ $ics = $dir . '/multiday.ics';
+
+ $content = "BEGIN:VCALENDAR\n"
+ . "BEGIN:VEVENT\n"
+ . "UID:multi-day-1\n"
+ . "DTSTART;VALUE=DATE:20260216\n"
+ . "DTEND;VALUE=DATE:20260218\n"
+ . "SUMMARY:Mehrtagesereignis\n"
+ . "END:VEVENT\n"
+ . "END:VCALENDAR\n";
+ @file_put_contents($ics, $content);
+
+ $eventsOn16 = ChronologicalIcsEvents::eventsForDate($ics, '2026-02-16');
+ $eventsOn17 = ChronologicalIcsEvents::eventsForDate($ics, '2026-02-17');
+ $eventsOn18 = ChronologicalIcsEvents::eventsForDate($ics, '2026-02-18');
+
+ $summaries16 = array_map(static fn(array $e): string => (string)$e['summary'], $eventsOn16);
+ $summaries17 = array_map(static fn(array $e): string => (string)$e['summary'], $eventsOn17);
+ $summaries18 = array_map(static fn(array $e): string => (string)$e['summary'], $eventsOn18);
+
+ if (!in_array('Mehrtagesereignis', $summaries16, true)) {
+ throw new \Exception('Multi-day all-day event should appear on start day');
+ }
+ if (!in_array('Mehrtagesereignis', $summaries17, true)) {
+ throw new \Exception('Multi-day all-day event should appear on overlapping day');
+ }
+ if (in_array('Mehrtagesereignis', $summaries18, true)) {
+ throw new \Exception('Multi-day all-day event should respect exclusive DTEND day');
+ }
+ }
+}
diff --git a/_test/SyntaxTest.php b/_test/SyntaxTest.php
index ceee6d4..b00c4bf 100644
--- a/_test/SyntaxTest.php
+++ b/_test/SyntaxTest.php
@@ -404,4 +404,121 @@ class plugin_luxtools_test extends DokuWikiTest
// Directory row should trigger the same behaviour as {{open>...}} for that folder
$this->assertStringContainsString('data-path="/Scape/exampledir"', $xhtml);
}
+
+ /**
+ * Strict ISO dates in plain text should be auto-linked to canonical day IDs.
+ */
+ public function test_auto_link_iso_date_plain_text()
+ {
+ $instructions = p_get_instructions('Meeting with John on 2024-10-24.');
+ $xhtml = p_render('xhtml', $instructions, $info);
+
+ if (strpos($xhtml, '>2024-10-24') === false) {
+ throw new \Exception('Auto-link text for 2024-10-24 not found');
+ }
+
+ if (strpos(urldecode($xhtml), 'chronological:2024:10:24') === false) {
+ throw new \Exception('Auto-link target chronological:2024:10:24 not found');
+ }
+ }
+
+ /**
+ * Auto-linking must not run inside code blocks.
+ */
+ public function test_auto_link_skips_code_blocks()
+ {
+ $syntax = 'Outside date 2024-10-25.' . "\n\n" . 'Inside code 2024-10-24';
+ $instructions = p_get_instructions($syntax);
+ $xhtml = p_render('xhtml', $instructions, $info);
+
+ if (strpos($xhtml, '>2024-10-25') === false) {
+ throw new \Exception('Outside date 2024-10-25 was not auto-linked');
+ }
+
+ if (strpos(urldecode($xhtml), 'chronological:2024:10:25') === false) {
+ throw new \Exception('Outside auto-link target chronological:2024:10:25 not found');
+ }
+
+ if (strpos(urldecode($xhtml), 'chronological:2024:10:24') !== false) {
+ throw new \Exception('Date inside code block was incorrectly auto-linked');
+ }
+
+ if (strpos($xhtml, 'Inside code 2024-10-24') === false) {
+ throw new \Exception('Code block content was unexpectedly altered');
+ }
+ }
+
+ /**
+ * Calendar widget should render links to canonical day IDs.
+ */
+ public function test_calendar_widget_links_canonical_day_ids()
+ {
+ $instructions = p_get_instructions('{{calendar>2024-10}}');
+ $xhtml = p_render('xhtml', $instructions, $info);
+
+ if (strpos($xhtml, 'luxtools-calendar') === false) {
+ throw new \Exception('Calendar container not rendered');
+ }
+
+ $decoded = urldecode($xhtml);
+ if (strpos($decoded, 'chronological:2024:10:01') === false) {
+ throw new \Exception('Expected canonical day link for 2024-10-01 not found');
+ }
+ if (strpos($decoded, 'chronological:2024:10:31') === false) {
+ throw new \Exception('Expected canonical day link for 2024-10-31 not found');
+ }
+
+ if (strpos($decoded, 'chronological:2024:10') === false) {
+ throw new \Exception('Expected month link chronological:2024:10 not found in header');
+ }
+ if (strpos($decoded, 'chronological:2024') === false) {
+ throw new \Exception('Expected year link chronological:2024 not found in header');
+ }
+ if (strpos($decoded, 'chronological:2024:09') === false) {
+ throw new \Exception('Expected previous month canonical ID chronological:2024:09 not found');
+ }
+ if (strpos($decoded, 'chronological:2024:11') === false) {
+ throw new \Exception('Expected next month canonical ID chronological:2024:11 not found');
+ }
+
+ if (strpos($xhtml, 'luxtools-calendar-nav') === false) {
+ throw new \Exception('Calendar navigation container not rendered');
+ }
+
+ if (strpos($xhtml, 'luxtools-calendar-nav-button') === false) {
+ throw new \Exception('Calendar navigation buttons not rendered');
+ }
+
+ if (strpos($xhtml, 'data-luxtools-calendar="1"') === false) {
+ throw new \Exception('Calendar JS state attribute not rendered');
+ }
+ if (strpos($xhtml, 'data-luxtools-ajax-url=') === false) {
+ throw new \Exception('Calendar AJAX endpoint metadata not rendered');
+ }
+
+ if (strpos($xhtml, 'luxtools-calendar-day') === false || strpos($xhtml, 'register_hook(
+ "RENDERER_CONTENT_POSTPROCESS",
+ "BEFORE",
+ $this,
+ "autoLinkChronologicalDates",
+ );
+ $controller->register_hook(
+ "RENDERER_CONTENT_POSTPROCESS",
+ "BEFORE",
+ $this,
+ "appendChronologicalDayEvents",
+ );
+ $controller->register_hook(
+ "RENDERER_CONTENT_POSTPROCESS",
+ "BEFORE",
+ $this,
+ "appendChronologicalDayPhotos",
+ );
+ $controller->register_hook(
+ "COMMON_PAGETPL_LOAD",
+ "BEFORE",
+ $this,
+ "prefillChronologicalDayTemplate",
+ );
+ $controller->register_hook(
+ "TPL_ACT_RENDER",
+ "BEFORE",
+ $this,
+ "renderVirtualChronologicalDayPage",
+ );
$controller->register_hook(
"CSS_STYLES_INCLUDED",
"BEFORE",
$this,
"addTemporaryInputStyles",
);
+ $controller->register_hook(
+ "AJAX_CALL_UNKNOWN",
+ "BEFORE",
+ $this,
+ "handleCalendarWidgetAjax",
+ );
$controller->register_hook(
"TOOLBAR_DEFINE",
"AFTER",
@@ -52,6 +96,7 @@ class action_plugin_luxtools extends ActionPlugin
"date-fix.js",
"page-link.js",
"linkfavicon.js",
+ "calendar-widget.js",
"main.js",
];
@@ -63,6 +108,46 @@ class action_plugin_luxtools extends ActionPlugin
}
}
+ /**
+ * Serve server-rendered calendar widget HTML for month navigation.
+ *
+ * @param Event $event
+ * @param mixed $param
+ * @return void
+ */
+ public function handleCalendarWidgetAjax(Event $event, $param)
+ {
+ if ($event->data !== 'luxtools_calendar_month') return;
+
+ $event->preventDefault();
+ $event->stopPropagation();
+
+ global $INPUT;
+
+ $year = (int)$INPUT->int('year');
+ $month = (int)$INPUT->int('month');
+ $baseNs = trim((string)$INPUT->str('base'));
+ if ($baseNs === '') {
+ $baseNs = 'chronological';
+ }
+
+ if (!ChronologicalCalendarWidget::isValidMonth($year, $month)) {
+ http_status(400);
+ echo 'Invalid month';
+ return;
+ }
+
+ $html = ChronologicalCalendarWidget::render($year, $month, $baseNs);
+ if ($html === '') {
+ http_status(500);
+ echo 'Calendar rendering failed';
+ return;
+ }
+
+ header('Content-Type: text/html; charset=utf-8');
+ echo $html;
+ }
+
/**
* Include temporary global input styling via css.php so @ini_* placeholders resolve.
*
@@ -84,6 +169,378 @@ class action_plugin_luxtools extends ActionPlugin
$event->data['files'][DOKU_PLUGIN . $plugin . '/temp-input-colors.css'] = DOKU_BASE . 'lib/plugins/' . $plugin . '/';
}
+ /**
+ * Auto-link strict ISO dates (YYYY-MM-DD) in rendered XHTML text nodes.
+ *
+ * Excludes content inside tags where links should not be altered.
+ *
+ * @param Event $event
+ * @param mixed $param
+ * @return void
+ */
+ public function autoLinkChronologicalDates(Event $event, $param)
+ {
+ if (!is_array($event->data)) return;
+
+ $mode = (string)($event->data[0] ?? '');
+ if ($mode !== 'xhtml') return;
+
+ $doc = $event->data[1] ?? null;
+ if (!is_string($doc) || $doc === '') return;
+ if (!preg_match('/\d{4}-\d{2}-\d{2}/', $doc)) return;
+
+ $event->data[1] = ChronologicalDateAutoLinker::linkHtml($doc);
+ }
+
+ /**
+ * Prefill new chronological day pages with a German date headline.
+ *
+ * @param Event $event
+ * @param mixed $param
+ * @return void
+ */
+ public function prefillChronologicalDayTemplate(Event $event, $param)
+ {
+ if (!is_array($event->data)) return;
+
+ $id = (string)($event->data['id'] ?? '');
+ if ($id === '') return;
+
+ if (function_exists('cleanID')) {
+ $id = (string)cleanID($id);
+ }
+ if ($id === '') return;
+ if (!ChronoID::isDayId($id)) return;
+
+ $template = ChronologicalDayTemplate::buildForDayId($id);
+ if ($template === null || $template === '') return;
+
+ $event->data['tpl'] = $template;
+ $event->data['tplfile'] = '';
+ $event->data['doreplace'] = false;
+ }
+
+ /**
+ * Append matching date-prefixed photos to chronological day page output.
+ *
+ * @param Event $event
+ * @param mixed $param
+ * @return void
+ */
+ public function appendChronologicalDayPhotos(Event $event, $param)
+ {
+ if (self::$internalRenderInProgress) return;
+ if (!is_array($event->data)) return;
+
+ $mode = (string)($event->data[0] ?? '');
+ if ($mode !== 'xhtml') return;
+
+ global $ACT;
+ if (!is_string($ACT) || $ACT !== 'show') return;
+
+ $doc = $event->data[1] ?? null;
+ if (!is_string($doc)) return;
+ if (str_contains($doc, 'luxtools-chronological-photos')) return;
+
+ global $ID;
+ $id = is_string($ID) ? $ID : '';
+ if ($id === '') return;
+ if (function_exists('cleanID')) {
+ $id = (string)cleanID($id);
+ }
+ if ($id === '') return;
+
+ $parts = ChronoID::parseDayId($id);
+ if ($parts === null) return;
+
+ if (!function_exists('page_exists') || !page_exists($id)) return;
+
+ $basePath = trim((string)$this->getConf('image_base_path'));
+ if ($basePath === '') return;
+
+ $dateIso = sprintf('%04d-%02d-%02d', $parts['year'], $parts['month'], $parts['day']);
+ if (!$this->hasAnyChronologicalPhotos($dateIso)) return;
+
+ $photosHtml = $this->renderChronologicalPhotosMacro($dateIso);
+ if ($photosHtml === '') return;
+
+ $event->data[1] = $doc . $photosHtml;
+ }
+
+ /**
+ * Append local calendar events to existing chronological day pages.
+ *
+ * @param Event $event
+ * @param mixed $param
+ * @return void
+ */
+ public function appendChronologicalDayEvents(Event $event, $param)
+ {
+ static $appendInProgress = false;
+ if ($appendInProgress) return;
+ if (self::$internalRenderInProgress) return;
+
+ if (!is_array($event->data)) return;
+
+ $mode = (string)($event->data[0] ?? '');
+ if ($mode !== 'xhtml') return;
+
+ global $ACT;
+ if (!is_string($ACT) || $ACT !== 'show') return;
+
+ $doc = $event->data[1] ?? null;
+ if (!is_string($doc)) return;
+ if (str_contains($doc, 'luxtools-chronological-events')) return;
+
+ global $ID;
+ $id = is_string($ID) ? $ID : '';
+ if ($id === '') return;
+ if (function_exists('cleanID')) {
+ $id = (string)cleanID($id);
+ }
+ if ($id === '') return;
+
+ $parts = ChronoID::parseDayId($id);
+ if ($parts === null) return;
+ if (!function_exists('page_exists') || !page_exists($id)) return;
+
+ $dateIso = sprintf('%04d-%02d-%02d', $parts['year'], $parts['month'], $parts['day']);
+ $appendInProgress = true;
+ try {
+ $eventsHtml = $this->renderChronologicalEventsHtml($dateIso);
+ } finally {
+ $appendInProgress = false;
+ }
+ if ($eventsHtml === '') return;
+
+ $event->data[1] = $doc . $eventsHtml;
+ }
+
+ /**
+ * Render chronological day photos using existing {{images>...}} syntax.
+ *
+ * @param string $dateIso
+ * @return string
+ */
+ protected function renderChronologicalPhotosMacro(string $dateIso): string
+ {
+ $syntax = $this->buildChronologicalImagesSyntax($dateIso);
+ if ($syntax === '') return '';
+
+ if (self::$internalRenderInProgress) return '';
+ self::$internalRenderInProgress = true;
+
+ try {
+ $info = ['cache' => false];
+ $instructions = p_get_instructions($syntax);
+ $galleryHtml = (string)p_render('xhtml', $instructions, $info);
+ } finally {
+ self::$internalRenderInProgress = false;
+ }
+
+ if ($galleryHtml === '') return '';
+
+ $title = (string)$this->getLang('chronological_photos_title');
+ if ($title === '') $title = 'Photos';
+
+ return ''
+ . '
' . hsc($title) . ' '
+ . $galleryHtml
+ . '';
+ }
+
+ /**
+ * Build {{images>...}} syntax for a given day.
+ *
+ * @param string $dateIso
+ * @return string
+ */
+ protected function buildChronologicalImagesSyntax(string $dateIso): string
+ {
+ $basePath = trim((string)$this->getConf('image_base_path'));
+ if ($basePath === '') return '';
+
+ $base = \dokuwiki\plugin\luxtools\Path::cleanPath($basePath);
+ if (!is_dir($base) || !is_readable($base)) return '';
+
+ $yearDir = rtrim($base, '/') . '/' . substr($dateIso, 0, 4) . '/';
+ $targetDir = (is_dir($yearDir) && is_readable($yearDir)) ? $yearDir : $base;
+
+ return '{{images>' . $targetDir . $dateIso . '*&recursive=0}}';
+ }
+
+ /**
+ * Render a virtual day page for missing chronological day IDs.
+ *
+ * Shows a German date heading and existing day photos (if any) without creating the page.
+ *
+ * @param Event $event
+ * @param mixed $param
+ * @return void
+ */
+ public function renderVirtualChronologicalDayPage(Event $event, $param)
+ {
+ if (!is_string($event->data) || $event->data !== 'show') return;
+
+ global $ID;
+ $id = is_string($ID) ? $ID : '';
+ if ($id === '') return;
+ if (function_exists('cleanID')) {
+ $id = (string)cleanID($id);
+ }
+ if ($id === '') return;
+
+ if (!ChronoID::isDayId($id)) return;
+ if (function_exists('page_exists') && page_exists($id)) return;
+
+ $wikiText = ChronologicalDayTemplate::buildForDayId($id) ?? '';
+ if ($wikiText === '') return;
+
+ $parts = ChronoID::parseDayId($id);
+ $extraHtml = '';
+ if ($parts !== null) {
+ $dateIso = sprintf('%04d-%02d-%02d', $parts['year'], $parts['month'], $parts['day']);
+
+ $eventsHtml = $this->renderChronologicalEventsHtml($dateIso);
+ if ($eventsHtml !== '') {
+ $extraHtml .= $eventsHtml;
+ }
+
+ if ($this->hasAnyChronologicalPhotos($dateIso)) {
+ $photosHtml = $this->renderChronologicalPhotosMacro($dateIso);
+ if ($photosHtml !== '') {
+ $extraHtml .= $photosHtml;
+ }
+ }
+ }
+
+ $editUrl = function_exists('wl') ? (string)wl($id, ['do' => 'edit']) : '';
+ $createLinkHtml = '';
+ if ($editUrl !== '') {
+ $label = (string)$this->getLang('btn_create');
+ if ($label === '') $label = 'Create this page';
+ $createLinkHtml = '✎ ' . hsc($label) . '
';
+ }
+
+ $info = ['cache' => false];
+ $instructions = p_get_instructions($wikiText);
+ $html = (string)p_render('xhtml', $instructions, $info);
+
+ echo $html . $createLinkHtml . $extraHtml;
+ $event->preventDefault();
+ $event->stopPropagation();
+ }
+
+ /**
+ * Check if there is at least one date-prefixed image for the given day.
+ *
+ * @param string $dateIso
+ * @return bool
+ */
+ protected function hasAnyChronologicalPhotos(string $dateIso): bool
+ {
+ if (!ChronoID::isIsoDate($dateIso)) return false;
+
+ $basePath = trim((string)$this->getConf('image_base_path'));
+ if ($basePath === '') return false;
+
+ $base = \dokuwiki\plugin\luxtools\Path::cleanPath($basePath);
+ if (!is_dir($base) || !is_readable($base)) return false;
+
+ $yearDir = rtrim($base, '/') . '/' . substr($dateIso, 0, 4) . '/';
+ $targetDir = (is_dir($yearDir) && is_readable($yearDir)) ? $yearDir : $base;
+
+ $pattern = rtrim($targetDir, '/') . '/' . $dateIso . '*';
+ $matches = glob($pattern) ?: [];
+ foreach ($matches as $match) {
+ if (!is_file($match)) continue;
+ $ext = strtolower(pathinfo($match, PATHINFO_EXTENSION));
+ if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'], true)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Render local calendar events section for a given date.
+ *
+ * @param string $dateIso
+ * @return string
+ */
+ protected function renderChronologicalEventsHtml(string $dateIso): string
+ {
+ $icsConfig = (string)$this->getConf('calendar_ics_files');
+ if (trim($icsConfig) === '') return '';
+
+ $events = ChronologicalIcsEvents::eventsForDate($icsConfig, $dateIso);
+ if ($events === []) return '';
+
+ $title = (string)$this->getLang('chronological_events_title');
+ if ($title === '') $title = 'Events';
+
+ $items = '';
+ foreach ($events as $entry) {
+ $summary = trim((string)($entry['summary'] ?? ''));
+ if ($summary === '') $summary = '(ohne Titel)';
+
+ $time = trim((string)($entry['time'] ?? ''));
+ $startIso = trim((string)($entry['startIso'] ?? ''));
+ $isAllDay = (bool)($entry['allDay'] ?? false);
+
+ if ($isAllDay || $time === '') {
+ $items .= '' . hsc($summary) . ' ';
+ } else {
+ $timeHtml = '';
+ $items .= '' . $timeHtml . ' - ' . hsc($summary) . ' ';
+ }
+ }
+
+ if ($items === '') return '';
+ $html = '';
+
+ return ''
+ . '
' . hsc($title) . ' '
+ . $html
+ . '';
+ }
+
+ /**
+ * Build wiki bullet list for local calendar events.
+ *
+ * @param string $dateIso
+ * @return string
+ */
+ protected function buildChronologicalEventsWiki(string $dateIso): string
+ {
+ $icsConfig = (string)$this->getConf('calendar_ics_files');
+ if (trim($icsConfig) === '') return '';
+
+ $events = ChronologicalIcsEvents::eventsForDate($icsConfig, $dateIso);
+ if ($events === []) return '';
+
+ $lines = [];
+ foreach ($events as $event) {
+ $summary = trim((string)($event['summary'] ?? ''));
+ if ($summary === '') $summary = '(ohne Titel)';
+ $summary = str_replace(["\n", "\r"], ' ', $summary);
+
+ $time = trim((string)($event['time'] ?? ''));
+ if ((bool)($event['allDay'] ?? false) || $time === '') {
+ $lines[] = ' * ' . $summary;
+ } else {
+ $lines[] = ' * ' . $time . ' - ' . $summary;
+ }
+ }
+
+ return implode("\n", $lines);
+ }
+
/**
* Add custom toolbar button for code blocks.
*
diff --git a/admin/main.php b/admin/main.php
index c1ae652..e77f66a 100644
--- a/admin/main.php
+++ b/admin/main.php
@@ -28,6 +28,8 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin
'thumb_placeholder',
'gallery_thumb_scale',
'open_service_url',
+ 'image_base_path',
+ 'calendar_ics_files',
'pagelink_search_depth',
];
@@ -86,6 +88,11 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin
$newConf['thumb_placeholder'] = $INPUT->str('thumb_placeholder');
$newConf['gallery_thumb_scale'] = $INPUT->str('gallery_thumb_scale');
$newConf['open_service_url'] = $INPUT->str('open_service_url');
+ $newConf['image_base_path'] = $INPUT->str('image_base_path');
+
+ $icsFiles = $INPUT->str('calendar_ics_files');
+ $icsFiles = str_replace(["\r\n", "\r"], "\n", $icsFiles);
+ $newConf['calendar_ics_files'] = $icsFiles;
$depth = (int)$INPUT->int('pagelink_search_depth');
if ($depth < 0) $depth = 0;
@@ -228,6 +235,17 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin
echo ' ';
echo ' ';
+ // image_base_path
+ echo '' . hsc($this->getLang('image_base_path')) . ' ';
+ echo ' ';
+ echo ' ';
+
+ // calendar_ics_files
+ $icsFiles = $this->normalizeMultilineDisplay((string)$this->getConf('calendar_ics_files'), 'calendar_ics_files');
+ echo '' . hsc($this->getLang('calendar_ics_files')) . ' ';
+ echo '';
+ echo ' ';
+
// pagelink_search_depth
echo '' . hsc($this->getLang('pagelink_search_depth')) . ' ';
echo ' ';
diff --git a/autoload.php b/autoload.php
index 67e4ba3..f5935b1 100644
--- a/autoload.php
+++ b/autoload.php
@@ -9,6 +9,11 @@
* This file registers a minimal autoloader for the plugin namespace.
*/
+$composerAutoload = __DIR__ . '/vendor/autoload.php';
+if (is_file($composerAutoload)) {
+ require_once $composerAutoload;
+}
+
spl_autoload_register(static function ($class) {
$prefix = 'dokuwiki\\plugin\\luxtools\\';
$prefixLen = strlen($prefix);
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..7a7db0d
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,5 @@
+{
+ "require": {
+ "sabre/vobject": "^4.5"
+ }
+}
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 0000000..bf2ba31
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,252 @@
+{
+ "_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": "440454aa6bd2975652e94f60998e9adc",
+ "packages": [
+ {
+ "name": "sabre/uri",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sabre-io/uri.git",
+ "reference": "38eeab6ed9eec435a2188db489d4649c56272c51"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sabre-io/uri/zipball/38eeab6ed9eec435a2188db489d4649c56272c51",
+ "reference": "38eeab6ed9eec435a2188db489d4649c56272c51",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.64",
+ "phpstan/extension-installer": "^1.4",
+ "phpstan/phpstan": "^1.12",
+ "phpstan/phpstan-phpunit": "^1.4",
+ "phpstan/phpstan-strict-rules": "^1.6",
+ "phpunit/phpunit": "^9.6"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "lib/functions.php"
+ ],
+ "psr-4": {
+ "Sabre\\Uri\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Evert Pot",
+ "email": "me@evertpot.com",
+ "homepage": "http://evertpot.com/",
+ "role": "Developer"
+ }
+ ],
+ "description": "Functions for making sense out of URIs.",
+ "homepage": "http://sabre.io/uri/",
+ "keywords": [
+ "rfc3986",
+ "uri",
+ "url"
+ ],
+ "support": {
+ "forum": "https://groups.google.com/group/sabredav-discuss",
+ "issues": "https://github.com/sabre-io/uri/issues",
+ "source": "https://github.com/fruux/sabre-uri"
+ },
+ "time": "2024-09-04T15:30:08+00:00"
+ },
+ {
+ "name": "sabre/vobject",
+ "version": "4.5.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sabre-io/vobject.git",
+ "reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sabre-io/vobject/zipball/d554eb24d64232922e1eab5896cc2f84b3b9ffb1",
+ "reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": "^7.1 || ^8.0",
+ "sabre/xml": "^2.1 || ^3.0 || ^4.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "~2.17.1",
+ "phpstan/phpstan": "^0.12 || ^1.12 || ^2.0",
+ "phpunit/php-invoker": "^2.0 || ^3.1",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
+ },
+ "suggest": {
+ "hoa/bench": "If you would like to run the benchmark scripts"
+ },
+ "bin": [
+ "bin/vobject",
+ "bin/generate_vcards"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Sabre\\VObject\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Evert Pot",
+ "email": "me@evertpot.com",
+ "homepage": "http://evertpot.com/",
+ "role": "Developer"
+ },
+ {
+ "name": "Dominik Tobschall",
+ "email": "dominik@fruux.com",
+ "homepage": "http://tobschall.de/",
+ "role": "Developer"
+ },
+ {
+ "name": "Ivan Enderlin",
+ "email": "ivan.enderlin@hoa-project.net",
+ "homepage": "http://mnt.io/",
+ "role": "Developer"
+ }
+ ],
+ "description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects",
+ "homepage": "http://sabre.io/vobject/",
+ "keywords": [
+ "availability",
+ "freebusy",
+ "iCalendar",
+ "ical",
+ "ics",
+ "jCal",
+ "jCard",
+ "recurrence",
+ "rfc2425",
+ "rfc2426",
+ "rfc2739",
+ "rfc4770",
+ "rfc5545",
+ "rfc5546",
+ "rfc6321",
+ "rfc6350",
+ "rfc6351",
+ "rfc6474",
+ "rfc6638",
+ "rfc6715",
+ "rfc6868",
+ "vCalendar",
+ "vCard",
+ "vcf",
+ "xCal",
+ "xCard"
+ ],
+ "support": {
+ "forum": "https://groups.google.com/group/sabredav-discuss",
+ "issues": "https://github.com/sabre-io/vobject/issues",
+ "source": "https://github.com/fruux/sabre-vobject"
+ },
+ "time": "2026-01-12T10:45:19+00:00"
+ },
+ {
+ "name": "sabre/xml",
+ "version": "4.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sabre-io/xml.git",
+ "reference": "a89257fd188ce30e456b841b6915f27905dfdbe3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sabre-io/xml/zipball/a89257fd188ce30e456b841b6915f27905dfdbe3",
+ "reference": "a89257fd188ce30e456b841b6915f27905dfdbe3",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-xmlreader": "*",
+ "ext-xmlwriter": "*",
+ "lib-libxml": ">=2.6.20",
+ "php": "^7.4 || ^8.0",
+ "sabre/uri": ">=2.0,<4.0.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.64",
+ "phpstan/phpstan": "^1.12",
+ "phpunit/phpunit": "^9.6"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "lib/Deserializer/functions.php",
+ "lib/Serializer/functions.php"
+ ],
+ "psr-4": {
+ "Sabre\\Xml\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Evert Pot",
+ "email": "me@evertpot.com",
+ "homepage": "http://evertpot.com/",
+ "role": "Developer"
+ },
+ {
+ "name": "Markus Staab",
+ "email": "markus.staab@redaxo.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "sabre/xml is an XML library that you may not hate.",
+ "homepage": "https://sabre.io/xml/",
+ "keywords": [
+ "XMLReader",
+ "XMLWriter",
+ "dom",
+ "xml"
+ ],
+ "support": {
+ "forum": "https://groups.google.com/group/sabredav-discuss",
+ "issues": "https://github.com/sabre-io/xml/issues",
+ "source": "https://github.com/fruux/sabre-xml"
+ },
+ "time": "2024-09-06T08:00:55+00:00"
+ }
+ ],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": {},
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": {},
+ "platform-dev": {},
+ "plugin-api-version": "2.9.0"
+}
diff --git a/conf/default.php b/conf/default.php
index e1e1812..1791116 100644
--- a/conf/default.php
+++ b/conf/default.php
@@ -34,6 +34,12 @@ $conf['gallery_thumb_scale'] = 1;
// Local client service used by {{open>...}}.
$conf['open_service_url'] = 'http://127.0.0.1:8765';
+// Base filesystem path for chronological photo integration.
+$conf['image_base_path'] = '';
+
+// Local calendar ICS files (one absolute file path per line).
+$conf['calendar_ics_files'] = '';
+
// Maximum depth when searching for .pagelink files under allowed roots.
$conf['pagelink_search_depth'] = 3;
diff --git a/js/calendar-widget.js b/js/calendar-widget.js
new file mode 100644
index 0000000..cd8b97a
--- /dev/null
+++ b/js/calendar-widget.js
@@ -0,0 +1,129 @@
+/* global window, document, fetch, URLSearchParams */
+
+(function () {
+ 'use strict';
+
+ var Luxtools = window.Luxtools || (window.Luxtools = {});
+
+ function findCalendarRoot(target) {
+ var el = target;
+ while (el && el !== document) {
+ if (el.classList && el.classList.contains('luxtools-calendar') && el.getAttribute('data-luxtools-calendar') === '1') {
+ return el;
+ }
+ el = el.parentNode;
+ }
+ return null;
+ }
+
+ function getNextMonth(year, month, direction) {
+ var cursor = new Date(year, month - 1, 1);
+ cursor.setMonth(cursor.getMonth() + direction);
+ return {
+ year: cursor.getFullYear(),
+ month: cursor.getMonth() + 1
+ };
+ }
+
+ function parseCalendarFromHtml(html) {
+ if (!html) return null;
+
+ var wrapper = document.createElement('div');
+ wrapper.innerHTML = html;
+
+ return wrapper.querySelector('div.luxtools-calendar[data-luxtools-calendar="1"]');
+ }
+
+ function setCalendarBusy(calendar, busy) {
+ if (!calendar) return;
+
+ if (busy) {
+ calendar.setAttribute('data-luxtools-loading', '1');
+ } else {
+ calendar.removeAttribute('data-luxtools-loading');
+ }
+
+ var buttons = calendar.querySelectorAll('button.luxtools-calendar-nav-button');
+ for (var i = 0; i < buttons.length; i++) {
+ buttons[i].disabled = !!busy;
+ }
+ }
+
+ function fetchCalendarMonth(calendar, year, month) {
+ var ajaxUrl = calendar.getAttribute('data-luxtools-ajax-url') || '';
+ if (!ajaxUrl) return Promise.reject(new Error('Missing calendar ajax url'));
+
+ var baseNs = calendar.getAttribute('data-base-ns') || 'chronological';
+ var params = new URLSearchParams({
+ call: 'luxtools_calendar_month',
+ year: String(year),
+ month: String(month),
+ base: baseNs
+ });
+
+ var url = ajaxUrl + (ajaxUrl.indexOf('?') >= 0 ? '&' : '?') + params.toString();
+
+ return fetch(url, {
+ method: 'GET',
+ credentials: 'same-origin',
+ headers: {
+ 'X-Requested-With': 'XMLHttpRequest'
+ }
+ }).then(function (response) {
+ if (!response.ok) {
+ throw new Error('Calendar request failed: ' + response.status);
+ }
+ return response.text();
+ });
+ }
+
+ function navigateCalendarMonth(calendar, direction) {
+ var year = parseInt(calendar.getAttribute('data-current-year') || '', 10);
+ var month = parseInt(calendar.getAttribute('data-current-month') || '', 10);
+ if (!year || !month) return;
+
+ var next = getNextMonth(year, month, direction);
+ setCalendarBusy(calendar, true);
+
+ fetchCalendarMonth(calendar, next.year, next.month)
+ .then(function (html) {
+ var replacement = parseCalendarFromHtml(html);
+ if (!replacement) {
+ throw new Error('Calendar markup missing in response');
+ }
+
+ calendar.replaceWith(replacement);
+ })
+ .catch(function () {
+ var fallbackLink = calendar.querySelector('a.luxtools-calendar-month-link');
+ if (fallbackLink && fallbackLink.href) {
+ window.location.href = fallbackLink.href;
+ }
+ })
+ .finally(function () {
+ setCalendarBusy(calendar, false);
+ });
+ }
+
+ function onCalendarClick(event) {
+ var target = event.target;
+ if (!target || !target.classList || !target.classList.contains('luxtools-calendar-nav-button')) return;
+
+ var calendar = findCalendarRoot(target);
+ if (!calendar) return;
+
+ var direction = parseInt(target.getAttribute('data-luxtools-dir') || '0', 10);
+ if (direction !== -1 && direction !== 1) return;
+
+ event.preventDefault();
+ navigateCalendarMonth(calendar, direction);
+ }
+
+ function initCalendarWidgets() {
+ document.addEventListener('click', onCalendarClick, false);
+ }
+
+ Luxtools.CalendarWidget = {
+ init: initCalendarWidgets
+ };
+})();
diff --git a/js/main.js b/js/main.js
index 9d8f4cd..f2909bc 100644
--- a/js/main.js
+++ b/js/main.js
@@ -8,6 +8,7 @@
var OpenService = Luxtools.OpenService;
var GalleryThumbnails = Luxtools.GalleryThumbnails;
var Scratchpads = Luxtools.Scratchpads;
+ var CalendarWidget = Luxtools.CalendarWidget;
// ============================================================
// Click Handlers
@@ -79,12 +80,49 @@
});
}
+ function initChronologicalEventTimes() {
+ var nodes = document.querySelectorAll('.luxtools-event-time[data-luxtools-start]');
+ if (!nodes || nodes.length === 0) return;
+
+ var formatter;
+ try {
+ formatter = new Intl.DateTimeFormat(undefined, {
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ } catch (e) {
+ formatter = null;
+ }
+
+ for (var i = 0; i < nodes.length; i++) {
+ var node = nodes[i];
+ var raw = node.getAttribute('data-luxtools-start') || '';
+ if (!raw) continue;
+
+ var date = new Date(raw);
+ if (isNaN(date.getTime())) continue;
+
+ var label;
+ if (formatter) {
+ label = formatter.format(date);
+ } else {
+ var hh = String(date.getHours()).padStart(2, '0');
+ var mm = String(date.getMinutes()).padStart(2, '0');
+ label = hh + ':' + mm;
+ }
+
+ node.textContent = label;
+ }
+ }
+
// ============================================================
// Initialize
// ============================================================
document.addEventListener('click', onClick, false);
document.addEventListener('DOMContentLoaded', function () {
if (GalleryThumbnails && GalleryThumbnails.init) GalleryThumbnails.init();
+ initChronologicalEventTimes();
+ if (CalendarWidget && CalendarWidget.init) CalendarWidget.init();
}, false);
document.addEventListener('DOMContentLoaded', function () {
if (Scratchpads && Scratchpads.init) Scratchpads.init();
diff --git a/lang/de/lang.php b/lang/de/lang.php
index 9ed85df..4a47c86 100644
--- a/lang/de/lang.php
+++ b/lang/de/lang.php
@@ -62,6 +62,10 @@ $lang["gallery_thumb_scale"] =
"Skalierungsfaktor für Galerie-Thumbnails. 2 erzeugt schärfere Thumbnails auf HiDPI-Displays (Anzeige bleibt 150×150).";
$lang["open_service_url"] =
"URL des lokalen Client-Dienstes für {{open>...}} (z.B. http://127.0.0.1:8765).";
+$lang["image_base_path"] =
+ "Basis-Dateisystempfad für die chronologische Foto-Integration.";
+$lang["calendar_ics_files"] =
+ "Lokale Kalender-.ics-Dateien (ein absoluter Dateipfad pro Zeile).";
$lang["pagelink_search_depth"] =
"Maximale Verzeichnisebene für .pagelink-Suche (0 = nur Root).";
@@ -79,3 +83,5 @@ $lang["toolbar_datefix_title"] = "Datums-Fix";
$lang["toolbar_datefix_all_title"] = "Datums-Fix (Alle)";
$lang["pagelink_unlinked"] = "Seite nicht verknüpft";
$lang["pagelink_multi_warning"] = "Mehrere Ordner verknüpft";
+$lang["chronological_photos_title"] = "Fotos";
+$lang["chronological_events_title"] = "Termine";
diff --git a/lang/en/lang.php b/lang/en/lang.php
index 63c7fe5..e3efdfb 100644
--- a/lang/en/lang.php
+++ b/lang/en/lang.php
@@ -62,6 +62,10 @@ $lang["gallery_thumb_scale"] =
"Gallery thumbnail scale factor. Use 2 for sharper thumbnails on HiDPI screens (still displayed as 150×150).";
$lang["open_service_url"] =
"Local client service URL for the {{open>...}} button (e.g. http://127.0.0.1:8765).";
+$lang["image_base_path"] =
+ "Base filesystem path for chronological photo integration.";
+$lang["calendar_ics_files"] =
+ "Local calendar .ics files (one absolute file path per line).";
$lang["pagelink_search_depth"] =
"Maximum directory depth for .pagelink search (0 = only root).";
@@ -79,3 +83,6 @@ $lang["toolbar_datefix_title"] = "Date Fix";
$lang["toolbar_datefix_all_title"] = "Date Fix (All)";
$lang["pagelink_unlinked"] = "Page not linked";
$lang["pagelink_multi_warning"] = "Multiple folders linked";
+$lang["calendar_err_badmonth"] = "Invalid calendar month. Use YYYY-MM.";
+$lang["chronological_photos_title"] = "Photos";
+$lang["chronological_events_title"] = "Events";
diff --git a/src/ChronoID.php b/src/ChronoID.php
new file mode 100644
index 0000000..286d331
--- /dev/null
+++ b/src/ChronoID.php
@@ -0,0 +1,241 @@
+ $year,
+ 'month' => $month,
+ 'day' => $day,
+ ];
+ }
+
+ /**
+ * Parse canonical month ID.
+ *
+ * @param string $id
+ * @param string $baseNs
+ * @return array{year:int,month:int}|null
+ */
+ public static function parseMonthId(string $id, string $baseNs = 'chronological'): ?array
+ {
+ $ns = self::normalizeBaseNs($baseNs);
+ if ($ns === null) return null;
+
+ $id = trim($id);
+ $pattern = '/^' . preg_quote($ns, '/') . ':(\d{4}):(\d{2})$/';
+ if (!preg_match($pattern, $id, $matches)) return null;
+
+ $year = (int)$matches[1];
+ $month = (int)$matches[2];
+ if ($year < 1) return null;
+ if ($month < 1 || $month > 12) return null;
+
+ return [
+ 'year' => $year,
+ 'month' => $month,
+ ];
+ }
+
+ /**
+ * Parse canonical year ID.
+ *
+ * @param string $id
+ * @param string $baseNs
+ * @return array{year:int}|null
+ */
+ public static function parseYearId(string $id, string $baseNs = 'chronological'): ?array
+ {
+ $ns = self::normalizeBaseNs($baseNs);
+ if ($ns === null) return null;
+
+ $id = trim($id);
+ $pattern = '/^' . preg_quote($ns, '/') . ':(\d{4})$/';
+ if (!preg_match($pattern, $id, $matches)) return null;
+
+ $year = (int)$matches[1];
+ if ($year < 1) return null;
+
+ return ['year' => $year];
+ }
+
+ /**
+ * Parse strict ISO date YYYY-MM-DD.
+ *
+ * @param string $value
+ * @return int[]|null [year, month, day] or null
+ */
+ protected static function parseIsoDate(string $value): ?array
+ {
+ $value = trim($value);
+ if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $value, $matches)) {
+ return null;
+ }
+
+ $year = (int)$matches[1];
+ $month = (int)$matches[2];
+ $day = (int)$matches[3];
+
+ if ($year < 1) return null;
+ if (!checkdate($month, $day, $year)) return null;
+
+ return [$year, $month, $day];
+ }
+
+ /**
+ * Normalize and validate base namespace.
+ *
+ * Allows one or more namespace segments with characters [a-z0-9_-].
+ *
+ * @param string $baseNs
+ * @return string|null
+ */
+ protected static function normalizeBaseNs(string $baseNs): ?string
+ {
+ $baseNs = strtolower(trim($baseNs));
+ $baseNs = trim($baseNs, ':');
+ if ($baseNs === '') return null;
+
+ if (!preg_match('/^[a-z0-9][a-z0-9_-]*(?::[a-z0-9][a-z0-9_-]*)*$/', $baseNs)) {
+ return null;
+ }
+
+ return $baseNs;
+ }
+}
diff --git a/src/ChronologicalCalendarWidget.php b/src/ChronologicalCalendarWidget.php
new file mode 100644
index 0000000..96f8062
--- /dev/null
+++ b/src/ChronologicalCalendarWidget.php
@@ -0,0 +1,143 @@
+sub(new \DateInterval('P1M'));
+ $nextMonth = $monthCursor->add(new \DateInterval('P1M'));
+
+ $monthStartDate = sprintf('%04d-%02d-01', $year, $month);
+ $monthDayId = ChronoID::dateToDayId($monthStartDate, $baseNs);
+ $monthId = $monthDayId !== null ? ChronoID::dayIdToMonthId($monthDayId, $baseNs) : null;
+ $yearId = $monthId !== null ? ChronoID::monthIdToYearId($monthId, $baseNs) : null;
+
+ $prevStartDate = $prevMonth->format('Y-m-d');
+ $prevDayId = ChronoID::dateToDayId($prevStartDate, $baseNs);
+ $prevMonthId = $prevDayId !== null ? ChronoID::dayIdToMonthId($prevDayId, $baseNs) : null;
+
+ $nextStartDate = $nextMonth->format('Y-m-d');
+ $nextDayId = ChronoID::dateToDayId($nextStartDate, $baseNs);
+ $nextMonthId = $nextDayId !== null ? ChronoID::dayIdToMonthId($nextDayId, $baseNs) : null;
+
+ $leadingEmpty = $firstWeekday - 1;
+ $totalCells = (int)ceil(($leadingEmpty + $daysInMonth) / 7) * 7;
+
+ $todayY = (int)date('Y');
+ $todayM = (int)date('m');
+ $todayD = (int)date('d');
+
+ $dayUrlTemplate = function_exists('wl') ? (string)wl('__LUXTOOLS_ID_RAW__') : '';
+ $monthUrlTemplate = $dayUrlTemplate;
+ $yearUrlTemplate = $dayUrlTemplate;
+ $ajaxUrl = defined('DOKU_BASE') ? (string)DOKU_BASE . 'lib/exe/ajax.php' : 'lib/exe/ajax.php';
+
+ $html = '';
+ return $html;
+ }
+
+ /**
+ * @param int $year
+ * @param int $month
+ * @return bool
+ */
+ public static function isValidMonth(int $year, int $month): bool
+ {
+ if ($year < 1) return false;
+ if ($month < 1 || $month > 12) return false;
+ return true;
+ }
+}
diff --git a/src/ChronologicalDateAutoLinker.php b/src/ChronologicalDateAutoLinker.php
new file mode 100644
index 0000000..fc991cb
--- /dev/null
+++ b/src/ChronologicalDateAutoLinker.php
@@ -0,0 +1,134 @@
+]+>)/u', $html, -1, PREG_SPLIT_DELIM_CAPTURE);
+ if (!is_array($parts)) return $html;
+
+ $blocked = [];
+ foreach (self::$blockedTags as $tag) {
+ $blocked[$tag] = 0;
+ }
+
+ $out = '';
+ foreach ($parts as $part) {
+ if ($part === '') {
+ $out .= $part;
+ continue;
+ }
+
+ if (str_starts_with($part, '<')) {
+ self::updateBlockedTagCounters($part, $blocked);
+ $out .= $part;
+ continue;
+ }
+
+ if (self::isBlockedContext($blocked)) {
+ $out .= $part;
+ continue;
+ }
+
+ $out .= self::linkText($part);
+ }
+
+ return $out;
+ }
+
+ /**
+ * Link strict ISO dates in plain text.
+ *
+ * @param string $text
+ * @return string
+ */
+ protected static function linkText(string $text): string
+ {
+ $replaced = preg_replace_callback(
+ '/(?' . hsc($date) . '';
+ }
+
+ return $matches[0];
+ },
+ $text
+ );
+
+ return is_string($replaced) ? $replaced : $text;
+ }
+
+ /**
+ * Update blocked-tag counters while traversing HTML tokens.
+ *
+ * @param string $token
+ * @param array $blocked
+ * @return void
+ */
+ protected static function updateBlockedTagCounters(string $token, array &$blocked): void
+ {
+ if (!preg_match('/^<\s*(\/?)\s*([a-zA-Z0-9:_-]+)/', $token, $matches)) {
+ return;
+ }
+
+ $isClosing = $matches[1] === '/';
+ $tag = strtolower((string)$matches[2]);
+ if (!array_key_exists($tag, $blocked)) return;
+
+ if ($isClosing) {
+ if ($blocked[$tag] > 0) $blocked[$tag]--;
+ return;
+ }
+
+ $selfClosing = preg_match('/\/\s*>$/', $token) === 1;
+ if (!$selfClosing) {
+ $blocked[$tag]++;
+ }
+ }
+
+ /**
+ * Check if traversal is currently inside a blocked context.
+ *
+ * @param array $blocked
+ * @return bool
+ */
+ protected static function isBlockedContext(array $blocked): bool
+ {
+ foreach ($blocked as $count) {
+ if ($count > 0) return true;
+ }
+ return false;
+ }
+}
diff --git a/src/ChronologicalDayTemplate.php b/src/ChronologicalDayTemplate.php
new file mode 100644
index 0000000..527c5c3
--- /dev/null
+++ b/src/ChronologicalDayTemplate.php
@@ -0,0 +1,71 @@
+ 'Montag',
+ 2 => 'Dienstag',
+ 3 => 'Mittwoch',
+ 4 => 'Donnerstag',
+ 5 => 'Freitag',
+ 6 => 'Samstag',
+ 7 => 'Sonntag',
+ ];
+
+ $months = [
+ 1 => 'Januar',
+ 2 => 'Februar',
+ 3 => 'März',
+ 4 => 'April',
+ 5 => 'Mai',
+ 6 => 'Juni',
+ 7 => 'Juli',
+ 8 => 'August',
+ 9 => 'September',
+ 10 => 'Oktober',
+ 11 => 'November',
+ 12 => 'Dezember',
+ ];
+
+ $weekdayIndex = (int)date('N', mktime(0, 0, 0, $month, $day, $year));
+ $weekday = $weekdays[$weekdayIndex] ?? '';
+ $monthName = $months[$month] ?? '';
+
+ return sprintf('%s, %d. %s %d', $weekday, $day, $monthName, $year);
+ }
+}
diff --git a/src/ChronologicalIcsEvents.php b/src/ChronologicalIcsEvents.php
new file mode 100644
index 0000000..d8771a5
--- /dev/null
+++ b/src/ChronologicalIcsEvents.php
@@ -0,0 +1,283 @@
+> In-request cache */
+ protected static $runtimeCache = [];
+
+ /**
+ * Return events for one day (YYYY-MM-DD) from configured local ICS files.
+ *
+ * @param string $icsConfig Multiline list of local ICS file paths
+ * @param string $dateIso
+ * @return array
+ */
+ public static function eventsForDate(string $icsConfig, string $dateIso): array
+ {
+ if (!ChronoID::isIsoDate($dateIso)) return [];
+
+ $files = self::parseConfiguredFiles($icsConfig);
+ if ($files === []) return [];
+
+ $signature = self::buildSignature($files);
+ if ($signature === '') return [];
+
+ $cacheKey = $signature . '|' . $dateIso;
+
+ if (isset(self::$runtimeCache[$cacheKey])) {
+ return self::$runtimeCache[$cacheKey];
+ }
+
+ $utc = new DateTimeZone('UTC');
+ $rangeStart = new DateTimeImmutable($dateIso . ' 00:00:00', $utc);
+ $rangeStart = $rangeStart->sub(new DateInterval('P1D'));
+ $rangeEnd = $rangeStart->add(new DateInterval('P3D'));
+
+ $events = [];
+ $seen = [];
+
+ foreach ($files as $file) {
+ foreach (self::readEventsFromFile($file, $dateIso, $rangeStart, $rangeEnd) as $entry) {
+ $dedupeKey = implode('|', [
+ (string)($entry['summary'] ?? ''),
+ (string)($entry['time'] ?? ''),
+ ((bool)($entry['allDay'] ?? false)) ? '1' : '0',
+ ]);
+ if (isset($seen[$dedupeKey])) continue;
+ $seen[$dedupeKey] = true;
+ $events[] = $entry;
+ }
+ }
+
+ usort($events, static function (array $a, array $b): int {
+ $aAllDay = (bool)($a['allDay'] ?? false);
+ $bAllDay = (bool)($b['allDay'] ?? false);
+ if ($aAllDay !== $bAllDay) {
+ return $aAllDay ? -1 : 1;
+ }
+
+ $timeCmp = strcmp((string)($a['time'] ?? ''), (string)($b['time'] ?? ''));
+ if ($timeCmp !== 0) return $timeCmp;
+
+ return strcmp((string)($a['summary'] ?? ''), (string)($b['summary'] ?? ''));
+ });
+
+ self::$runtimeCache[$cacheKey] = $events;
+ return $events;
+ }
+
+ /**
+ * @param string $icsConfig
+ * @return string[]
+ */
+ protected static function parseConfiguredFiles(string $icsConfig): array
+ {
+ $files = [];
+ $lines = preg_split('/\r\n|\r|\n/', $icsConfig) ?: [];
+
+ foreach ($lines as $line) {
+ $line = trim((string)$line);
+ if ($line === '') continue;
+ if (str_starts_with($line, '#')) continue;
+
+ $path = Path::cleanPath($line, false);
+ if (!is_file($path) || !is_readable($path)) continue;
+ $files[] = $path;
+ }
+
+ $files = array_values(array_unique($files));
+ sort($files, SORT_NATURAL | SORT_FLAG_CASE);
+ return $files;
+ }
+
+ /**
+ * Build signature from file path + mtime + size.
+ *
+ * @param string[] $files
+ * @return string
+ */
+ protected static function buildSignature(array $files): string
+ {
+ if ($files === []) return '';
+ $parts = [];
+
+ foreach ($files as $file) {
+ $mtime = @filemtime($file) ?: 0;
+ $size = @filesize($file) ?: 0;
+ $parts[] = $file . '|' . $mtime . '|' . $size;
+ }
+
+ return sha1(implode("\n", $parts));
+ }
+
+ /**
+ * Parse one ICS file and return normalized events for the target day.
+ *
+ * @param string $file
+ * @param string $dateIso
+ * @param DateTimeImmutable $rangeStart
+ * @param DateTimeImmutable $rangeEnd
+ * @return array
+ */
+ protected static function readEventsFromFile(
+ string $file,
+ string $dateIso,
+ DateTimeImmutable $rangeStart,
+ DateTimeImmutable $rangeEnd
+ ): array
+ {
+ $raw = @file_get_contents($file);
+ if (!is_string($raw) || trim($raw) === '') return [];
+
+ try {
+ $component = Reader::read($raw, Reader::OPTION_FORGIVING);
+ if (!($component instanceof VCalendar)) return [];
+
+ $expanded = $component->expand($rangeStart, $rangeEnd);
+ if (!($expanded instanceof VCalendar)) return [];
+
+ return self::collectEventsFromCalendar($expanded, $dateIso);
+ } catch (Throwable $e) {
+ return [];
+ }
+ }
+
+ /**
+ * @param VCalendar $calendar
+ * @param string $dateIso
+ * @return array
+ */
+ protected static function collectEventsFromCalendar(
+ VCalendar $calendar,
+ string $dateIso
+ ): array {
+ $result = [];
+ $seen = [];
+
+ foreach ($calendar->select('VEVENT') as $vevent) {
+ if (!($vevent instanceof VEvent)) continue;
+
+ $normalized = self::normalizeEventForDay($vevent, $dateIso);
+ if ($normalized === null) continue;
+
+ $dedupeKey = implode('|', [
+ (string)($normalized['uid'] ?? ''),
+ (string)($normalized['rid'] ?? ''),
+ (string)($normalized['start'] ?? ''),
+ (string)($normalized['summary'] ?? ''),
+ (string)($normalized['time'] ?? ''),
+ ((bool)($normalized['allDay'] ?? false)) ? '1' : '0',
+ ]);
+ if (isset($seen[$dedupeKey])) continue;
+ $seen[$dedupeKey] = true;
+
+ $result[] = [
+ 'summary' => (string)$normalized['summary'],
+ 'time' => (string)$normalized['time'],
+ 'startIso' => (string)$normalized['start'],
+ 'allDay' => (bool)$normalized['allDay'],
+ ];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Convert VEVENT to output item when it intersects the target day.
+ *
+ * @param VEvent $vevent
+ * @param string $dateIso
+ * @return array|null
+ */
+ protected static function normalizeEventForDay(
+ VEvent $vevent,
+ string $dateIso
+ ): ?array
+ {
+ if (!isset($vevent->DTSTART)) return null;
+ if (!ChronoID::isIsoDate($dateIso)) return null;
+
+ $isAllDay = strtoupper((string)($vevent->DTSTART['VALUE'] ?? '')) === 'DATE';
+
+ $start = self::toImmutableDateTime($vevent->DTSTART->getDateTime());
+ if ($start === null) return null;
+
+ $end = null;
+ if (isset($vevent->DTEND)) {
+ $end = self::toImmutableDateTime($vevent->DTEND->getDateTime());
+ } elseif (isset($vevent->DURATION)) {
+ try {
+ $duration = $vevent->DURATION->getDateInterval();
+ if ($duration instanceof DateInterval) {
+ $end = $start->add($duration);
+ }
+ } catch (Throwable $e) {
+ $end = null;
+ }
+ }
+
+ if ($end === null) {
+ $end = $isAllDay ? $start->add(new DateInterval('P1D')) : $start;
+ }
+
+ if ($end <= $start) {
+ $end = $isAllDay ? $start->add(new DateInterval('P1D')) : $start;
+ }
+
+ $eventTimezone = $start->getTimezone();
+ $dayStart = new DateTimeImmutable($dateIso . ' 00:00:00', $eventTimezone);
+ $dayEnd = $dayStart->add(new DateInterval('P1D'));
+
+ $intersects = ($start < $dayEnd) && ($end > $dayStart);
+ if (!$intersects && !$isAllDay && $start >= $dayStart && $start < $dayEnd && $end == $start) {
+ $intersects = true;
+ }
+ if (!$intersects) return null;
+
+ $summary = trim((string)($vevent->SUMMARY ?? ''));
+ if ($summary === '') $summary = '(ohne Titel)';
+
+ $uid = trim((string)($vevent->UID ?? ''));
+ $rid = '';
+ if (isset($vevent->{'RECURRENCE-ID'})) {
+ $rid = trim((string)$vevent->{'RECURRENCE-ID'});
+ }
+
+ return [
+ 'uid' => $uid,
+ 'rid' => $rid,
+ 'start' => $start->format(DateTimeInterface::ATOM),
+ 'summary' => $summary,
+ 'time' => $isAllDay ? '' : $start->format('H:i'),
+ 'allDay' => $isAllDay,
+ ];
+ }
+
+ /**
+ * @param DateTimeInterface $dateTime
+ * @return DateTimeImmutable|null
+ */
+ protected static function toImmutableDateTime(DateTimeInterface $dateTime): ?DateTimeImmutable
+ {
+ if ($dateTime instanceof DateTimeImmutable) return $dateTime;
+
+ $immutable = DateTimeImmutable::createFromFormat('U', (string)$dateTime->getTimestamp());
+ if (!($immutable instanceof DateTimeImmutable)) return null;
+
+ return $immutable->setTimezone($dateTime->getTimezone());
+ }
+}
diff --git a/style.css b/style.css
index 4cf5502..45216a8 100644
--- a/style.css
+++ b/style.css
@@ -508,3 +508,114 @@ html.luxtools-noscroll body {
padding: 3px;
text-align: left;
}
+
+/* ========================================================================
+ * Calendar widget
+ * ======================================================================== */
+
+div.luxtools-calendar {
+ width: 100%;
+ max-width: 100%;
+ font-size: 88%;
+}
+
+div.luxtools-calendar .luxtools-calendar-title {
+ font-weight: bold;
+ margin-bottom: 0.25em;
+ font-size: 95%;
+ text-align: center;
+}
+
+div.luxtools-calendar .luxtools-calendar-nav {
+ display: grid;
+ grid-template-columns: 1fr auto 1fr;
+ align-items: center;
+ margin-bottom: 0.25em;
+}
+
+div.luxtools-calendar .luxtools-calendar-nav-prev {
+ text-align: left;
+}
+
+div.luxtools-calendar .luxtools-calendar-nav-next {
+ text-align: right;
+}
+
+div.luxtools-calendar .luxtools-calendar-nav-prev a,
+div.luxtools-calendar .luxtools-calendar-nav-next a {
+ text-decoration: none;
+}
+
+div.luxtools-calendar .luxtools-calendar-nav-button {
+ border: 1px solid @ini_border;
+ background-color: @ini_background_alt;
+ color: @ini_text;
+ font: inherit;
+ line-height: 1;
+ padding: 0.2em 0.45em;
+ cursor: pointer;
+}
+
+div.luxtools-calendar .luxtools-calendar-nav-button:hover,
+div.luxtools-calendar .luxtools-calendar-nav-button:focus {
+ background-color: @ini_highlight;
+ outline: none;
+}
+
+div.luxtools-calendar table.luxtools-calendar-table {
+ width: 100%;
+ border-collapse: collapse;
+ table-layout: fixed;
+}
+
+div.luxtools-calendar table.luxtools-calendar-table th,
+div.luxtools-calendar table.luxtools-calendar-table td {
+ border: 1px solid @ini_border;
+ padding: 0;
+ text-align: center;
+ vertical-align: middle;
+}
+
+div.luxtools-calendar table.luxtools-calendar-table th {
+ background-color: @ini_background_alt;
+ font-size: 85%;
+ font-weight: normal;
+}
+
+div.luxtools-calendar td.luxtools-calendar-day-empty {
+ background-color: @ini_background_alt;
+}
+
+div.luxtools-calendar td.luxtools-calendar-day-today {
+ background-color: @ini_highlight;
+}
+
+div.luxtools-calendar td.luxtools-calendar-day a {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ min-height: 1.9em;
+ background: transparent;
+ text-decoration: none;
+ border-bottom: 0;
+ box-shadow: none;
+ padding: 0.1em 0;
+}
+
+div.luxtools-calendar td.luxtools-calendar-day a:hover,
+div.luxtools-calendar td.luxtools-calendar-day a:focus,
+div.luxtools-calendar td.luxtools-calendar-day a:active,
+div.luxtools-calendar td.luxtools-calendar-day a:visited {
+ text-decoration: none;
+ border-bottom: 0;
+ box-shadow: none;
+}
+
+div.luxtools-calendar td.luxtools-calendar-day:hover {
+ background-color: @ini_background_alt;
+}
+
+div.luxtools-calendar td.luxtools-calendar-day.luxtools-calendar-day-today:hover {
+ background-color: @ini_highlight;
+}
diff --git a/syntax/calendar.php b/syntax/calendar.php
new file mode 100644
index 0000000..1280bd9
--- /dev/null
+++ b/syntax/calendar.php
@@ -0,0 +1,147 @@
+}} current month
+ * - {{calendar>YYYY-MM}} specific month
+ * - {{calendar>YYYY-MM&base=chronological}} custom base namespace (optional)
+ */
+class syntax_plugin_luxtools_calendar extends SyntaxPlugin
+{
+ /** @inheritdoc */
+ public function getType()
+ {
+ return 'substition';
+ }
+
+ /** @inheritdoc */
+ public function getPType()
+ {
+ return 'block';
+ }
+
+ /** @inheritdoc */
+ public function getSort()
+ {
+ return 224;
+ }
+
+ /** @inheritdoc */
+ public function connectTo($mode)
+ {
+ $this->Lexer->addSpecialPattern('\{\{calendar>.*?\}\}', $mode, 'plugin_luxtools_calendar');
+ }
+
+ /** @inheritdoc */
+ public function handle($match, $state, $pos, Doku_Handler $handler)
+ {
+ $match = substr($match, strlen('{{calendar>'), -2);
+ [$target, $flags] = array_pad(explode('&', $match, 2), 2, '');
+
+ $target = trim((string)$target);
+ $params = $this->parseFlags($flags);
+ $baseNs = $params['base'] ?? 'chronological';
+
+ $resolved = $this->resolveTargetMonth($target);
+ if ($resolved === null) {
+ return [
+ 'ok' => false,
+ 'error' => 'calendar_err_badmonth',
+ ];
+ }
+
+ return [
+ 'ok' => true,
+ 'year' => $resolved['year'],
+ 'month' => $resolved['month'],
+ 'base' => $baseNs,
+ ];
+ }
+
+ /** @inheritdoc */
+ public function render($format, Doku_Renderer $renderer, $data)
+ {
+ if ($data === false || !is_array($data)) return false;
+ if ($format !== 'xhtml') return false;
+ if (!($renderer instanceof Doku_Renderer_xhtml)) return false;
+
+ if (!($data['ok'] ?? false)) {
+ $message = (string)$this->getLang((string)($data['error'] ?? 'calendar_err_badmonth'));
+ if ($message === '') $message = 'Invalid calendar month. Use YYYY-MM.';
+ $renderer->doc .= '';
+ return true;
+ }
+
+ $year = (int)$data['year'];
+ $month = (int)$data['month'];
+ $baseNs = (string)$data['base'];
+
+ $renderer->doc .= ChronologicalCalendarWidget::render($year, $month, $baseNs);
+
+ return true;
+ }
+
+ /**
+ * @param string $flags
+ * @return array
+ */
+ protected function parseFlags(string $flags): array
+ {
+ $params = [];
+ foreach (explode('&', $flags) as $flag) {
+ if (trim($flag) === '') continue;
+ [$name, $value] = array_pad(explode('=', $flag, 2), 2, '');
+ $name = strtolower(trim($name));
+ $value = trim($value);
+ if ($name === '') continue;
+ $params[$name] = $value;
+ }
+
+ if (!isset($params['base']) || trim($params['base']) === '') {
+ $params['base'] = 'chronological';
+ }
+
+ return $params;
+ }
+
+ /**
+ * Resolve target string to year/month.
+ *
+ * Accepted formats:
+ * - '' (current month)
+ * - YYYY-MM
+ *
+ * @param string $target
+ * @return array{year:int,month:int}|null
+ */
+ protected function resolveTargetMonth(string $target): ?array
+ {
+ if ($target === '') {
+ return [
+ 'year' => (int)date('Y'),
+ 'month' => (int)date('m'),
+ ];
+ }
+
+ if (!preg_match('/^(\d{4})-(\d{2})$/', $target, $matches)) {
+ return null;
+ }
+
+ $year = (int)$matches[1];
+ $month = (int)$matches[2];
+ if ($year < 1) return null;
+ if ($month < 1 || $month > 12) return null;
+
+ return [
+ 'year' => $year,
+ 'month' => $month,
+ ];
+ }
+}
diff --git a/vendor/autoload.php b/vendor/autoload.php
new file mode 100644
index 0000000..26d62c6
--- /dev/null
+++ b/vendor/autoload.php
@@ -0,0 +1,22 @@
+realpath = realpath($opened_path) ?: $opened_path;
+ $opened_path = $this->realpath;
+ $this->handle = fopen($this->realpath, $mode);
+ $this->position = 0;
+
+ return (bool) $this->handle;
+ }
+
+ public function stream_read($count)
+ {
+ $data = fread($this->handle, $count);
+
+ if ($this->position === 0) {
+ $data = preg_replace('{^#!.*\r?\n}', '', $data);
+ }
+
+ $this->position += strlen($data);
+
+ return $data;
+ }
+
+ public function stream_cast($castAs)
+ {
+ return $this->handle;
+ }
+
+ public function stream_close()
+ {
+ fclose($this->handle);
+ }
+
+ public function stream_lock($operation)
+ {
+ return $operation ? flock($this->handle, $operation) : true;
+ }
+
+ public function stream_seek($offset, $whence)
+ {
+ if (0 === fseek($this->handle, $offset, $whence)) {
+ $this->position = ftell($this->handle);
+ return true;
+ }
+
+ return false;
+ }
+
+ public function stream_tell()
+ {
+ return $this->position;
+ }
+
+ public function stream_eof()
+ {
+ return feof($this->handle);
+ }
+
+ public function stream_stat()
+ {
+ return array();
+ }
+
+ public function stream_set_option($option, $arg1, $arg2)
+ {
+ return true;
+ }
+
+ public function url_stat($path, $flags)
+ {
+ $path = substr($path, 17);
+ if (file_exists($path)) {
+ return stat($path);
+ }
+
+ return false;
+ }
+ }
+ }
+
+ if (
+ (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
+ || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
+ ) {
+ return include("phpvfscomposer://" . __DIR__ . '/..'.'/sabre/vobject/bin/generate_vcards');
+ }
+}
+
+return include __DIR__ . '/..'.'/sabre/vobject/bin/generate_vcards';
diff --git a/vendor/bin/vobject b/vendor/bin/vobject
new file mode 100755
index 0000000..2a50071
--- /dev/null
+++ b/vendor/bin/vobject
@@ -0,0 +1,119 @@
+#!/usr/bin/env php
+realpath = realpath($opened_path) ?: $opened_path;
+ $opened_path = $this->realpath;
+ $this->handle = fopen($this->realpath, $mode);
+ $this->position = 0;
+
+ return (bool) $this->handle;
+ }
+
+ public function stream_read($count)
+ {
+ $data = fread($this->handle, $count);
+
+ if ($this->position === 0) {
+ $data = preg_replace('{^#!.*\r?\n}', '', $data);
+ }
+
+ $this->position += strlen($data);
+
+ return $data;
+ }
+
+ public function stream_cast($castAs)
+ {
+ return $this->handle;
+ }
+
+ public function stream_close()
+ {
+ fclose($this->handle);
+ }
+
+ public function stream_lock($operation)
+ {
+ return $operation ? flock($this->handle, $operation) : true;
+ }
+
+ public function stream_seek($offset, $whence)
+ {
+ if (0 === fseek($this->handle, $offset, $whence)) {
+ $this->position = ftell($this->handle);
+ return true;
+ }
+
+ return false;
+ }
+
+ public function stream_tell()
+ {
+ return $this->position;
+ }
+
+ public function stream_eof()
+ {
+ return feof($this->handle);
+ }
+
+ public function stream_stat()
+ {
+ return array();
+ }
+
+ public function stream_set_option($option, $arg1, $arg2)
+ {
+ return true;
+ }
+
+ public function url_stat($path, $flags)
+ {
+ $path = substr($path, 17);
+ if (file_exists($path)) {
+ return stat($path);
+ }
+
+ return false;
+ }
+ }
+ }
+
+ if (
+ (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
+ || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
+ ) {
+ return include("phpvfscomposer://" . __DIR__ . '/..'.'/sabre/vobject/bin/vobject');
+ }
+}
+
+return include __DIR__ . '/..'.'/sabre/vobject/bin/vobject';
diff --git a/vendor/composer/ClassLoader.php b/vendor/composer/ClassLoader.php
new file mode 100644
index 0000000..7824d8f
--- /dev/null
+++ b/vendor/composer/ClassLoader.php
@@ -0,0 +1,579 @@
+
+ * 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..2052022
--- /dev/null
+++ b/vendor/composer/InstalledVersions.php
@@ -0,0 +1,396 @@
+
+ * 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 string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
+ * @internal
+ */
+ private static $selfDir = null;
+
+ /**
+ * @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
+ */
+ private static $installedIsLocalDir;
+
+ /**
+ * @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();
+
+ // when using reload, we disable the duplicate protection to ensure that self::$installed data is
+ // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
+ // so we have to assume it does not, and that may result in duplicate data being returned when listing
+ // all installed packages for example
+ self::$installedIsLocalDir = false;
+ }
+
+ /**
+ * @return string
+ */
+ private static function getSelfDir()
+ {
+ if (self::$selfDir === null) {
+ self::$selfDir = strtr(__DIR__, '\\', '/');
+ }
+
+ return self::$selfDir;
+ }
+
+ /**
+ * @return array[]
+ * @psalm-return list}>
+ */
+ private static function getInstalled()
+ {
+ if (null === self::$canGetVendors) {
+ self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
+ }
+
+ $installed = array();
+ $copiedLocalDir = false;
+
+ if (self::$canGetVendors) {
+ $selfDir = self::getSelfDir();
+ foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
+ $vendorDir = strtr($vendorDir, '\\', '/');
+ 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';
+ self::$installedByVendor[$vendorDir] = $required;
+ $installed[] = $required;
+ if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
+ self::$installed = $required;
+ self::$installedIsLocalDir = true;
+ }
+ }
+ if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
+ $copiedLocalDir = true;
+ }
+ }
+ }
+
+ 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() && !$copiedLocalDir) {
+ $installed[] = self::$installed;
+ }
+
+ return $installed;
+ }
+}
diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE
new file mode 100644
index 0000000..f27399a
--- /dev/null
+++ b/vendor/composer/LICENSE
@@ -0,0 +1,21 @@
+
+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_files.php b/vendor/composer/autoload_files.php
new file mode 100644
index 0000000..3490fbb
--- /dev/null
+++ b/vendor/composer/autoload_files.php
@@ -0,0 +1,12 @@
+ $vendorDir . '/sabre/uri/lib/functions.php',
+ '3569eecfeed3bcf0bad3c998a494ecb8' => $vendorDir . '/sabre/xml/lib/Deserializer/functions.php',
+ '93aa591bc4ca510c520999e34229ee79' => $vendorDir . '/sabre/xml/lib/Serializer/functions.php',
+);
diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php
new file mode 100644
index 0000000..15a2ff3
--- /dev/null
+++ b/vendor/composer/autoload_namespaces.php
@@ -0,0 +1,9 @@
+ array($vendorDir . '/sabre/xml/lib'),
+ 'Sabre\\VObject\\' => array($vendorDir . '/sabre/vobject/lib'),
+ 'Sabre\\Uri\\' => array($vendorDir . '/sabre/uri/lib'),
+);
diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php
new file mode 100644
index 0000000..d852a2f
--- /dev/null
+++ b/vendor/composer/autoload_real.php
@@ -0,0 +1,50 @@
+register(true);
+
+ $filesToLoad = \Composer\Autoload\ComposerStaticInit440454aa6bd2975652e94f60998e9adc::$files;
+ $requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
+ if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
+ $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
+
+ require $file;
+ }
+ }, null, null);
+ foreach ($filesToLoad as $fileIdentifier => $file) {
+ $requireFile($fileIdentifier, $file);
+ }
+
+ return $loader;
+ }
+}
diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php
new file mode 100644
index 0000000..44a80ed
--- /dev/null
+++ b/vendor/composer/autoload_static.php
@@ -0,0 +1,52 @@
+ __DIR__ . '/..' . '/sabre/uri/lib/functions.php',
+ '3569eecfeed3bcf0bad3c998a494ecb8' => __DIR__ . '/..' . '/sabre/xml/lib/Deserializer/functions.php',
+ '93aa591bc4ca510c520999e34229ee79' => __DIR__ . '/..' . '/sabre/xml/lib/Serializer/functions.php',
+ );
+
+ public static $prefixLengthsPsr4 = array (
+ 'S' =>
+ array (
+ 'Sabre\\Xml\\' => 10,
+ 'Sabre\\VObject\\' => 14,
+ 'Sabre\\Uri\\' => 10,
+ ),
+ );
+
+ public static $prefixDirsPsr4 = array (
+ 'Sabre\\Xml\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/sabre/xml/lib',
+ ),
+ 'Sabre\\VObject\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/sabre/vobject/lib',
+ ),
+ 'Sabre\\Uri\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/sabre/uri/lib',
+ ),
+ );
+
+ public static $classMap = array (
+ 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
+ );
+
+ public static function getInitializer(ClassLoader $loader)
+ {
+ return \Closure::bind(function () use ($loader) {
+ $loader->prefixLengthsPsr4 = ComposerStaticInit440454aa6bd2975652e94f60998e9adc::$prefixLengthsPsr4;
+ $loader->prefixDirsPsr4 = ComposerStaticInit440454aa6bd2975652e94f60998e9adc::$prefixDirsPsr4;
+ $loader->classMap = ComposerStaticInit440454aa6bd2975652e94f60998e9adc::$classMap;
+
+ }, null, ClassLoader::class);
+ }
+}
diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json
new file mode 100644
index 0000000..ba77142
--- /dev/null
+++ b/vendor/composer/installed.json
@@ -0,0 +1,248 @@
+{
+ "packages": [
+ {
+ "name": "sabre/uri",
+ "version": "3.0.2",
+ "version_normalized": "3.0.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sabre-io/uri.git",
+ "reference": "38eeab6ed9eec435a2188db489d4649c56272c51"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sabre-io/uri/zipball/38eeab6ed9eec435a2188db489d4649c56272c51",
+ "reference": "38eeab6ed9eec435a2188db489d4649c56272c51",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.64",
+ "phpstan/extension-installer": "^1.4",
+ "phpstan/phpstan": "^1.12",
+ "phpstan/phpstan-phpunit": "^1.4",
+ "phpstan/phpstan-strict-rules": "^1.6",
+ "phpunit/phpunit": "^9.6"
+ },
+ "time": "2024-09-04T15:30:08+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "files": [
+ "lib/functions.php"
+ ],
+ "psr-4": {
+ "Sabre\\Uri\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Evert Pot",
+ "email": "me@evertpot.com",
+ "homepage": "http://evertpot.com/",
+ "role": "Developer"
+ }
+ ],
+ "description": "Functions for making sense out of URIs.",
+ "homepage": "http://sabre.io/uri/",
+ "keywords": [
+ "rfc3986",
+ "uri",
+ "url"
+ ],
+ "support": {
+ "forum": "https://groups.google.com/group/sabredav-discuss",
+ "issues": "https://github.com/sabre-io/uri/issues",
+ "source": "https://github.com/fruux/sabre-uri"
+ },
+ "install-path": "../sabre/uri"
+ },
+ {
+ "name": "sabre/vobject",
+ "version": "4.5.8",
+ "version_normalized": "4.5.8.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sabre-io/vobject.git",
+ "reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sabre-io/vobject/zipball/d554eb24d64232922e1eab5896cc2f84b3b9ffb1",
+ "reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": "^7.1 || ^8.0",
+ "sabre/xml": "^2.1 || ^3.0 || ^4.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "~2.17.1",
+ "phpstan/phpstan": "^0.12 || ^1.12 || ^2.0",
+ "phpunit/php-invoker": "^2.0 || ^3.1",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
+ },
+ "suggest": {
+ "hoa/bench": "If you would like to run the benchmark scripts"
+ },
+ "time": "2026-01-12T10:45:19+00:00",
+ "bin": [
+ "bin/vobject",
+ "bin/generate_vcards"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0.x-dev"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "Sabre\\VObject\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Evert Pot",
+ "email": "me@evertpot.com",
+ "homepage": "http://evertpot.com/",
+ "role": "Developer"
+ },
+ {
+ "name": "Dominik Tobschall",
+ "email": "dominik@fruux.com",
+ "homepage": "http://tobschall.de/",
+ "role": "Developer"
+ },
+ {
+ "name": "Ivan Enderlin",
+ "email": "ivan.enderlin@hoa-project.net",
+ "homepage": "http://mnt.io/",
+ "role": "Developer"
+ }
+ ],
+ "description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects",
+ "homepage": "http://sabre.io/vobject/",
+ "keywords": [
+ "availability",
+ "freebusy",
+ "iCalendar",
+ "ical",
+ "ics",
+ "jCal",
+ "jCard",
+ "recurrence",
+ "rfc2425",
+ "rfc2426",
+ "rfc2739",
+ "rfc4770",
+ "rfc5545",
+ "rfc5546",
+ "rfc6321",
+ "rfc6350",
+ "rfc6351",
+ "rfc6474",
+ "rfc6638",
+ "rfc6715",
+ "rfc6868",
+ "vCalendar",
+ "vCard",
+ "vcf",
+ "xCal",
+ "xCard"
+ ],
+ "support": {
+ "forum": "https://groups.google.com/group/sabredav-discuss",
+ "issues": "https://github.com/sabre-io/vobject/issues",
+ "source": "https://github.com/fruux/sabre-vobject"
+ },
+ "install-path": "../sabre/vobject"
+ },
+ {
+ "name": "sabre/xml",
+ "version": "4.0.6",
+ "version_normalized": "4.0.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sabre-io/xml.git",
+ "reference": "a89257fd188ce30e456b841b6915f27905dfdbe3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sabre-io/xml/zipball/a89257fd188ce30e456b841b6915f27905dfdbe3",
+ "reference": "a89257fd188ce30e456b841b6915f27905dfdbe3",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-xmlreader": "*",
+ "ext-xmlwriter": "*",
+ "lib-libxml": ">=2.6.20",
+ "php": "^7.4 || ^8.0",
+ "sabre/uri": ">=2.0,<4.0.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.64",
+ "phpstan/phpstan": "^1.12",
+ "phpunit/phpunit": "^9.6"
+ },
+ "time": "2024-09-06T08:00:55+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "files": [
+ "lib/Deserializer/functions.php",
+ "lib/Serializer/functions.php"
+ ],
+ "psr-4": {
+ "Sabre\\Xml\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Evert Pot",
+ "email": "me@evertpot.com",
+ "homepage": "http://evertpot.com/",
+ "role": "Developer"
+ },
+ {
+ "name": "Markus Staab",
+ "email": "markus.staab@redaxo.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "sabre/xml is an XML library that you may not hate.",
+ "homepage": "https://sabre.io/xml/",
+ "keywords": [
+ "XMLReader",
+ "XMLWriter",
+ "dom",
+ "xml"
+ ],
+ "support": {
+ "forum": "https://groups.google.com/group/sabredav-discuss",
+ "issues": "https://github.com/sabre-io/xml/issues",
+ "source": "https://github.com/fruux/sabre-xml"
+ },
+ "install-path": "../sabre/xml"
+ }
+ ],
+ "dev": true,
+ "dev-package-names": []
+}
diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php
new file mode 100644
index 0000000..5af5597
--- /dev/null
+++ b/vendor/composer/installed.php
@@ -0,0 +1,50 @@
+ array(
+ 'name' => '__root__',
+ 'pretty_version' => 'dev-main',
+ 'version' => 'dev-main',
+ 'reference' => 'b70aae837708d2d4458a68e4dbc5801ca173048d',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../../',
+ 'aliases' => array(),
+ 'dev' => true,
+ ),
+ 'versions' => array(
+ '__root__' => array(
+ 'pretty_version' => 'dev-main',
+ 'version' => 'dev-main',
+ 'reference' => 'b70aae837708d2d4458a68e4dbc5801ca173048d',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../../',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ 'sabre/uri' => array(
+ 'pretty_version' => '3.0.2',
+ 'version' => '3.0.2.0',
+ 'reference' => '38eeab6ed9eec435a2188db489d4649c56272c51',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../sabre/uri',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ 'sabre/vobject' => array(
+ 'pretty_version' => '4.5.8',
+ 'version' => '4.5.8.0',
+ 'reference' => 'd554eb24d64232922e1eab5896cc2f84b3b9ffb1',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../sabre/vobject',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ 'sabre/xml' => array(
+ 'pretty_version' => '4.0.6',
+ 'version' => '4.0.6.0',
+ 'reference' => 'a89257fd188ce30e456b841b6915f27905dfdbe3',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../sabre/xml',
+ '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..d2225c7
--- /dev/null
+++ b/vendor/composer/platform_check.php
@@ -0,0 +1,25 @@
+= 70400)) {
+ $issues[] = 'Your Composer dependencies require a PHP version ">= 7.4.0". 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;
+ }
+ }
+ throw new \RuntimeException(
+ 'Composer detected issues in your platform: ' . implode(' ', $issues)
+ );
+}
diff --git a/vendor/sabre/uri/LICENSE b/vendor/sabre/uri/LICENSE
new file mode 100644
index 0000000..ae2c992
--- /dev/null
+++ b/vendor/sabre/uri/LICENSE
@@ -0,0 +1,27 @@
+Copyright (C) 2014-2019 fruux GmbH (https://fruux.com/)
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name Sabre nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/sabre/uri/composer.json b/vendor/sabre/uri/composer.json
new file mode 100644
index 0000000..2d26c23
--- /dev/null
+++ b/vendor/sabre/uri/composer.json
@@ -0,0 +1,68 @@
+{
+ "name": "sabre/uri",
+ "description": "Functions for making sense out of URIs.",
+ "keywords": [
+ "URI",
+ "URL",
+ "rfc3986"
+ ],
+ "homepage": "http://sabre.io/uri/",
+ "license": "BSD-3-Clause",
+ "require": {
+ "php": "^7.4 || ^8.0"
+ },
+ "authors": [
+ {
+ "name": "Evert Pot",
+ "email": "me@evertpot.com",
+ "homepage": "http://evertpot.com/",
+ "role": "Developer"
+ }
+ ],
+ "support": {
+ "forum": "https://groups.google.com/group/sabredav-discuss",
+ "source": "https://github.com/fruux/sabre-uri"
+ },
+ "autoload": {
+ "files" : [
+ "lib/functions.php"
+ ],
+ "psr-4" : {
+ "Sabre\\Uri\\" : "lib/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Sabre\\Uri\\": "tests/Uri"
+ }
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.64",
+ "phpstan/phpstan": "^1.12",
+ "phpstan/phpstan-phpunit": "^1.4",
+ "phpstan/phpstan-strict-rules": "^1.6",
+ "phpstan/extension-installer": "^1.4",
+ "phpunit/phpunit" : "^9.6"
+ },
+ "scripts": {
+ "phpstan": [
+ "phpstan analyse lib tests"
+ ],
+ "cs-fixer": [
+ "php-cs-fixer fix"
+ ],
+ "phpunit": [
+ "phpunit --configuration tests/phpunit.xml"
+ ],
+ "test": [
+ "composer phpstan",
+ "composer cs-fixer",
+ "composer phpunit"
+ ]
+ },
+ "config": {
+ "allow-plugins": {
+ "phpstan/extension-installer": true
+ }
+ }
+}
diff --git a/vendor/sabre/uri/lib/InvalidUriException.php b/vendor/sabre/uri/lib/InvalidUriException.php
new file mode 100644
index 0000000..7f37ca5
--- /dev/null
+++ b/vendor/sabre/uri/lib/InvalidUriException.php
@@ -0,0 +1,19 @@
+ 0) {
+ // If the path starts with a slash
+ if ('/' === $delta['path'][0]) {
+ $path = $delta['path'];
+ } else {
+ // Removing last component from base path.
+ $path = (string) $base['path'];
+ $length = strrpos($path, '/');
+ if (false !== $length) {
+ $path = substr($path, 0, $length);
+ }
+ $path .= '/'.$delta['path'];
+ }
+ } else {
+ $path = $base['path'] ?? '/';
+ if ('' === $path) {
+ $path = '/';
+ }
+ }
+ // Removing .. and .
+ $pathParts = explode('/', $path);
+ $newPathParts = [];
+ foreach ($pathParts as $pathPart) {
+ switch ($pathPart) {
+ // case '' :
+ case '.':
+ break;
+ case '..':
+ array_pop($newPathParts);
+ break;
+ default:
+ $newPathParts[] = $pathPart;
+ break;
+ }
+ }
+
+ $path = implode('/', $newPathParts);
+
+ // If the source url ended with a /, we want to preserve that.
+ $newParts['path'] = 0 === strpos($path, '/') ? $path : '/'.$path;
+ // From PHP 8, no "?" query at all causes 'query' to be null.
+ // An empty query "http://example.com/foo?" causes 'query' to be the empty string
+ if (null !== $delta['query'] && '' !== $delta['query']) {
+ $newParts['query'] = $delta['query'];
+ } elseif (isset($base['query']) && null === $delta['host'] && null === $delta['path']) {
+ // Keep the old query if host and path didn't change
+ $newParts['query'] = $base['query'];
+ }
+ // From PHP 8, no "#" fragment at all causes 'fragment' to be null.
+ // An empty fragment "http://example.com/foo#" causes 'fragment' to be the empty string
+ if (null !== $delta['fragment'] && '' !== $delta['fragment']) {
+ $newParts['fragment'] = $delta['fragment'];
+ }
+
+ return build($newParts);
+}
+
+/**
+ * Takes a URI or partial URI as its argument, and normalizes it.
+ *
+ * After normalizing a URI, you can safely compare it to other URIs.
+ * This function will for instance convert a %7E into a tilde, according to
+ * rfc3986.
+ *
+ * It will also change a %3a into a %3A.
+ *
+ * @throws InvalidUriException
+ */
+function normalize(string $uri): string
+{
+ $parts = parse($uri);
+
+ if (null !== $parts['path']) {
+ $pathParts = explode('/', ltrim($parts['path'], '/'));
+ $newPathParts = [];
+ foreach ($pathParts as $pathPart) {
+ switch ($pathPart) {
+ case '.':
+ // skip
+ break;
+ case '..':
+ // One level up in the hierarchy
+ array_pop($newPathParts);
+ break;
+ default:
+ // Ensuring that everything is correctly percent-encoded.
+ $newPathParts[] = rawurlencode(rawurldecode($pathPart));
+ break;
+ }
+ }
+ $parts['path'] = '/'.implode('/', $newPathParts);
+ }
+
+ if (null !== $parts['scheme']) {
+ $parts['scheme'] = strtolower($parts['scheme']);
+ $defaultPorts = [
+ 'http' => '80',
+ 'https' => '443',
+ ];
+
+ if (null !== $parts['port'] && isset($defaultPorts[$parts['scheme']]) && $defaultPorts[$parts['scheme']] == $parts['port']) {
+ // Removing default ports.
+ unset($parts['port']);
+ }
+ // A few HTTP specific rules.
+ switch ($parts['scheme']) {
+ case 'http':
+ case 'https':
+ if (null === $parts['path']) {
+ // An empty path is equivalent to / in http.
+ $parts['path'] = '/';
+ }
+ break;
+ }
+ }
+
+ if (null !== $parts['host']) {
+ $parts['host'] = strtolower($parts['host']);
+ }
+
+ return build($parts);
+}
+
+/**
+ * Parses a URI and returns its individual components.
+ *
+ * This method largely behaves the same as PHP's parse_url, except that it will
+ * return an array with all the array keys, including the ones that are not
+ * set by parse_url, which makes it a bit easier to work with.
+ *
+ * Unlike PHP's parse_url, it will also convert any non-ascii characters to
+ * percent-encoded strings. PHP's parse_url corrupts these characters on OS X.
+ *
+ * In the return array, key "port" is an int value. Other keys have a string value.
+ * "Unused" keys have value null.
+ *
+ * @return array{scheme: string|null, host: string|null, path: string|null, port: positive-int|null, user: string|null, query: string|null, fragment: string|null}
+ *
+ * @throws InvalidUriException
+ */
+function parse(string $uri): array
+{
+ // Normally a URI must be ASCII. However, often it's not and
+ // parse_url might corrupt these strings.
+ //
+ // For that reason we take any non-ascii characters from the uri and
+ // uriencode them first.
+ $uri = preg_replace_callback(
+ '/[^[:ascii:]]/u',
+ function ($matches) {
+ return rawurlencode($matches[0]);
+ },
+ $uri
+ );
+
+ if (null === $uri) {
+ throw new InvalidUriException('Invalid, or could not parse URI');
+ }
+
+ $result = parse_url($uri);
+ if (false === $result) {
+ $result = _parse_fallback($uri);
+ } else {
+ // Add empty host and leading slash to Windows file paths
+ // file:///C:/path or file:///C:\path
+ // Note: the regex fragment [a-zA-Z]:[\/\\\\].* end up being
+ // [a-zA-Z]:[\/\\].*
+ // The 4 backslash in a row are the way to get 2 backslash into the actual string
+ // that is used as the regex. The 2 backslash are then the way to get 1 backslash
+ // character into the character set "a forward slash or a backslash"
+ if (isset($result['scheme']) && 'file' === $result['scheme'] && isset($result['path'])
+ && 1 === preg_match('/^(? [a-zA-Z]:([\/\\\\].*)?)$/x', $result['path'])) {
+ $result['path'] = '/'.$result['path'];
+ $result['host'] = '';
+ }
+ }
+
+ /*
+ * phpstan is not able to process all the things that happen while this function
+ * constructs the result array. It only understands the $result is
+ * non-empty-array
+ *
+ * But the detail of the returned array is correctly specified in the PHPdoc
+ * above the function call.
+ *
+ * @phpstan-ignore-next-line
+ */
+ return
+ $result + [
+ 'scheme' => null,
+ 'host' => null,
+ 'path' => null,
+ 'port' => null,
+ 'user' => null,
+ 'query' => null,
+ 'fragment' => null,
+ ];
+}
+
+/**
+ * This function takes the components returned from PHP's parse_url, and uses
+ * it to generate a new uri.
+ *
+ * @param array $parts
+ */
+function build(array $parts): string
+{
+ $uri = '';
+
+ $authority = '';
+ if (isset($parts['host'])) {
+ $authority = $parts['host'];
+ if (isset($parts['user'])) {
+ $authority = $parts['user'].'@'.$authority;
+ }
+ if (isset($parts['port'])) {
+ $authority = $authority.':'.$parts['port'];
+ }
+ }
+
+ if (isset($parts['scheme'])) {
+ // If there's a scheme, there's also a host.
+ $uri = $parts['scheme'].':';
+ }
+ if ('' !== $authority || (isset($parts['scheme']) && 'file' === $parts['scheme'])) {
+ // No scheme, but there is a host.
+ $uri .= '//'.$authority;
+ }
+
+ if (isset($parts['path'])) {
+ $uri .= $parts['path'];
+ }
+ if (isset($parts['query'])) {
+ $uri .= '?'.$parts['query'];
+ }
+ if (isset($parts['fragment'])) {
+ $uri .= '#'.$parts['fragment'];
+ }
+
+ return $uri;
+}
+
+/**
+ * Returns the 'dirname' and 'basename' for a path.
+ *
+ * The reason there is a custom function for this purpose, is because
+ * basename() is locale aware (behaviour changes if C locale or a UTF-8 locale
+ * is used) and we need a method that just operates on UTF-8 characters.
+ *
+ * In addition basename and dirname are platform aware, and will treat
+ * backslash (\) as a directory separator on Windows.
+ *
+ * This method returns the 2 components as an array.
+ *
+ * If there is no dirname, it will return an empty string. Any / appearing at
+ * the end of the string is stripped off.
+ *
+ * @return list
+ */
+function split(string $path): array
+{
+ $matches = [];
+ if (1 === preg_match('/^(?:(?:(.*)(?:\/+))?([^\/]+))(?:\/?)$/u', $path, $matches)) {
+ return [$matches[1], $matches[2]];
+ }
+
+ return [null, null];
+}
+
+/**
+ * This function is another implementation of parse_url, except this one is
+ * fully written in PHP.
+ *
+ * The reason is that the PHP bug team is not willing to admit that there are
+ * bugs in the parse_url implementation.
+ *
+ * This function is only called if the main parse method fails. It's pretty
+ * crude and probably slow, so the original parse_url is usually preferred.
+ *
+ * @return array{scheme: string|null, host: string|null, path: string|null, port: positive-int|null, user: string|null, query: string|null, fragment: string|null}
+ *
+ * @throws InvalidUriException
+ */
+function _parse_fallback(string $uri): array
+{
+ // Normally a URI must be ASCII, however. However, often it's not and
+ // parse_url might corrupt these strings.
+ //
+ // For that reason we take any non-ascii characters from the uri and
+ // uriencode them first.
+ $uri = preg_replace_callback(
+ '/[^[:ascii:]]/u',
+ function ($matches) {
+ return rawurlencode($matches[0]);
+ },
+ $uri
+ );
+
+ if (null === $uri) {
+ throw new InvalidUriException('Invalid, or could not parse URI');
+ }
+
+ $result = [
+ 'scheme' => null,
+ 'host' => null,
+ 'port' => null,
+ 'user' => null,
+ 'path' => null,
+ 'fragment' => null,
+ 'query' => null,
+ ];
+
+ if (1 === preg_match('% ^([A-Za-z][A-Za-z0-9+-\.]+): %x', $uri, $matches)) {
+ $result['scheme'] = $matches[1];
+ // Take what's left.
+ $uri = substr($uri, strlen($result['scheme']) + 1);
+ if (false === $uri) {
+ // There was nothing left.
+ $uri = '';
+ }
+ }
+
+ // Taking off a fragment part
+ if (false !== strpos($uri, '#')) {
+ list($uri, $result['fragment']) = explode('#', $uri, 2);
+ }
+ // Taking off the query part
+ if (false !== strpos($uri, '?')) {
+ list($uri, $result['query']) = explode('?', $uri, 2);
+ }
+
+ if ('///' === substr($uri, 0, 3)) {
+ // The triple slash uris are a bit unusual, but we have special handling
+ // for them.
+ $path = substr($uri, 2);
+ if (false === $path) {
+ throw new \RuntimeException('The string cannot be false');
+ }
+ $result['path'] = $path;
+ $result['host'] = '';
+ } elseif ('//' === substr($uri, 0, 2)) {
+ // Uris that have an authority part.
+ $regex = '%^
+ //
+ (?: (? [^:@]+) (: (? [^@]+)) @)?
+ (? ( [^:/]* | \[ [^\]]+ \] ))
+ (?: : (? [0-9]+))?
+ (? / .*)?
+ $%x';
+ if (1 !== preg_match($regex, $uri, $matches)) {
+ throw new InvalidUriException('Invalid, or could not parse URI');
+ }
+ if (isset($matches['host']) && '' !== $matches['host']) {
+ $result['host'] = $matches['host'];
+ }
+ if (isset($matches['port'])) {
+ $port = (int) $matches['port'];
+ if ($port > 0) {
+ $result['port'] = $port;
+ }
+ }
+ if (isset($matches['path'])) {
+ $result['path'] = $matches['path'];
+ }
+ if (isset($matches['user']) && '' !== $matches['user']) {
+ $result['user'] = $matches['user'];
+ }
+ if (isset($matches['pass']) && '' !== $matches['pass']) {
+ $result['pass'] = $matches['pass'];
+ }
+ } else {
+ $result['path'] = $uri;
+ }
+
+ return $result;
+}
diff --git a/vendor/sabre/vobject/LICENSE b/vendor/sabre/vobject/LICENSE
new file mode 100644
index 0000000..a99c8da
--- /dev/null
+++ b/vendor/sabre/vobject/LICENSE
@@ -0,0 +1,27 @@
+Copyright (C) 2011-2016 fruux GmbH (https://fruux.com/)
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name Sabre nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/sabre/vobject/README.md b/vendor/sabre/vobject/README.md
new file mode 100644
index 0000000..659e3fa
--- /dev/null
+++ b/vendor/sabre/vobject/README.md
@@ -0,0 +1,55 @@
+sabre/vobject
+=============
+
+The VObject library allows you to easily parse and manipulate [iCalendar](https://tools.ietf.org/html/rfc5545)
+and [vCard](https://tools.ietf.org/html/rfc6350) objects using PHP.
+
+The goal of the VObject library is to create a very complete library, with an easy-to-use API.
+
+
+Installation
+------------
+
+Make sure you have [Composer][1] installed, and then run:
+
+ composer require sabre/vobject "^4.0"
+
+This package requires PHP 5.5. If you need the PHP 5.3/5.4 version of this package instead, use:
+
+
+ composer require sabre/vobject "^3.4"
+
+
+Usage
+-----
+
+* [Working with vCards](http://sabre.io/vobject/vcard/)
+* [Working with iCalendar](http://sabre.io/vobject/icalendar/)
+
+
+
+Build status
+------------
+
+| branch | status |
+| ------ | ------ |
+| master | [](https://travis-ci.org/sabre-io/vobject) |
+| 3.5 | [](https://travis-ci.org/sabre-io/vobject) |
+| 3.4 | [](https://travis-ci.org/sabre-io/vobject) |
+| 3.1 | [](https://travis-ci.org/sabre-io/vobject) |
+| 2.1 | [](https://travis-ci.org/sabre-io/vobject) |
+| 2.0 | [](https://travis-ci.org/sabre-io/vobject) |
+
+
+
+Support
+-------
+
+Head over to the [SabreDAV mailing list](http://groups.google.com/group/sabredav-discuss) for any questions.
+
+Made at fruux
+-------------
+
+This library is being developed by [fruux](https://fruux.com/). Drop us a line for commercial services or enterprise support.
+
+[1]: https://getcomposer.org/
diff --git a/vendor/sabre/vobject/bin/bench.php b/vendor/sabre/vobject/bin/bench.php
new file mode 100755
index 0000000..0a2736f
--- /dev/null
+++ b/vendor/sabre/vobject/bin/bench.php
@@ -0,0 +1,12 @@
+#!/usr/bin/env php
+parse->start();
+
+$vcal = Sabre\VObject\Reader::read(fopen($inputFile, 'r'));
+
+$bench->parse->stop();
+
+$repeat = 100;
+$start = new \DateTime('2000-01-01');
+$end = new \DateTime('2020-01-01');
+$timeZone = new \DateTimeZone('America/Toronto');
+
+$bench->fb->start();
+
+for ($i = 0; $i < $repeat; ++$i) {
+ $fb = new Sabre\VObject\FreeBusyGenerator($start, $end, $vcal, $timeZone);
+ $results = $fb->getResult();
+}
+$bench->fb->stop();
+
+echo $bench,"\n";
+
+function formatMemory($input)
+{
+ if (strlen($input) > 6) {
+ return round($input / (1024 * 1024)).'M';
+ } elseif (strlen($input) > 3) {
+ return round($input / 1024).'K';
+ }
+}
+
+unset($input, $splitter);
+
+echo 'peak memory usage: '.formatMemory(memory_get_peak_usage()), "\n";
+echo 'current memory usage: '.formatMemory(memory_get_usage()), "\n";
diff --git a/vendor/sabre/vobject/bin/bench_manipulatevcard.php b/vendor/sabre/vobject/bin/bench_manipulatevcard.php
new file mode 100644
index 0000000..df6d9f2
--- /dev/null
+++ b/vendor/sabre/vobject/bin/bench_manipulatevcard.php
@@ -0,0 +1,64 @@
+parse->start();
+ $vcard = $splitter->getNext();
+ $bench->parse->pause();
+
+ if (!$vcard) {
+ break;
+ }
+
+ $bench->manipulate->start();
+ $vcard->{'X-FOO'} = 'Random new value!';
+ $emails = [];
+ if (isset($vcard->EMAIL)) {
+ foreach ($vcard->EMAIL as $email) {
+ $emails[] = (string) $email;
+ }
+ }
+ $bench->manipulate->pause();
+
+ $bench->serialize->start();
+ $vcard2 = $vcard->serialize();
+ $bench->serialize->pause();
+
+ $vcard->destroy();
+}
+
+echo $bench,"\n";
+
+function formatMemory($input)
+{
+ if (strlen($input) > 6) {
+ return round($input / (1024 * 1024)).'M';
+ } elseif (strlen($input) > 3) {
+ return round($input / 1024).'K';
+ }
+}
+
+unset($input, $splitter);
+
+echo 'peak memory usage: '.formatMemory(memory_get_peak_usage()), "\n";
+echo 'current memory usage: '.formatMemory(memory_get_usage()), "\n";
diff --git a/vendor/sabre/vobject/bin/fetch_windows_zones.php b/vendor/sabre/vobject/bin/fetch_windows_zones.php
new file mode 100755
index 0000000..2361dc3
--- /dev/null
+++ b/vendor/sabre/vobject/bin/fetch_windows_zones.php
@@ -0,0 +1,48 @@
+#!/usr/bin/env php
+xpath('//mapZone') as $mapZone) {
+ $from = (string) $mapZone['other'];
+ $to = (string) $mapZone['type'];
+
+ list($to) = explode(' ', $to, 2);
+
+ if (!isset($map[$from])) {
+ $map[$from] = $to;
+ }
+}
+
+ksort($map);
+echo "Writing to: $outputFile\n";
+
+$f = fopen($outputFile, 'w');
+fwrite($f, " testdata.vcf
+
+HI;
+
+ fwrite(STDERR, $help);
+ exit(2);
+}
+
+$count = (int)$argv[1];
+if ($count < 1) {
+ fwrite(STDERR, "Count must be at least 1\n");
+ exit(2);
+}
+
+fwrite(STDERR, "sabre/vobject " . Version::VERSION . "\n");
+fwrite(STDERR, "Generating " . $count . " vcards in vCard 4.0 format\n");
+
+/**
+ * The following list is just some random data we compiled from various
+ * sources online.
+ *
+ * Very little thought went into compiling this list, and certainly nothing
+ * political or ethical.
+ *
+ * We would _love_ more additions to this to add more variation to this list.
+ *
+ * Send us PR's and don't be shy adding your own first and last name for fun.
+ */
+
+$sets = array(
+ "nl" => array(
+ "country" => "Netherlands",
+ "boys" => array(
+ "Anno",
+ "Bram",
+ "Daan",
+ "Evert",
+ "Finn",
+ "Jayden",
+ "Jens",
+ "Jesse",
+ "Levi",
+ "Lucas",
+ "Luuk",
+ "Milan",
+ "René",
+ "Sem",
+ "Sibrand",
+ "Willem",
+ ),
+ "girls" => array(
+ "Celia",
+ "Emma",
+ "Fenna",
+ "Geke",
+ "Inge",
+ "Julia",
+ "Lisa",
+ "Lotte",
+ "Mila",
+ "Sara",
+ "Sophie",
+ "Tess",
+ "Zoë",
+ ),
+ "last" => array(
+ "Bakker",
+ "Bos",
+ "De Boer",
+ "De Groot",
+ "De Jong",
+ "De Vries",
+ "Jansen",
+ "Janssen",
+ "Meyer",
+ "Mulder",
+ "Peters",
+ "Smit",
+ "Van Dijk",
+ "Van den Berg",
+ "Visser",
+ "Vos",
+ ),
+ ),
+ "us" => array(
+ "country" => "United States",
+ "boys" => array(
+ "Aiden",
+ "Alexander",
+ "Charles",
+ "David",
+ "Ethan",
+ "Jacob",
+ "James",
+ "Jayden",
+ "John",
+ "Joseph",
+ "Liam",
+ "Mason",
+ "Michael",
+ "Noah",
+ "Richard",
+ "Robert",
+ "Thomas",
+ "William",
+ ),
+ "girls" => array(
+ "Ava",
+ "Barbara",
+ "Chloe",
+ "Dorothy",
+ "Elizabeth",
+ "Emily",
+ "Emma",
+ "Isabella",
+ "Jennifer",
+ "Lily",
+ "Linda",
+ "Margaret",
+ "Maria",
+ "Mary",
+ "Mia",
+ "Olivia",
+ "Patricia",
+ "Roxy",
+ "Sophia",
+ "Susan",
+ "Zoe",
+ ),
+ "last" => array(
+ "Smith",
+ "Johnson",
+ "Williams",
+ "Jones",
+ "Brown",
+ "Davis",
+ "Miller",
+ "Wilson",
+ "Moore",
+ "Taylor",
+ "Anderson",
+ "Thomas",
+ "Jackson",
+ "White",
+ "Harris",
+ "Martin",
+ "Thompson",
+ "Garcia",
+ "Martinez",
+ "Robinson",
+ ),
+ ),
+);
+
+$current = 0;
+
+$r = function($arr) {
+
+ return $arr[mt_rand(0,count($arr)-1)];
+
+};
+
+$bdayStart = strtotime('-85 years');
+$bdayEnd = strtotime('-20 years');
+
+while($current < $count) {
+
+ $current++;
+ fwrite(STDERR, "\033[100D$current/$count");
+
+ $country = array_rand($sets);
+ $gender = mt_rand(0,1)?'girls':'boys';
+
+ $vcard = new Component\VCard(array(
+ 'VERSION' => '4.0',
+ 'FN' => $r($sets[$country][$gender]) . ' ' . $r($sets[$country]['last']),
+ 'UID' => UUIDUtil::getUUID(),
+ ));
+
+ $bdayRatio = mt_rand(0,9);
+
+ if($bdayRatio < 2) {
+ // 20% has a birthday property with a full date
+ $dt = new \DateTime('@' . mt_rand($bdayStart, $bdayEnd));
+ $vcard->add('BDAY', $dt->format('Ymd'));
+
+ } elseif ($bdayRatio < 3) {
+ // 10% we only know the month and date of
+ $dt = new \DateTime('@' . mt_rand($bdayStart, $bdayEnd));
+ $vcard->add('BDAY', '--' . $dt->format('md'));
+ }
+ if ($result = $vcard->validate()) {
+ ob_start();
+ echo "\nWe produced an invalid vcard somehow!\n";
+ foreach($result as $message) {
+ echo " " . $message['message'] . "\n";
+ }
+ fwrite(STDERR, ob_get_clean());
+ }
+ echo $vcard->serialize();
+
+}
+
+fwrite(STDERR,"\nDone.\n");
diff --git a/vendor/sabre/vobject/bin/generateicalendardata.php b/vendor/sabre/vobject/bin/generateicalendardata.php
new file mode 100755
index 0000000..019ed97
--- /dev/null
+++ b/vendor/sabre/vobject/bin/generateicalendardata.php
@@ -0,0 +1,87 @@
+#!/usr/bin/env php
+add('VEVENT');
+ $event->DTSTART = 'bla';
+ $event->SUMMARY = 'Event #'.$ii;
+ $event->UID = md5(microtime(true));
+
+ $doctorRandom = mt_rand(1, 1000);
+
+ switch ($doctorRandom) {
+ // All-day event
+ case 1:
+ $event->DTEND = 'bla';
+ $dtStart = clone $currentDate;
+ $dtEnd = clone $currentDate;
+ $dtEnd->modify('+'.mt_rand(1, 3).' days');
+ $event->DTSTART->setDateTime($dtStart);
+ $event->DTSTART['VALUE'] = 'DATE';
+ $event->DTEND->setDateTime($dtEnd);
+ break;
+ case 2:
+ $event->RRULE = 'FREQ=DAILY;COUNT='.mt_rand(1, 10);
+ // no break intentional
+ default:
+ $dtStart = clone $currentDate;
+ $dtStart->setTime(mt_rand(1, 23), mt_rand(0, 59), mt_rand(0, 59));
+ $event->DTSTART->setDateTime($dtStart);
+ $event->DURATION = 'PT'.mt_rand(1, 3).'H';
+ break;
+ }
+
+ $currentDate->modify('+ '.mt_rand(0, 3).' days');
+}
+fwrite(STDERR, "Validating\n");
+
+$result = $calendar->validate();
+if ($result) {
+ fwrite(STDERR, "Errors!\n");
+ fwrite(STDERR, print_r($result, true));
+ exit(-1);
+}
+
+fwrite(STDERR, "Serializing this beast\n");
+
+echo $calendar->serialize();
+
+fwrite(STDERR, "done.\n");
diff --git a/vendor/sabre/vobject/bin/mergeduplicates.php b/vendor/sabre/vobject/bin/mergeduplicates.php
new file mode 100755
index 0000000..31b2c14
--- /dev/null
+++ b/vendor/sabre/vobject/bin/mergeduplicates.php
@@ -0,0 +1,160 @@
+#!/usr/bin/env php
+ 0,
+ 'No FN property' => 0,
+ 'Ignored duplicates' => 0,
+ 'Merged values' => 0,
+ 'Error' => 0,
+ 'Unique cards' => 0,
+ 'Total written' => 0,
+];
+
+function writeStats()
+{
+ global $stats;
+ foreach ($stats as $name => $value) {
+ echo str_pad($name, 23, ' ', STR_PAD_RIGHT), str_pad($value, 6, ' ', STR_PAD_LEFT), "\n";
+ }
+ // Moving cursor back a few lines.
+ echo "\033[".count($stats).'A';
+}
+
+function write($vcard)
+{
+ global $stats, $output;
+
+ ++$stats['Total written'];
+ fwrite($output, $vcard->serialize()."\n");
+}
+
+while ($vcard = $splitter->getNext()) {
+ ++$stats['Total vcards'];
+ writeStats();
+
+ $fn = isset($vcard->FN) ? (string) $vcard->FN : null;
+
+ if (empty($fn)) {
+ // Immediately write this vcard, we don't compare it.
+ ++$stats['No FN property'];
+ ++$stats['Unique cards'];
+ write($vcard);
+ $vcard->destroy();
+ continue;
+ }
+
+ if (!isset($collectedNames[$fn])) {
+ $collectedNames[$fn] = $vcard;
+ ++$stats['Unique cards'];
+ continue;
+ } else {
+ // Starting comparison for all properties. We only check if properties
+ // in the current vcard exactly appear in the earlier vcard as well.
+ foreach ($vcard->children() as $newProp) {
+ if (in_array($newProp->name, $ignoredProperties)) {
+ // We don't care about properties such as UID and REV.
+ continue;
+ }
+ $ok = false;
+ foreach ($collectedNames[$fn]->select($newProp->name) as $compareProp) {
+ if ($compareProp->serialize() === $newProp->serialize()) {
+ $ok = true;
+ break;
+ }
+ }
+
+ if (!$ok) {
+ if ('EMAIL' === $newProp->name || 'TEL' === $newProp->name) {
+ // We're going to make another attempt to find this
+ // property, this time just by value. If we find it, we
+ // consider it a success.
+ foreach ($collectedNames[$fn]->select($newProp->name) as $compareProp) {
+ if ($compareProp->getValue() === $newProp->getValue()) {
+ $ok = true;
+ break;
+ }
+ }
+
+ if (!$ok) {
+ // Merging the new value in the old vcard.
+ $collectedNames[$fn]->add(clone $newProp);
+ $ok = true;
+ ++$stats['Merged values'];
+ }
+ }
+ }
+
+ if (!$ok) {
+ // echo $newProp->serialize() . " does not appear in earlier vcard!\n";
+ ++$stats['Error'];
+ if ($debug) {
+ fwrite($debug, "Missing '".$newProp->name."' property in duplicate. Earlier vcard:\n".$collectedNames[$fn]->serialize()."\n\nLater:\n".$vcard->serialize()."\n\n");
+ }
+
+ $vcard->destroy();
+ continue 2;
+ }
+ }
+ }
+
+ $vcard->destroy();
+ ++$stats['Ignored duplicates'];
+}
+
+foreach ($collectedNames as $vcard) {
+ // Overwriting any old PRODID
+ $vcard->PRODID = '-//Sabre//Sabre VObject '.Version::VERSION.'//EN';
+ write($vcard);
+ writeStats();
+}
+
+echo str_repeat("\n", count($stats)), "\nDone.\n";
diff --git a/vendor/sabre/vobject/bin/rrulebench.php b/vendor/sabre/vobject/bin/rrulebench.php
new file mode 100644
index 0000000..6900800
--- /dev/null
+++ b/vendor/sabre/vobject/bin/rrulebench.php
@@ -0,0 +1,32 @@
+parse->start();
+
+echo "Parsing.\n";
+$vobj = Sabre\VObject\Reader::read(fopen($inputFile, 'r'));
+
+$bench->parse->stop();
+
+echo "Expanding.\n";
+$bench->expand->start();
+
+$vobj->expand(new DateTime($startDate), new DateTime($endDate));
+
+$bench->expand->stop();
+
+echo $bench,"\n";
diff --git a/vendor/sabre/vobject/bin/vobject b/vendor/sabre/vobject/bin/vobject
new file mode 100755
index 0000000..2aca7e7
--- /dev/null
+++ b/vendor/sabre/vobject/bin/vobject
@@ -0,0 +1,27 @@
+#!/usr/bin/env php
+main($argv));
+
diff --git a/vendor/sabre/vobject/composer.json b/vendor/sabre/vobject/composer.json
new file mode 100644
index 0000000..9d1b426
--- /dev/null
+++ b/vendor/sabre/vobject/composer.json
@@ -0,0 +1,107 @@
+{
+ "name": "sabre/vobject",
+ "description" : "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects",
+ "keywords" : [
+ "iCalendar",
+ "iCal",
+ "vCalendar",
+ "vCard",
+ "jCard",
+ "jCal",
+ "ics",
+ "vcf",
+ "xCard",
+ "xCal",
+ "freebusy",
+ "recurrence",
+ "availability",
+ "rfc2425",
+ "rfc2426",
+ "rfc2739",
+ "rfc4770",
+ "rfc5545",
+ "rfc5546",
+ "rfc6321",
+ "rfc6350",
+ "rfc6351",
+ "rfc6474",
+ "rfc6638",
+ "rfc6715",
+ "rfc6868"
+ ],
+ "homepage" : "http://sabre.io/vobject/",
+ "license" : "BSD-3-Clause",
+ "require" : {
+ "php" : "^7.1 || ^8.0",
+ "ext-mbstring" : "*",
+ "sabre/xml" : "^2.1 || ^3.0 || ^4.0"
+ },
+ "require-dev" : {
+ "friendsofphp/php-cs-fixer": "~2.17.1",
+ "phpunit/phpunit" : "^7.5 || ^8.5 || ^9.6",
+ "phpunit/php-invoker" : "^2.0 || ^3.1",
+ "phpstan/phpstan": "^0.12 || ^1.12 || ^2.0"
+ },
+ "suggest" : {
+ "hoa/bench" : "If you would like to run the benchmark scripts"
+ },
+ "authors" : [
+ {
+ "name" : "Evert Pot",
+ "email" : "me@evertpot.com",
+ "homepage" : "http://evertpot.com/",
+ "role" : "Developer"
+ },
+ {
+ "name" : "Dominik Tobschall",
+ "email" : "dominik@fruux.com",
+ "homepage" : "http://tobschall.de/",
+ "role" : "Developer"
+ },
+ {
+ "name" : "Ivan Enderlin",
+ "email" : "ivan.enderlin@hoa-project.net",
+ "homepage" : "http://mnt.io/",
+ "role" : "Developer"
+ }
+ ],
+ "support" : {
+ "forum" : "https://groups.google.com/group/sabredav-discuss",
+ "source" : "https://github.com/fruux/sabre-vobject"
+ },
+ "autoload" : {
+ "psr-4" : {
+ "Sabre\\VObject\\" : "lib/"
+ }
+ },
+ "autoload-dev" : {
+ "psr-4" : {
+ "Sabre\\VObject\\" : "tests/VObject"
+ }
+ },
+ "bin" : [
+ "bin/vobject",
+ "bin/generate_vcards"
+ ],
+ "extra" : {
+ "branch-alias" : {
+ "dev-master" : "4.0.x-dev"
+ }
+ },
+ "scripts": {
+ "phpstan": [
+ "phpstan analyse lib tests"
+ ],
+ "cs-fixer": [
+ "php-cs-fixer fix"
+ ],
+ "phpunit": [
+ "phpunit --configuration tests/phpunit.xml"
+ ],
+ "test": [
+ "composer phpstan",
+ "composer cs-fixer",
+ "composer phpunit"
+ ]
+ }
+}
diff --git a/vendor/sabre/vobject/lib/BirthdayCalendarGenerator.php b/vendor/sabre/vobject/lib/BirthdayCalendarGenerator.php
new file mode 100644
index 0000000..fade50e
--- /dev/null
+++ b/vendor/sabre/vobject/lib/BirthdayCalendarGenerator.php
@@ -0,0 +1,172 @@
+setObjects($objects);
+ }
+ }
+
+ /**
+ * Sets the input objects.
+ *
+ * You must either supply a vCard as a string or as a Component/VCard object.
+ * It's also possible to supply an array of strings or objects.
+ *
+ * @param mixed $objects
+ */
+ public function setObjects($objects)
+ {
+ if (!is_array($objects)) {
+ $objects = [$objects];
+ }
+
+ $this->objects = [];
+ foreach ($objects as $object) {
+ if (is_string($object)) {
+ $vObj = Reader::read($object);
+ if (!$vObj instanceof Component\VCard) {
+ throw new \InvalidArgumentException('String could not be parsed as \\Sabre\\VObject\\Component\\VCard by setObjects');
+ }
+
+ $this->objects[] = $vObj;
+ } elseif ($object instanceof Component\VCard) {
+ $this->objects[] = $object;
+ } else {
+ throw new \InvalidArgumentException('You can only pass strings or \\Sabre\\VObject\\Component\\VCard arguments to setObjects');
+ }
+ }
+ }
+
+ /**
+ * Sets the output format for the SUMMARY.
+ *
+ * @param string $format
+ */
+ public function setFormat($format)
+ {
+ $this->format = $format;
+ }
+
+ /**
+ * Parses the input data and returns a VCALENDAR.
+ *
+ * @return Component/VCalendar
+ */
+ public function getResult()
+ {
+ $calendar = new VCalendar();
+
+ foreach ($this->objects as $object) {
+ // Skip if there is no BDAY property.
+ if (!$object->select('BDAY')) {
+ continue;
+ }
+
+ // We've seen clients (ez-vcard) putting "BDAY:" properties
+ // without a value into vCards. If we come across those, we'll
+ // skip them.
+ if (empty($object->BDAY->getValue())) {
+ continue;
+ }
+
+ // We're always converting to vCard 4.0 so we can rely on the
+ // VCardConverter handling the X-APPLE-OMIT-YEAR property for us.
+ $object = $object->convert(Document::VCARD40);
+
+ // Skip if the card has no FN property.
+ if (!isset($object->FN)) {
+ continue;
+ }
+
+ // Skip if the BDAY property is not of the right type.
+ if (!$object->BDAY instanceof Property\VCard\DateAndOrTime) {
+ continue;
+ }
+
+ // Skip if we can't parse the BDAY value.
+ try {
+ $dateParts = DateTimeParser::parseVCardDateTime($object->BDAY->getValue());
+ } catch (InvalidDataException $e) {
+ continue;
+ }
+
+ // Set a year if it's not set.
+ $unknownYear = false;
+
+ if (!$dateParts['year']) {
+ $object->BDAY = self::DEFAULT_YEAR.'-'.$dateParts['month'].'-'.$dateParts['date'];
+
+ $unknownYear = true;
+ }
+
+ // Create event.
+ $event = $calendar->add('VEVENT', [
+ 'SUMMARY' => sprintf($this->format, $object->FN->getValue()),
+ 'DTSTART' => new \DateTime($object->BDAY->getValue()),
+ 'RRULE' => 'FREQ=YEARLY',
+ 'TRANSP' => 'TRANSPARENT',
+ ]);
+
+ // add VALUE=date
+ $event->DTSTART['VALUE'] = 'DATE';
+
+ // Add X-SABRE-BDAY property.
+ if ($unknownYear) {
+ $event->add('X-SABRE-BDAY', 'BDAY', [
+ 'X-SABRE-VCARD-UID' => $object->UID->getValue(),
+ 'X-SABRE-VCARD-FN' => $object->FN->getValue(),
+ 'X-SABRE-OMIT-YEAR' => self::DEFAULT_YEAR,
+ ]);
+ } else {
+ $event->add('X-SABRE-BDAY', 'BDAY', [
+ 'X-SABRE-VCARD-UID' => $object->UID->getValue(),
+ 'X-SABRE-VCARD-FN' => $object->FN->getValue(),
+ ]);
+ }
+ }
+
+ return $calendar;
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Cli.php b/vendor/sabre/vobject/lib/Cli.php
new file mode 100644
index 0000000..3bde16f
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Cli.php
@@ -0,0 +1,705 @@
+stderr) {
+ $this->stderr = fopen('php://stderr', 'w');
+ }
+ if (!$this->stdout) {
+ $this->stdout = fopen('php://stdout', 'w');
+ }
+ if (!$this->stdin) {
+ $this->stdin = fopen('php://stdin', 'r');
+ }
+
+ // @codeCoverageIgnoreEnd
+
+ try {
+ list($options, $positional) = $this->parseArguments($argv);
+
+ if (isset($options['q'])) {
+ $this->quiet = true;
+ }
+ $this->log($this->colorize('green', 'sabre/vobject ').$this->colorize('yellow', Version::VERSION));
+
+ foreach ($options as $name => $value) {
+ switch ($name) {
+ case 'q':
+ // Already handled earlier.
+ break;
+ case 'h':
+ case 'help':
+ $this->showHelp();
+
+ return 0;
+ break;
+ case 'format':
+ switch ($value) {
+ // jcard/jcal documents
+ case 'jcard':
+ case 'jcal':
+ // specific document versions
+ case 'vcard21':
+ case 'vcard30':
+ case 'vcard40':
+ case 'icalendar20':
+ // specific formats
+ case 'json':
+ case 'mimedir':
+ // icalendar/vcad
+ case 'icalendar':
+ case 'vcard':
+ $this->format = $value;
+ break;
+
+ default:
+ throw new InvalidArgumentException('Unknown format: '.$value);
+ }
+ break;
+ case 'pretty':
+ if (version_compare(PHP_VERSION, '5.4.0') >= 0) {
+ $this->pretty = true;
+ }
+ break;
+ case 'forgiving':
+ $this->forgiving = true;
+ break;
+ case 'inputformat':
+ switch ($value) {
+ // json formats
+ case 'jcard':
+ case 'jcal':
+ case 'json':
+ $this->inputFormat = 'json';
+ break;
+
+ // mimedir formats
+ case 'mimedir':
+ case 'icalendar':
+ case 'vcard':
+ case 'vcard21':
+ case 'vcard30':
+ case 'vcard40':
+ case 'icalendar20':
+ $this->inputFormat = 'mimedir';
+ break;
+
+ default:
+ throw new InvalidArgumentException('Unknown format: '.$value);
+ }
+ break;
+ default:
+ throw new InvalidArgumentException('Unknown option: '.$name);
+ }
+ }
+
+ if (0 === count($positional)) {
+ $this->showHelp();
+
+ return 1;
+ }
+
+ if (1 === count($positional)) {
+ throw new InvalidArgumentException('Inputfile is a required argument');
+ }
+
+ if (count($positional) > 3) {
+ throw new InvalidArgumentException('Too many arguments');
+ }
+
+ if (!in_array($positional[0], ['validate', 'repair', 'convert', 'color'])) {
+ throw new InvalidArgumentException('Unknown command: '.$positional[0]);
+ }
+ } catch (InvalidArgumentException $e) {
+ $this->showHelp();
+ $this->log('Error: '.$e->getMessage(), 'red');
+
+ return 1;
+ }
+
+ $command = $positional[0];
+
+ $this->inputPath = $positional[1];
+ $this->outputPath = isset($positional[2]) ? $positional[2] : '-';
+
+ if ('-' !== $this->outputPath) {
+ $this->stdout = fopen($this->outputPath, 'w');
+ }
+
+ if (!$this->inputFormat) {
+ if ('.json' === substr($this->inputPath, -5)) {
+ $this->inputFormat = 'json';
+ } else {
+ $this->inputFormat = 'mimedir';
+ }
+ }
+ if (!$this->format) {
+ if ('.json' === substr($this->outputPath, -5)) {
+ $this->format = 'json';
+ } else {
+ $this->format = 'mimedir';
+ }
+ }
+
+ $realCode = 0;
+
+ try {
+ while ($input = $this->readInput()) {
+ $returnCode = $this->$command($input);
+ if (0 !== $returnCode) {
+ $realCode = $returnCode;
+ }
+ }
+ } catch (EofException $e) {
+ // end of file
+ } catch (\Exception $e) {
+ $this->log('Error: '.$e->getMessage(), 'red');
+
+ return 2;
+ }
+
+ return $realCode;
+ }
+
+ /**
+ * Shows the help message.
+ */
+ protected function showHelp()
+ {
+ $this->log('Usage:', 'yellow');
+ $this->log(' vobject [options] command [arguments]');
+ $this->log('');
+ $this->log('Options:', 'yellow');
+ $this->log($this->colorize('green', ' -q ')."Don't output anything.");
+ $this->log($this->colorize('green', ' -help -h ').'Display this help message.');
+ $this->log($this->colorize('green', ' --format ').'Convert to a specific format. Must be one of: vcard, vcard21,');
+ $this->log($this->colorize('green', ' --forgiving ').'Makes the parser less strict.');
+ $this->log(' vcard30, vcard40, icalendar20, jcal, jcard, json, mimedir.');
+ $this->log($this->colorize('green', ' --inputformat ').'If the input format cannot be guessed from the extension, it');
+ $this->log(' must be specified here.');
+ // Only PHP 5.4 and up
+ if (version_compare(PHP_VERSION, '5.4.0') >= 0) {
+ $this->log($this->colorize('green', ' --pretty ').'json pretty-print.');
+ }
+ $this->log('');
+ $this->log('Commands:', 'yellow');
+ $this->log($this->colorize('green', ' validate').' source_file Validates a file for correctness.');
+ $this->log($this->colorize('green', ' repair').' source_file [output_file] Repairs a file.');
+ $this->log($this->colorize('green', ' convert').' source_file [output_file] Converts a file.');
+ $this->log($this->colorize('green', ' color').' source_file Colorize a file, useful for debugging.');
+ $this->log(
+ <<log('Examples:', 'yellow');
+ $this->log(' vobject convert contact.vcf contact.json');
+ $this->log(' vobject convert --format=vcard40 old.vcf new.vcf');
+ $this->log(' vobject convert --inputformat=json --format=mimedir - -');
+ $this->log(' vobject color calendar.ics');
+ $this->log('');
+ $this->log('https://github.com/fruux/sabre-vobject', 'purple');
+ }
+
+ /**
+ * Validates a VObject file.
+ *
+ * @return int
+ */
+ protected function validate(Component $vObj)
+ {
+ $returnCode = 0;
+
+ switch ($vObj->name) {
+ case 'VCALENDAR':
+ $this->log('iCalendar: '.(string) $vObj->VERSION);
+ break;
+ case 'VCARD':
+ $this->log('vCard: '.(string) $vObj->VERSION);
+ break;
+ }
+
+ $warnings = $vObj->validate();
+ if (!count($warnings)) {
+ $this->log(' No warnings!');
+ } else {
+ $levels = [
+ 1 => 'REPAIRED',
+ 2 => 'WARNING',
+ 3 => 'ERROR',
+ ];
+ $returnCode = 2;
+ foreach ($warnings as $warn) {
+ $extra = '';
+ if ($warn['node'] instanceof Property) {
+ $extra = ' (property: "'.$warn['node']->name.'")';
+ }
+ $this->log(' ['.$levels[$warn['level']].'] '.$warn['message'].$extra);
+ }
+ }
+
+ return $returnCode;
+ }
+
+ /**
+ * Repairs a VObject file.
+ *
+ * @return int
+ */
+ protected function repair(Component $vObj)
+ {
+ $returnCode = 0;
+
+ switch ($vObj->name) {
+ case 'VCALENDAR':
+ $this->log('iCalendar: '.(string) $vObj->VERSION);
+ break;
+ case 'VCARD':
+ $this->log('vCard: '.(string) $vObj->VERSION);
+ break;
+ }
+
+ $warnings = $vObj->validate(Node::REPAIR);
+ if (!count($warnings)) {
+ $this->log(' No warnings!');
+ } else {
+ $levels = [
+ 1 => 'REPAIRED',
+ 2 => 'WARNING',
+ 3 => 'ERROR',
+ ];
+ $returnCode = 2;
+ foreach ($warnings as $warn) {
+ $extra = '';
+ if ($warn['node'] instanceof Property) {
+ $extra = ' (property: "'.$warn['node']->name.'")';
+ }
+ $this->log(' ['.$levels[$warn['level']].'] '.$warn['message'].$extra);
+ }
+ }
+ fwrite($this->stdout, $vObj->serialize());
+
+ return $returnCode;
+ }
+
+ /**
+ * Converts a vObject file to a new format.
+ *
+ * @param Component $vObj
+ *
+ * @return int
+ */
+ protected function convert($vObj)
+ {
+ $json = false;
+ $convertVersion = null;
+ $forceInput = null;
+
+ switch ($this->format) {
+ case 'json':
+ $json = true;
+ if ('VCARD' === $vObj->name) {
+ $convertVersion = Document::VCARD40;
+ }
+ break;
+ case 'jcard':
+ $json = true;
+ $forceInput = 'VCARD';
+ $convertVersion = Document::VCARD40;
+ break;
+ case 'jcal':
+ $json = true;
+ $forceInput = 'VCALENDAR';
+ break;
+ case 'mimedir':
+ case 'icalendar':
+ case 'icalendar20':
+ case 'vcard':
+ break;
+ case 'vcard21':
+ $convertVersion = Document::VCARD21;
+ break;
+ case 'vcard30':
+ $convertVersion = Document::VCARD30;
+ break;
+ case 'vcard40':
+ $convertVersion = Document::VCARD40;
+ break;
+ }
+
+ if ($forceInput && $vObj->name !== $forceInput) {
+ throw new \Exception('You cannot convert a '.strtolower($vObj->name).' to '.$this->format);
+ }
+ if ($convertVersion) {
+ $vObj = $vObj->convert($convertVersion);
+ }
+ if ($json) {
+ $jsonOptions = 0;
+ if ($this->pretty) {
+ $jsonOptions = JSON_PRETTY_PRINT;
+ }
+ fwrite($this->stdout, json_encode($vObj->jsonSerialize(), $jsonOptions));
+ } else {
+ fwrite($this->stdout, $vObj->serialize());
+ }
+
+ return 0;
+ }
+
+ /**
+ * Colorizes a file.
+ *
+ * @param Component $vObj
+ */
+ protected function color($vObj)
+ {
+ $this->serializeComponent($vObj);
+ }
+
+ /**
+ * Returns an ansi color string for a color name.
+ *
+ * @param string $color
+ *
+ * @return string
+ */
+ protected function colorize($color, $str, $resetTo = 'default')
+ {
+ $colors = [
+ 'cyan' => '1;36',
+ 'red' => '1;31',
+ 'yellow' => '1;33',
+ 'blue' => '0;34',
+ 'green' => '0;32',
+ 'default' => '0',
+ 'purple' => '0;35',
+ ];
+
+ return "\033[".$colors[$color].'m'.$str."\033[".$colors[$resetTo].'m';
+ }
+
+ /**
+ * Writes out a string in specific color.
+ *
+ * @param string $color
+ * @param string $str
+ */
+ protected function cWrite($color, $str)
+ {
+ fwrite($this->stdout, $this->colorize($color, $str));
+ }
+
+ protected function serializeComponent(Component $vObj)
+ {
+ $this->cWrite('cyan', 'BEGIN');
+ $this->cWrite('red', ':');
+ $this->cWrite('yellow', $vObj->name."\n");
+
+ /**
+ * Gives a component a 'score' for sorting purposes.
+ *
+ * This is solely used by the childrenSort method.
+ *
+ * A higher score means the item will be lower in the list.
+ * To avoid score collisions, each "score category" has a reasonable
+ * space to accommodate elements. The $key is added to the $score to
+ * preserve the original relative order of elements.
+ *
+ * @param int $key
+ * @param array $array
+ *
+ * @return int
+ */
+ $sortScore = function ($key, $array) {
+ if ($array[$key] instanceof Component) {
+ // We want to encode VTIMEZONE first, this is a personal
+ // preference.
+ if ('VTIMEZONE' === $array[$key]->name) {
+ $score = 300000000;
+
+ return $score + $key;
+ } else {
+ $score = 400000000;
+
+ return $score + $key;
+ }
+ } else {
+ // Properties get encoded first
+ // VCARD version 4.0 wants the VERSION property to appear first
+ if ($array[$key] instanceof Property) {
+ if ('VERSION' === $array[$key]->name) {
+ $score = 100000000;
+
+ return $score + $key;
+ } else {
+ // All other properties
+ $score = 200000000;
+
+ return $score + $key;
+ }
+ }
+ }
+ };
+
+ $children = $vObj->children();
+ $tmp = $children;
+ uksort(
+ $children,
+ function ($a, $b) use ($sortScore, $tmp) {
+ $sA = $sortScore($a, $tmp);
+ $sB = $sortScore($b, $tmp);
+
+ return $sA - $sB;
+ }
+ );
+
+ foreach ($children as $child) {
+ if ($child instanceof Component) {
+ $this->serializeComponent($child);
+ } else {
+ $this->serializeProperty($child);
+ }
+ }
+
+ $this->cWrite('cyan', 'END');
+ $this->cWrite('red', ':');
+ $this->cWrite('yellow', $vObj->name."\n");
+ }
+
+ /**
+ * Colorizes a property.
+ */
+ protected function serializeProperty(Property $property)
+ {
+ if ($property->group) {
+ $this->cWrite('default', $property->group);
+ $this->cWrite('red', '.');
+ }
+
+ $this->cWrite('yellow', $property->name);
+
+ foreach ($property->parameters as $param) {
+ $this->cWrite('red', ';');
+ $this->cWrite('blue', $param->serialize());
+ }
+ $this->cWrite('red', ':');
+
+ if ($property instanceof Property\Binary) {
+ $this->cWrite('default', 'embedded binary stripped. ('.strlen($property->getValue()).' bytes)');
+ } else {
+ $parts = $property->getParts();
+ $first1 = true;
+ // Looping through property values
+ foreach ($parts as $part) {
+ if ($first1) {
+ $first1 = false;
+ } else {
+ $this->cWrite('red', $property->delimiter);
+ }
+ $first2 = true;
+ // Looping through property sub-values
+ foreach ((array) $part as $subPart) {
+ if ($first2) {
+ $first2 = false;
+ } else {
+ // The sub-value delimiter is always comma
+ $this->cWrite('red', ',');
+ }
+
+ $subPart = strtr(
+ $subPart,
+ [
+ '\\' => $this->colorize('purple', '\\\\', 'green'),
+ ';' => $this->colorize('purple', '\;', 'green'),
+ ',' => $this->colorize('purple', '\,', 'green'),
+ "\n" => $this->colorize('purple', "\\n\n\t", 'green'),
+ "\r" => '',
+ ]
+ );
+
+ $this->cWrite('green', $subPart);
+ }
+ }
+ }
+ $this->cWrite('default', "\n");
+ }
+
+ /**
+ * Parses the list of arguments.
+ */
+ protected function parseArguments(array $argv)
+ {
+ $positional = [];
+ $options = [];
+
+ for ($ii = 0; $ii < count($argv); ++$ii) {
+ // Skipping the first argument.
+ if (0 === $ii) {
+ continue;
+ }
+
+ $v = $argv[$ii];
+
+ if ('--' === substr($v, 0, 2)) {
+ // This is a long-form option.
+ $optionName = substr($v, 2);
+ $optionValue = true;
+ if (strpos($optionName, '=')) {
+ list($optionName, $optionValue) = explode('=', $optionName);
+ }
+ $options[$optionName] = $optionValue;
+ } elseif ('-' === substr($v, 0, 1) && strlen($v) > 1) {
+ // This is a short-form option.
+ foreach (str_split(substr($v, 1)) as $option) {
+ $options[$option] = true;
+ }
+ } else {
+ $positional[] = $v;
+ }
+ }
+
+ return [$options, $positional];
+ }
+
+ protected $parser;
+
+ /**
+ * Reads the input file.
+ *
+ * @return Component
+ */
+ protected function readInput()
+ {
+ if (!$this->parser) {
+ if ('-' !== $this->inputPath) {
+ $this->stdin = fopen($this->inputPath, 'r');
+ }
+
+ if ('mimedir' === $this->inputFormat) {
+ $this->parser = new Parser\MimeDir($this->stdin, ($this->forgiving ? Reader::OPTION_FORGIVING : 0));
+ } else {
+ $this->parser = new Parser\Json($this->stdin, ($this->forgiving ? Reader::OPTION_FORGIVING : 0));
+ }
+ }
+
+ return $this->parser->parse();
+ }
+
+ /**
+ * Sends a message to STDERR.
+ *
+ * @param string $msg
+ */
+ protected function log($msg, $color = 'default')
+ {
+ if (!$this->quiet) {
+ if ('default' !== $color) {
+ $msg = $this->colorize($color, $msg);
+ }
+ fwrite($this->stderr, $msg."\n");
+ }
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Component.php b/vendor/sabre/vobject/lib/Component.php
new file mode 100644
index 0000000..ca82ad4
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Component.php
@@ -0,0 +1,672 @@
+
+ */
+ protected $children = [];
+
+ /**
+ * Creates a new component.
+ *
+ * You can specify the children either in key=>value syntax, in which case
+ * properties will automatically be created, or you can just pass a list of
+ * Component and Property object.
+ *
+ * By default, a set of sensible values will be added to the component. For
+ * an iCalendar object, this may be something like CALSCALE:GREGORIAN. To
+ * ensure that this does not happen, set $defaults to false.
+ *
+ * @param string|null $name such as VCALENDAR, VEVENT
+ * @param bool $defaults
+ */
+ public function __construct(Document $root, $name, array $children = [], $defaults = true)
+ {
+ $this->name = isset($name) ? strtoupper($name) : '';
+ $this->root = $root;
+
+ if ($defaults) {
+ // This is a terribly convoluted way to do this, but this ensures
+ // that the order of properties as they are specified in both
+ // defaults and the childrens list, are inserted in the object in a
+ // natural way.
+ $list = $this->getDefaults();
+ $nodes = [];
+ foreach ($children as $key => $value) {
+ if ($value instanceof Node) {
+ if (isset($list[$value->name])) {
+ unset($list[$value->name]);
+ }
+ $nodes[] = $value;
+ } else {
+ $list[$key] = $value;
+ }
+ }
+ foreach ($list as $key => $value) {
+ $this->add($key, $value);
+ }
+ foreach ($nodes as $node) {
+ $this->add($node);
+ }
+ } else {
+ foreach ($children as $k => $child) {
+ if ($child instanceof Node) {
+ // Component or Property
+ $this->add($child);
+ } else {
+ // Property key=>value
+ $this->add($k, $child);
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds a new property or component, and returns the new item.
+ *
+ * This method has 3 possible signatures:
+ *
+ * add(Component $comp) // Adds a new component
+ * add(Property $prop) // Adds a new property
+ * add($name, $value, array $parameters = []) // Adds a new property
+ * add($name, array $children = []) // Adds a new component
+ * by name.
+ *
+ * @return Node
+ */
+ public function add()
+ {
+ $arguments = func_get_args();
+
+ if ($arguments[0] instanceof Node) {
+ if (isset($arguments[1])) {
+ throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject Node');
+ }
+ $arguments[0]->parent = $this;
+ $newNode = $arguments[0];
+ } elseif (is_string($arguments[0])) {
+ $newNode = call_user_func_array([$this->root, 'create'], $arguments);
+ } else {
+ throw new \InvalidArgumentException('The first argument must either be a \\Sabre\\VObject\\Node or a string');
+ }
+
+ $name = $newNode->name;
+ if (isset($this->children[$name])) {
+ $this->children[$name][] = $newNode;
+ } else {
+ $this->children[$name] = [$newNode];
+ }
+
+ return $newNode;
+ }
+
+ /**
+ * This method removes a component or property from this component.
+ *
+ * You can either specify the item by name (like DTSTART), in which case
+ * all properties/components with that name will be removed, or you can
+ * pass an instance of a property or component, in which case only that
+ * exact item will be removed.
+ *
+ * @param string|Property|Component $item
+ */
+ public function remove($item)
+ {
+ if (is_string($item)) {
+ // If there's no dot in the name, it's an exact property name and
+ // we can just wipe out all those properties.
+ //
+ if (false === strpos($item, '.')) {
+ unset($this->children[strtoupper($item)]);
+
+ return;
+ }
+ // If there was a dot, we need to ask select() to help us out and
+ // then we just call remove recursively.
+ foreach ($this->select($item) as $child) {
+ $this->remove($child);
+ }
+ } else {
+ foreach ($this->select($item->name) as $k => $child) {
+ if ($child === $item) {
+ unset($this->children[$item->name][$k]);
+
+ return;
+ }
+ }
+
+ throw new \InvalidArgumentException('The item you passed to remove() was not a child of this component');
+ }
+ }
+
+ /**
+ * Returns a flat list of all the properties and components in this
+ * component.
+ *
+ * @return array
+ */
+ public function children()
+ {
+ $result = [];
+ foreach ($this->children as $childGroup) {
+ $result = array_merge($result, $childGroup);
+ }
+
+ return $result;
+ }
+
+ /**
+ * This method only returns a list of sub-components. Properties are
+ * ignored.
+ *
+ * @return array
+ */
+ public function getComponents()
+ {
+ $result = [];
+
+ foreach ($this->children as $childGroup) {
+ foreach ($childGroup as $child) {
+ if ($child instanceof self) {
+ $result[] = $child;
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns an array with elements that match the specified name.
+ *
+ * This function is also aware of MIME-Directory groups (as they appear in
+ * vcards). This means that if a property is grouped as "HOME.EMAIL", it
+ * will also be returned when searching for just "EMAIL". If you want to
+ * search for a property in a specific group, you can select on the entire
+ * string ("HOME.EMAIL"). If you want to search on a specific property that
+ * has not been assigned a group, specify ".EMAIL".
+ *
+ * @param string $name
+ *
+ * @return array
+ */
+ public function select($name)
+ {
+ $group = null;
+ $name = strtoupper($name);
+ if (false !== strpos($name, '.')) {
+ list($group, $name) = explode('.', $name, 2);
+ }
+ if ('' === $name) {
+ $name = null;
+ }
+
+ if (!is_null($name)) {
+ $result = isset($this->children[$name]) ? $this->children[$name] : [];
+
+ if (is_null($group)) {
+ return $result;
+ } else {
+ // If we have a group filter as well, we need to narrow it down
+ // more.
+ return array_filter(
+ $result,
+ function ($child) use ($group) {
+ return $child instanceof Property && (null !== $child->group ? strtoupper($child->group) : '') === $group;
+ }
+ );
+ }
+ }
+
+ // If we got to this point, it means there was no 'name' specified for
+ // searching, implying that this is a group-only search.
+ $result = [];
+ foreach ($this->children as $childGroup) {
+ foreach ($childGroup as $child) {
+ if ($child instanceof Property && (null !== $child->group ? strtoupper($child->group) : '') === $group) {
+ $result[] = $child;
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Turns the object back into a serialized blob.
+ *
+ * @return string
+ */
+ public function serialize()
+ {
+ $str = 'BEGIN:'.$this->name."\r\n";
+
+ /**
+ * Gives a component a 'score' for sorting purposes.
+ *
+ * This is solely used by the childrenSort method.
+ *
+ * A higher score means the item will be lower in the list.
+ * To avoid score collisions, each "score category" has a reasonable
+ * space to accommodate elements. The $key is added to the $score to
+ * preserve the original relative order of elements.
+ *
+ * @param int $key
+ * @param array $array
+ *
+ * @return int
+ */
+ $sortScore = function ($key, $array) {
+ if ($array[$key] instanceof Component) {
+ // We want to encode VTIMEZONE first, this is a personal
+ // preference.
+ if ('VTIMEZONE' === $array[$key]->name) {
+ $score = 300000000;
+
+ return $score + $key;
+ } else {
+ $score = 400000000;
+
+ return $score + $key;
+ }
+ } else {
+ // Properties get encoded first
+ // VCARD version 4.0 wants the VERSION property to appear first
+ if ($array[$key] instanceof Property) {
+ if ('VERSION' === $array[$key]->name) {
+ $score = 100000000;
+
+ return $score + $key;
+ } else {
+ // All other properties
+ $score = 200000000;
+
+ return $score + $key;
+ }
+ }
+ }
+ };
+
+ $children = $this->children();
+ $tmp = $children;
+ uksort(
+ $children,
+ function ($a, $b) use ($sortScore, $tmp) {
+ $sA = $sortScore($a, $tmp);
+ $sB = $sortScore($b, $tmp);
+
+ return $sA - $sB;
+ }
+ );
+
+ foreach ($children as $child) {
+ $str .= $child->serialize();
+ }
+ $str .= 'END:'.$this->name."\r\n";
+
+ return $str;
+ }
+
+ /**
+ * This method returns an array, with the representation as it should be
+ * encoded in JSON. This is used to create jCard or jCal documents.
+ *
+ * @return array
+ */
+ #[\ReturnTypeWillChange]
+ public function jsonSerialize()
+ {
+ $components = [];
+ $properties = [];
+
+ foreach ($this->children as $childGroup) {
+ foreach ($childGroup as $child) {
+ if ($child instanceof self) {
+ $components[] = $child->jsonSerialize();
+ } else {
+ $properties[] = $child->jsonSerialize();
+ }
+ }
+ }
+
+ return [
+ strtolower($this->name),
+ $properties,
+ $components,
+ ];
+ }
+
+ /**
+ * This method serializes the data into XML. This is used to create xCard or
+ * xCal documents.
+ *
+ * @param Xml\Writer $writer XML writer
+ */
+ public function xmlSerialize(Xml\Writer $writer): void
+ {
+ $components = [];
+ $properties = [];
+
+ foreach ($this->children as $childGroup) {
+ foreach ($childGroup as $child) {
+ if ($child instanceof self) {
+ $components[] = $child;
+ } else {
+ $properties[] = $child;
+ }
+ }
+ }
+
+ $writer->startElement(strtolower($this->name));
+
+ if (!empty($properties)) {
+ $writer->startElement('properties');
+
+ foreach ($properties as $property) {
+ $property->xmlSerialize($writer);
+ }
+
+ $writer->endElement();
+ }
+
+ if (!empty($components)) {
+ $writer->startElement('components');
+
+ foreach ($components as $component) {
+ $component->xmlSerialize($writer);
+ }
+
+ $writer->endElement();
+ }
+
+ $writer->endElement();
+ }
+
+ /**
+ * This method should return a list of default property values.
+ *
+ * @return array
+ */
+ protected function getDefaults()
+ {
+ return [];
+ }
+
+ /* Magic property accessors {{{ */
+
+ /**
+ * Using 'get' you will either get a property or component.
+ *
+ * If there were no child-elements found with the specified name,
+ * null is returned.
+ *
+ * To use this, this may look something like this:
+ *
+ * $event = $calendar->VEVENT;
+ *
+ * @param string $name
+ *
+ * @return Property|null
+ */
+ public function __get($name)
+ {
+ if ('children' === $name) {
+ throw new \RuntimeException('Starting sabre/vobject 4.0 the children property is now protected. You should use the children() method instead');
+ }
+
+ $matches = $this->select($name);
+ if (0 === count($matches)) {
+ return;
+ } else {
+ $firstMatch = current($matches);
+ /* @var $firstMatch Property */
+ $firstMatch->setIterator(new ElementList(array_values($matches)));
+
+ return $firstMatch;
+ }
+ }
+
+ /**
+ * This method checks if a sub-element with the specified name exists.
+ *
+ * @param string $name
+ *
+ * @return bool
+ */
+ public function __isset($name)
+ {
+ $matches = $this->select($name);
+
+ return count($matches) > 0;
+ }
+
+ /**
+ * Using the setter method you can add properties or subcomponents.
+ *
+ * You can either pass a Component, Property
+ * object, or a string to automatically create a Property.
+ *
+ * If the item already exists, it will be removed. If you want to add
+ * a new item with the same name, always use the add() method.
+ *
+ * @param string $name
+ * @param mixed $value
+ */
+ public function __set($name, $value)
+ {
+ $name = strtoupper($name);
+ $this->remove($name);
+ if ($value instanceof self || $value instanceof Property) {
+ $this->add($value);
+ } else {
+ $this->add($name, $value);
+ }
+ }
+
+ /**
+ * Removes all properties and components within this component with the
+ * specified name.
+ *
+ * @param string $name
+ */
+ public function __unset($name)
+ {
+ $this->remove($name);
+ }
+
+ /* }}} */
+
+ /**
+ * This method is automatically called when the object is cloned.
+ * Specifically, this will ensure all child elements are also cloned.
+ */
+ public function __clone()
+ {
+ foreach ($this->children as $childName => $childGroup) {
+ foreach ($childGroup as $key => $child) {
+ $clonedChild = clone $child;
+ $clonedChild->parent = $this;
+ $clonedChild->root = $this->root;
+ $this->children[$childName][$key] = $clonedChild;
+ }
+ }
+ }
+
+ /**
+ * A simple list of validation rules.
+ *
+ * This is simply a list of properties, and how many times they either
+ * must or must not appear.
+ *
+ * Possible values per property:
+ * * 0 - Must not appear.
+ * * 1 - Must appear exactly once.
+ * * + - Must appear at least once.
+ * * * - Can appear any number of times.
+ * * ? - May appear, but not more than once.
+ *
+ * It is also possible to specify defaults and severity levels for
+ * violating the rule.
+ *
+ * See the VEVENT implementation for getValidationRules for a more complex
+ * example.
+ *
+ * @var array
+ */
+ public function getValidationRules()
+ {
+ return [];
+ }
+
+ /**
+ * Validates the node for correctness.
+ *
+ * The following options are supported:
+ * Node::REPAIR - May attempt to automatically repair the problem.
+ * Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes.
+ * Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes.
+ *
+ * This method returns an array with detected problems.
+ * Every element has the following properties:
+ *
+ * * level - problem level.
+ * * message - A human-readable string describing the issue.
+ * * node - A reference to the problematic node.
+ *
+ * The level means:
+ * 1 - The issue was repaired (only happens if REPAIR was turned on).
+ * 2 - A warning.
+ * 3 - An error.
+ *
+ * @param int $options
+ *
+ * @return array
+ */
+ public function validate($options = 0)
+ {
+ $rules = $this->getValidationRules();
+ $defaults = $this->getDefaults();
+
+ $propertyCounters = [];
+
+ $messages = [];
+
+ foreach ($this->children() as $child) {
+ $name = strtoupper($child->name);
+ if (!isset($propertyCounters[$name])) {
+ $propertyCounters[$name] = 1;
+ } else {
+ ++$propertyCounters[$name];
+ }
+ $messages = array_merge($messages, $child->validate($options));
+ }
+
+ foreach ($rules as $propName => $rule) {
+ switch ($rule) {
+ case '0':
+ if (isset($propertyCounters[$propName])) {
+ $messages[] = [
+ 'level' => 3,
+ 'message' => $propName.' MUST NOT appear in a '.$this->name.' component',
+ 'node' => $this,
+ ];
+ }
+ break;
+ case '1':
+ if (!isset($propertyCounters[$propName]) || 1 !== $propertyCounters[$propName]) {
+ $repaired = false;
+ if ($options & self::REPAIR && isset($defaults[$propName])) {
+ $this->add($propName, $defaults[$propName]);
+ $repaired = true;
+ }
+ $messages[] = [
+ 'level' => $repaired ? 1 : 3,
+ 'message' => $propName.' MUST appear exactly once in a '.$this->name.' component',
+ 'node' => $this,
+ ];
+ }
+ break;
+ case '+':
+ if (!isset($propertyCounters[$propName]) || $propertyCounters[$propName] < 1) {
+ $messages[] = [
+ 'level' => 3,
+ 'message' => $propName.' MUST appear at least once in a '.$this->name.' component',
+ 'node' => $this,
+ ];
+ }
+ break;
+ case '*':
+ break;
+ case '?':
+ if (isset($propertyCounters[$propName]) && $propertyCounters[$propName] > 1) {
+ $level = 3;
+
+ // We try to repair the same property appearing multiple times with the exact same value
+ // by removing the duplicates and keeping only one property
+ if ($options & self::REPAIR) {
+ $properties = array_unique($this->select($propName), SORT_REGULAR);
+
+ if (1 === count($properties)) {
+ $this->remove($propName);
+ $this->add($properties[0]);
+
+ $level = 1;
+ }
+ }
+
+ $messages[] = [
+ 'level' => $level,
+ 'message' => $propName.' MUST NOT appear more than once in a '.$this->name.' component',
+ 'node' => $this,
+ ];
+ }
+ break;
+ }
+ }
+
+ return $messages;
+ }
+
+ /**
+ * Call this method on a document if you're done using it.
+ *
+ * It's intended to remove all circular references, so PHP can easily clean
+ * it up.
+ */
+ public function destroy()
+ {
+ parent::destroy();
+ foreach ($this->children as $childGroup) {
+ foreach ($childGroup as $child) {
+ $child->destroy();
+ }
+ }
+ $this->children = [];
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Component/Available.php b/vendor/sabre/vobject/lib/Component/Available.php
new file mode 100644
index 0000000..5510b9e
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Component/Available.php
@@ -0,0 +1,123 @@
+DTSTART->getDateTime();
+ if (isset($this->DTEND)) {
+ $effectiveEnd = $this->DTEND->getDateTime();
+ } else {
+ $effectiveEnd = $effectiveStart->add(VObject\DateTimeParser::parseDuration($this->DURATION));
+ }
+
+ return [$effectiveStart, $effectiveEnd];
+ }
+
+ /**
+ * A simple list of validation rules.
+ *
+ * This is simply a list of properties, and how many times they either
+ * must or must not appear.
+ *
+ * Possible values per property:
+ * * 0 - Must not appear.
+ * * 1 - Must appear exactly once.
+ * * + - Must appear at least once.
+ * * * - Can appear any number of times.
+ * * ? - May appear, but not more than once.
+ *
+ * @var array
+ */
+ public function getValidationRules()
+ {
+ return [
+ 'UID' => 1,
+ 'DTSTART' => 1,
+ 'DTSTAMP' => 1,
+
+ 'DTEND' => '?',
+ 'DURATION' => '?',
+
+ 'CREATED' => '?',
+ 'DESCRIPTION' => '?',
+ 'LAST-MODIFIED' => '?',
+ 'RECURRENCE-ID' => '?',
+ 'RRULE' => '?',
+ 'SUMMARY' => '?',
+
+ 'CATEGORIES' => '*',
+ 'COMMENT' => '*',
+ 'CONTACT' => '*',
+ 'EXDATE' => '*',
+ 'RDATE' => '*',
+
+ 'AVAILABLE' => '*',
+ ];
+ }
+
+ /**
+ * Validates the node for correctness.
+ *
+ * The following options are supported:
+ * Node::REPAIR - May attempt to automatically repair the problem.
+ * Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes.
+ * Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes.
+ *
+ * This method returns an array with detected problems.
+ * Every element has the following properties:
+ *
+ * * level - problem level.
+ * * message - A human-readable string describing the issue.
+ * * node - A reference to the problematic node.
+ *
+ * The level means:
+ * 1 - The issue was repaired (only happens if REPAIR was turned on).
+ * 2 - A warning.
+ * 3 - An error.
+ *
+ * @param int $options
+ *
+ * @return array
+ */
+ public function validate($options = 0)
+ {
+ $result = parent::validate($options);
+
+ if (isset($this->DTEND) && isset($this->DURATION)) {
+ $result[] = [
+ 'level' => 3,
+ 'message' => 'DTEND and DURATION cannot both be present',
+ 'node' => $this,
+ ];
+ }
+
+ return $result;
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Component/VAlarm.php b/vendor/sabre/vobject/lib/Component/VAlarm.php
new file mode 100644
index 0000000..bd00eb6
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Component/VAlarm.php
@@ -0,0 +1,138 @@
+TRIGGER;
+ if (!isset($trigger['VALUE']) || 'DURATION' === strtoupper($trigger['VALUE'])) {
+ $triggerDuration = VObject\DateTimeParser::parseDuration($this->TRIGGER);
+ $related = (isset($trigger['RELATED']) && 'END' == strtoupper($trigger['RELATED'])) ? 'END' : 'START';
+
+ $parentComponent = $this->parent;
+ if ('START' === $related) {
+ if ('VTODO' === $parentComponent->name) {
+ $propName = 'DUE';
+ } else {
+ $propName = 'DTSTART';
+ }
+
+ $effectiveTrigger = $parentComponent->$propName->getDateTime();
+ $effectiveTrigger = $effectiveTrigger->add($triggerDuration);
+ } else {
+ if ('VTODO' === $parentComponent->name) {
+ $endProp = 'DUE';
+ } elseif ('VEVENT' === $parentComponent->name) {
+ $endProp = 'DTEND';
+ } else {
+ throw new InvalidDataException('time-range filters on VALARM components are only supported when they are a child of VTODO or VEVENT');
+ }
+
+ if (isset($parentComponent->$endProp)) {
+ $effectiveTrigger = $parentComponent->$endProp->getDateTime();
+ $effectiveTrigger = $effectiveTrigger->add($triggerDuration);
+ } elseif (isset($parentComponent->DURATION)) {
+ $effectiveTrigger = $parentComponent->DTSTART->getDateTime();
+ $duration = VObject\DateTimeParser::parseDuration($parentComponent->DURATION);
+ $effectiveTrigger = $effectiveTrigger->add($duration);
+ $effectiveTrigger = $effectiveTrigger->add($triggerDuration);
+ } else {
+ $effectiveTrigger = $parentComponent->DTSTART->getDateTime();
+ $effectiveTrigger = $effectiveTrigger->add($triggerDuration);
+ }
+ }
+ } else {
+ $effectiveTrigger = $trigger->getDateTime();
+ }
+
+ return $effectiveTrigger;
+ }
+
+ /**
+ * Returns true or false depending on if the event falls in the specified
+ * time-range. This is used for filtering purposes.
+ *
+ * The rules used to determine if an event falls within the specified
+ * time-range is based on the CalDAV specification.
+ *
+ * @param DateTime $start
+ * @param DateTime $end
+ *
+ * @return bool
+ */
+ public function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end)
+ {
+ $effectiveTrigger = $this->getEffectiveTriggerTime();
+
+ if (isset($this->DURATION)) {
+ $duration = VObject\DateTimeParser::parseDuration($this->DURATION);
+ $repeat = (string) $this->REPEAT;
+ if (!$repeat) {
+ $repeat = 1;
+ }
+
+ $period = new \DatePeriod($effectiveTrigger, $duration, (int) $repeat);
+
+ foreach ($period as $occurrence) {
+ if ($start <= $occurrence && $end > $occurrence) {
+ return true;
+ }
+ }
+
+ return false;
+ } else {
+ return $start <= $effectiveTrigger && $end > $effectiveTrigger;
+ }
+ }
+
+ /**
+ * A simple list of validation rules.
+ *
+ * This is simply a list of properties, and how many times they either
+ * must or must not appear.
+ *
+ * Possible values per property:
+ * * 0 - Must not appear.
+ * * 1 - Must appear exactly once.
+ * * + - Must appear at least once.
+ * * * - Can appear any number of times.
+ * * ? - May appear, but not more than once.
+ *
+ * @var array
+ */
+ public function getValidationRules()
+ {
+ return [
+ 'ACTION' => 1,
+ 'TRIGGER' => 1,
+
+ 'DURATION' => '?',
+ 'REPEAT' => '?',
+
+ 'ATTACH' => '?',
+ ];
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Component/VAvailability.php b/vendor/sabre/vobject/lib/Component/VAvailability.php
new file mode 100644
index 0000000..04ec38d
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Component/VAvailability.php
@@ -0,0 +1,149 @@
+getEffectiveStartEnd();
+
+ return
+ (is_null($effectiveStart) || $start < $effectiveEnd) &&
+ (is_null($effectiveEnd) || $end > $effectiveStart)
+ ;
+ }
+
+ /**
+ * Returns the 'effective start' and 'effective end' of this VAVAILABILITY
+ * component.
+ *
+ * We use the DTSTART and DTEND or DURATION to determine this.
+ *
+ * The returned value is an array containing DateTimeImmutable instances.
+ * If either the start or end is 'unbounded' its value will be null
+ * instead.
+ *
+ * @return array
+ */
+ public function getEffectiveStartEnd()
+ {
+ $effectiveStart = null;
+ $effectiveEnd = null;
+
+ if (isset($this->DTSTART)) {
+ $effectiveStart = $this->DTSTART->getDateTime();
+ }
+ if (isset($this->DTEND)) {
+ $effectiveEnd = $this->DTEND->getDateTime();
+ } elseif ($effectiveStart && isset($this->DURATION)) {
+ $effectiveEnd = $effectiveStart->add(VObject\DateTimeParser::parseDuration($this->DURATION));
+ }
+
+ return [$effectiveStart, $effectiveEnd];
+ }
+
+ /**
+ * A simple list of validation rules.
+ *
+ * This is simply a list of properties, and how many times they either
+ * must or must not appear.
+ *
+ * Possible values per property:
+ * * 0 - Must not appear.
+ * * 1 - Must appear exactly once.
+ * * + - Must appear at least once.
+ * * * - Can appear any number of times.
+ * * ? - May appear, but not more than once.
+ *
+ * @var array
+ */
+ public function getValidationRules()
+ {
+ return [
+ 'UID' => 1,
+ 'DTSTAMP' => 1,
+
+ 'BUSYTYPE' => '?',
+ 'CLASS' => '?',
+ 'CREATED' => '?',
+ 'DESCRIPTION' => '?',
+ 'DTSTART' => '?',
+ 'LAST-MODIFIED' => '?',
+ 'ORGANIZER' => '?',
+ 'PRIORITY' => '?',
+ 'SEQUENCE' => '?',
+ 'SUMMARY' => '?',
+ 'URL' => '?',
+ 'DTEND' => '?',
+ 'DURATION' => '?',
+
+ 'CATEGORIES' => '*',
+ 'COMMENT' => '*',
+ 'CONTACT' => '*',
+ ];
+ }
+
+ /**
+ * Validates the node for correctness.
+ *
+ * The following options are supported:
+ * Node::REPAIR - May attempt to automatically repair the problem.
+ * Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes.
+ * Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes.
+ *
+ * This method returns an array with detected problems.
+ * Every element has the following properties:
+ *
+ * * level - problem level.
+ * * message - A human-readable string describing the issue.
+ * * node - A reference to the problematic node.
+ *
+ * The level means:
+ * 1 - The issue was repaired (only happens if REPAIR was turned on).
+ * 2 - A warning.
+ * 3 - An error.
+ *
+ * @param int $options
+ *
+ * @return array
+ */
+ public function validate($options = 0)
+ {
+ $result = parent::validate($options);
+
+ if (isset($this->DTEND) && isset($this->DURATION)) {
+ $result[] = [
+ 'level' => 3,
+ 'message' => 'DTEND and DURATION cannot both be present',
+ 'node' => $this,
+ ];
+ }
+
+ return $result;
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Component/VCalendar.php b/vendor/sabre/vobject/lib/Component/VCalendar.php
new file mode 100644
index 0000000..017aed7
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Component/VCalendar.php
@@ -0,0 +1,528 @@
+ self::class,
+ 'VALARM' => VAlarm::class,
+ 'VEVENT' => VEvent::class,
+ 'VFREEBUSY' => VFreeBusy::class,
+ 'VAVAILABILITY' => VAvailability::class,
+ 'AVAILABLE' => Available::class,
+ 'VJOURNAL' => VJournal::class,
+ 'VTIMEZONE' => VTimeZone::class,
+ 'VTODO' => VTodo::class,
+ ];
+
+ /**
+ * List of value-types, and which classes they map to.
+ *
+ * @var array
+ */
+ public static $valueMap = [
+ 'BINARY' => VObject\Property\Binary::class,
+ 'BOOLEAN' => VObject\Property\Boolean::class,
+ 'CAL-ADDRESS' => VObject\Property\ICalendar\CalAddress::class,
+ 'DATE' => VObject\Property\ICalendar\Date::class,
+ 'DATE-TIME' => VObject\Property\ICalendar\DateTime::class,
+ 'DURATION' => VObject\Property\ICalendar\Duration::class,
+ 'FLOAT' => VObject\Property\FloatValue::class,
+ 'INTEGER' => VObject\Property\IntegerValue::class,
+ 'PERIOD' => VObject\Property\ICalendar\Period::class,
+ 'RECUR' => VObject\Property\ICalendar\Recur::class,
+ 'TEXT' => VObject\Property\Text::class,
+ 'TIME' => VObject\Property\Time::class,
+ 'UNKNOWN' => VObject\Property\Unknown::class, // jCard / jCal-only.
+ 'URI' => VObject\Property\Uri::class,
+ 'UTC-OFFSET' => VObject\Property\UtcOffset::class,
+ ];
+
+ /**
+ * List of properties, and which classes they map to.
+ *
+ * @var array
+ */
+ public static $propertyMap = [
+ // Calendar properties
+ 'CALSCALE' => VObject\Property\FlatText::class,
+ 'METHOD' => VObject\Property\FlatText::class,
+ 'PRODID' => VObject\Property\FlatText::class,
+ 'VERSION' => VObject\Property\FlatText::class,
+
+ // Component properties
+ 'ATTACH' => VObject\Property\Uri::class,
+ 'CATEGORIES' => VObject\Property\Text::class,
+ 'CLASS' => VObject\Property\FlatText::class,
+ 'COMMENT' => VObject\Property\FlatText::class,
+ 'DESCRIPTION' => VObject\Property\FlatText::class,
+ 'GEO' => VObject\Property\FloatValue::class,
+ 'LOCATION' => VObject\Property\FlatText::class,
+ 'PERCENT-COMPLETE' => VObject\Property\IntegerValue::class,
+ 'PRIORITY' => VObject\Property\IntegerValue::class,
+ 'RESOURCES' => VObject\Property\Text::class,
+ 'STATUS' => VObject\Property\FlatText::class,
+ 'SUMMARY' => VObject\Property\FlatText::class,
+
+ // Date and Time Component Properties
+ 'COMPLETED' => VObject\Property\ICalendar\DateTime::class,
+ 'DTEND' => VObject\Property\ICalendar\DateTime::class,
+ 'DUE' => VObject\Property\ICalendar\DateTime::class,
+ 'DTSTART' => VObject\Property\ICalendar\DateTime::class,
+ 'DURATION' => VObject\Property\ICalendar\Duration::class,
+ 'FREEBUSY' => VObject\Property\ICalendar\Period::class,
+ 'TRANSP' => VObject\Property\FlatText::class,
+
+ // Time Zone Component Properties
+ 'TZID' => VObject\Property\FlatText::class,
+ 'TZNAME' => VObject\Property\FlatText::class,
+ 'TZOFFSETFROM' => VObject\Property\UtcOffset::class,
+ 'TZOFFSETTO' => VObject\Property\UtcOffset::class,
+ 'TZURL' => VObject\Property\Uri::class,
+
+ // Relationship Component Properties
+ 'ATTENDEE' => VObject\Property\ICalendar\CalAddress::class,
+ 'CONTACT' => VObject\Property\FlatText::class,
+ 'ORGANIZER' => VObject\Property\ICalendar\CalAddress::class,
+ 'RECURRENCE-ID' => VObject\Property\ICalendar\DateTime::class,
+ 'RELATED-TO' => VObject\Property\FlatText::class,
+ 'URL' => VObject\Property\Uri::class,
+ 'UID' => VObject\Property\FlatText::class,
+
+ // Recurrence Component Properties
+ 'EXDATE' => VObject\Property\ICalendar\DateTime::class,
+ 'RDATE' => VObject\Property\ICalendar\DateTime::class,
+ 'RRULE' => VObject\Property\ICalendar\Recur::class,
+ 'EXRULE' => VObject\Property\ICalendar\Recur::class, // Deprecated since rfc5545
+
+ // Alarm Component Properties
+ 'ACTION' => VObject\Property\FlatText::class,
+ 'REPEAT' => VObject\Property\IntegerValue::class,
+ 'TRIGGER' => VObject\Property\ICalendar\Duration::class,
+
+ // Change Management Component Properties
+ 'CREATED' => VObject\Property\ICalendar\DateTime::class,
+ 'DTSTAMP' => VObject\Property\ICalendar\DateTime::class,
+ 'LAST-MODIFIED' => VObject\Property\ICalendar\DateTime::class,
+ 'SEQUENCE' => VObject\Property\IntegerValue::class,
+
+ // Request Status
+ 'REQUEST-STATUS' => VObject\Property\Text::class,
+
+ // Additions from draft-daboo-valarm-extensions-04
+ 'ALARM-AGENT' => VObject\Property\Text::class,
+ 'ACKNOWLEDGED' => VObject\Property\ICalendar\DateTime::class,
+ 'PROXIMITY' => VObject\Property\Text::class,
+ 'DEFAULT-ALARM' => VObject\Property\Boolean::class,
+
+ // Additions from draft-daboo-calendar-availability-05
+ 'BUSYTYPE' => VObject\Property\Text::class,
+ ];
+
+ /**
+ * Returns the current document type.
+ *
+ * @return int
+ */
+ public function getDocumentType()
+ {
+ return self::ICALENDAR20;
+ }
+
+ /**
+ * Returns a list of all 'base components'. For instance, if an Event has
+ * a recurrence rule, and one instance is overridden, the overridden event
+ * will have the same UID, but will be excluded from this list.
+ *
+ * VTIMEZONE components will always be excluded.
+ *
+ * @param string $componentName filter by component name
+ *
+ * @return VObject\Component[]
+ */
+ public function getBaseComponents($componentName = null)
+ {
+ $isBaseComponent = function ($component) {
+ if (!$component instanceof VObject\Component) {
+ return false;
+ }
+ if ('VTIMEZONE' === $component->name) {
+ return false;
+ }
+ if (isset($component->{'RECURRENCE-ID'})) {
+ return false;
+ }
+
+ return true;
+ };
+
+ if ($componentName) {
+ // Early exit
+ return array_filter(
+ $this->select($componentName),
+ $isBaseComponent
+ );
+ }
+
+ $components = [];
+ foreach ($this->children as $childGroup) {
+ foreach ($childGroup as $child) {
+ if (!$child instanceof Component) {
+ // If one child is not a component, they all are so we skip
+ // the entire group.
+ continue 2;
+ }
+ if ($isBaseComponent($child)) {
+ $components[] = $child;
+ }
+ }
+ }
+
+ return $components;
+ }
+
+ /**
+ * Returns the first component that is not a VTIMEZONE, and does not have
+ * an RECURRENCE-ID.
+ *
+ * If there is no such component, null will be returned.
+ *
+ * @param string $componentName filter by component name
+ *
+ * @return VObject\Component|null
+ */
+ public function getBaseComponent($componentName = null)
+ {
+ $isBaseComponent = function ($component) {
+ if (!$component instanceof VObject\Component) {
+ return false;
+ }
+ if ('VTIMEZONE' === $component->name) {
+ return false;
+ }
+ if (isset($component->{'RECURRENCE-ID'})) {
+ return false;
+ }
+
+ return true;
+ };
+
+ if ($componentName) {
+ foreach ($this->select($componentName) as $child) {
+ if ($isBaseComponent($child)) {
+ return $child;
+ }
+ }
+
+ return null;
+ }
+
+ // Searching all components
+ foreach ($this->children as $childGroup) {
+ foreach ($childGroup as $child) {
+ if ($isBaseComponent($child)) {
+ return $child;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Expand all events in this VCalendar object and return a new VCalendar
+ * with the expanded events.
+ *
+ * If this calendar object, has events with recurrence rules, this method
+ * can be used to expand the event into multiple sub-events.
+ *
+ * Each event will be stripped from its recurrence information, and only
+ * the instances of the event in the specified timerange will be left
+ * alone.
+ *
+ * In addition, this method will cause timezone information to be stripped,
+ * and normalized to UTC.
+ *
+ * @param DateTimeZone $timeZone reference timezone for floating dates and
+ * times
+ *
+ * @return VCalendar
+ */
+ public function expand(DateTimeInterface $start, DateTimeInterface $end, ?DateTimeZone $timeZone = null)
+ {
+ $newChildren = [];
+ $recurringEvents = [];
+
+ if (!$timeZone) {
+ $timeZone = new DateTimeZone('UTC');
+ }
+
+ $stripTimezones = function (Component $component) use ($timeZone, &$stripTimezones) {
+ foreach ($component->children() as $componentChild) {
+ if ($componentChild instanceof Property\ICalendar\DateTime && $componentChild->hasTime()) {
+ $dt = $componentChild->getDateTimes($timeZone);
+ // We only need to update the first timezone, because
+ // setDateTimes will match all other timezones to the
+ // first.
+ $dt[0] = $dt[0]->setTimeZone(new DateTimeZone('UTC'));
+ $componentChild->setDateTimes($dt);
+ } elseif ($componentChild instanceof Component) {
+ $stripTimezones($componentChild);
+ }
+ }
+
+ return $component;
+ };
+
+ foreach ($this->children() as $child) {
+ if ($child instanceof Property && 'PRODID' !== $child->name) {
+ // We explicitly want to ignore PRODID, because we want to
+ // overwrite it with our own.
+ $newChildren[] = clone $child;
+ } elseif ($child instanceof Component && 'VTIMEZONE' !== $child->name) {
+ // We're also stripping all VTIMEZONE objects because we're
+ // converting everything to UTC.
+ if ('VEVENT' === $child->name && (isset($child->{'RECURRENCE-ID'}) || isset($child->RRULE) || isset($child->RDATE))) {
+ // Handle these a bit later.
+ $uid = (string) $child->UID;
+ if (!$uid) {
+ throw new InvalidDataException('Every VEVENT object must have a UID property');
+ }
+ if (isset($recurringEvents[$uid])) {
+ $recurringEvents[$uid][] = clone $child;
+ } else {
+ $recurringEvents[$uid] = [clone $child];
+ }
+ } elseif ('VEVENT' === $child->name && $child->isInTimeRange($start, $end)) {
+ $newChildren[] = $stripTimezones(clone $child);
+ }
+ }
+ }
+
+ foreach ($recurringEvents as $events) {
+ try {
+ $it = new EventIterator($events, null, $timeZone);
+ } catch (NoInstancesException $e) {
+ // This event is recurring, but it doesn't have a single
+ // instance. We are skipping this event from the output
+ // entirely.
+ continue;
+ }
+ $it->fastForward($start);
+
+ while ($it->valid() && $it->getDTStart() < $end) {
+ if ($it->getDTEnd() > $start) {
+ $newChildren[] = $stripTimezones($it->getEventObject());
+ }
+ $it->next();
+ }
+ }
+
+ return new self($newChildren);
+ }
+
+ /**
+ * This method should return a list of default property values.
+ *
+ * @return array
+ */
+ protected function getDefaults()
+ {
+ return [
+ 'VERSION' => '2.0',
+ 'PRODID' => '-//Sabre//Sabre VObject '.VObject\Version::VERSION.'//EN',
+ 'CALSCALE' => 'GREGORIAN',
+ ];
+ }
+
+ /**
+ * A simple list of validation rules.
+ *
+ * This is simply a list of properties, and how many times they either
+ * must or must not appear.
+ *
+ * Possible values per property:
+ * * 0 - Must not appear.
+ * * 1 - Must appear exactly once.
+ * * + - Must appear at least once.
+ * * * - Can appear any number of times.
+ * * ? - May appear, but not more than once.
+ *
+ * @var array
+ */
+ public function getValidationRules()
+ {
+ return [
+ 'PRODID' => 1,
+ 'VERSION' => 1,
+
+ 'CALSCALE' => '?',
+ 'METHOD' => '?',
+ ];
+ }
+
+ /**
+ * Validates the node for correctness.
+ *
+ * The following options are supported:
+ * Node::REPAIR - May attempt to automatically repair the problem.
+ * Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes.
+ * Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes.
+ *
+ * This method returns an array with detected problems.
+ * Every element has the following properties:
+ *
+ * * level - problem level.
+ * * message - A human-readable string describing the issue.
+ * * node - A reference to the problematic node.
+ *
+ * The level means:
+ * 1 - The issue was repaired (only happens if REPAIR was turned on).
+ * 2 - A warning.
+ * 3 - An error.
+ *
+ * @param int $options
+ *
+ * @return array
+ */
+ public function validate($options = 0)
+ {
+ $warnings = parent::validate($options);
+
+ if ($ver = $this->VERSION) {
+ if ('2.0' !== (string) $ver) {
+ $warnings[] = [
+ 'level' => 3,
+ 'message' => 'Only iCalendar version 2.0 as defined in rfc5545 is supported.',
+ 'node' => $this,
+ ];
+ }
+ }
+
+ $uidList = [];
+ $componentsFound = 0;
+ $componentTypes = [];
+
+ foreach ($this->children() as $child) {
+ if ($child instanceof Component) {
+ ++$componentsFound;
+
+ if (!in_array($child->name, ['VEVENT', 'VTODO', 'VJOURNAL'])) {
+ continue;
+ }
+ $componentTypes[] = $child->name;
+
+ $uid = (string) $child->UID;
+ $isMaster = isset($child->{'RECURRENCE-ID'}) ? 0 : 1;
+ if (isset($uidList[$uid])) {
+ ++$uidList[$uid]['count'];
+ if ($isMaster && $uidList[$uid]['hasMaster']) {
+ $warnings[] = [
+ 'level' => 3,
+ 'message' => 'More than one master object was found for the object with UID '.$uid,
+ 'node' => $this,
+ ];
+ }
+ $uidList[$uid]['hasMaster'] += $isMaster;
+ } else {
+ $uidList[$uid] = [
+ 'count' => 1,
+ 'hasMaster' => $isMaster,
+ ];
+ }
+ }
+ }
+
+ if (0 === $componentsFound) {
+ $warnings[] = [
+ 'level' => 3,
+ 'message' => 'An iCalendar object must have at least 1 component.',
+ 'node' => $this,
+ ];
+ }
+
+ if ($options & self::PROFILE_CALDAV) {
+ if (count($uidList) > 1) {
+ $warnings[] = [
+ 'level' => 3,
+ 'message' => 'A calendar object on a CalDAV server may only have components with the same UID.',
+ 'node' => $this,
+ ];
+ }
+ if (0 === count($componentTypes)) {
+ $warnings[] = [
+ 'level' => 3,
+ 'message' => 'A calendar object on a CalDAV server must have at least 1 component (VTODO, VEVENT, VJOURNAL).',
+ 'node' => $this,
+ ];
+ }
+ if (count(array_unique($componentTypes)) > 1) {
+ $warnings[] = [
+ 'level' => 3,
+ 'message' => 'A calendar object on a CalDAV server may only have 1 type of component (VEVENT, VTODO or VJOURNAL).',
+ 'node' => $this,
+ ];
+ }
+
+ if (isset($this->METHOD)) {
+ $warnings[] = [
+ 'level' => 3,
+ 'message' => 'A calendar object on a CalDAV server MUST NOT have a METHOD property.',
+ 'node' => $this,
+ ];
+ }
+ }
+
+ return $warnings;
+ }
+
+ /**
+ * Returns all components with a specific UID value.
+ *
+ * @return array
+ */
+ public function getByUID($uid)
+ {
+ return array_filter($this->getComponents(), function ($item) use ($uid) {
+ if (!$itemUid = $item->select('UID')) {
+ return false;
+ }
+ $itemUid = current($itemUid)->getValue();
+
+ return $uid === $itemUid;
+ });
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Component/VCard.php b/vendor/sabre/vobject/lib/Component/VCard.php
new file mode 100644
index 0000000..82fab82
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Component/VCard.php
@@ -0,0 +1,541 @@
+ VCard::class,
+ ];
+
+ /**
+ * List of value-types, and which classes they map to.
+ *
+ * @var array
+ */
+ public static $valueMap = [
+ 'BINARY' => VObject\Property\Binary::class,
+ 'BOOLEAN' => VObject\Property\Boolean::class,
+ 'CONTENT-ID' => VObject\Property\FlatText::class, // vCard 2.1 only
+ 'DATE' => VObject\Property\VCard\Date::class,
+ 'DATE-TIME' => VObject\Property\VCard\DateTime::class,
+ 'DATE-AND-OR-TIME' => VObject\Property\VCard\DateAndOrTime::class, // vCard only
+ 'FLOAT' => VObject\Property\FloatValue::class,
+ 'INTEGER' => VObject\Property\IntegerValue::class,
+ 'LANGUAGE-TAG' => VObject\Property\VCard\LanguageTag::class,
+ 'PHONE-NUMBER' => VObject\Property\VCard\PhoneNumber::class, // vCard 3.0 only
+ 'TIMESTAMP' => VObject\Property\VCard\TimeStamp::class,
+ 'TEXT' => VObject\Property\Text::class,
+ 'TIME' => VObject\Property\Time::class,
+ 'UNKNOWN' => VObject\Property\Unknown::class, // jCard / jCal-only.
+ 'URI' => VObject\Property\Uri::class,
+ 'URL' => VObject\Property\Uri::class, // vCard 2.1 only
+ 'UTC-OFFSET' => VObject\Property\UtcOffset::class,
+ ];
+
+ /**
+ * List of properties, and which classes they map to.
+ *
+ * @var array
+ */
+ public static $propertyMap = [
+ // vCard 2.1 properties and up
+ 'N' => VObject\Property\Text::class,
+ 'FN' => VObject\Property\FlatText::class,
+ 'PHOTO' => VObject\Property\Binary::class,
+ 'BDAY' => VObject\Property\VCard\DateAndOrTime::class,
+ 'ADR' => VObject\Property\Text::class,
+ 'LABEL' => VObject\Property\FlatText::class, // Removed in vCard 4.0
+ 'TEL' => VObject\Property\FlatText::class,
+ 'EMAIL' => VObject\Property\FlatText::class,
+ 'MAILER' => VObject\Property\FlatText::class, // Removed in vCard 4.0
+ 'GEO' => VObject\Property\FlatText::class,
+ 'TITLE' => VObject\Property\FlatText::class,
+ 'ROLE' => VObject\Property\FlatText::class,
+ 'LOGO' => VObject\Property\Binary::class,
+ // 'AGENT' => 'Sabre\\VObject\\Property\\', // Todo: is an embedded vCard. Probably rare, so
+ // not supported at the moment
+ 'ORG' => VObject\Property\Text::class,
+ 'NOTE' => VObject\Property\FlatText::class,
+ 'REV' => VObject\Property\VCard\TimeStamp::class,
+ 'SOUND' => VObject\Property\FlatText::class,
+ 'URL' => VObject\Property\Uri::class,
+ 'UID' => VObject\Property\FlatText::class,
+ 'VERSION' => VObject\Property\FlatText::class,
+ 'KEY' => VObject\Property\FlatText::class,
+ 'TZ' => VObject\Property\Text::class,
+
+ // vCard 3.0 properties
+ 'CATEGORIES' => VObject\Property\Text::class,
+ 'SORT-STRING' => VObject\Property\FlatText::class,
+ 'PRODID' => VObject\Property\FlatText::class,
+ 'NICKNAME' => VObject\Property\Text::class,
+ 'CLASS' => VObject\Property\FlatText::class, // Removed in vCard 4.0
+
+ // rfc2739 properties
+ 'FBURL' => VObject\Property\Uri::class,
+ 'CAPURI' => VObject\Property\Uri::class,
+ 'CALURI' => VObject\Property\Uri::class,
+ 'CALADRURI' => VObject\Property\Uri::class,
+
+ // rfc4770 properties
+ 'IMPP' => VObject\Property\Uri::class,
+
+ // vCard 4.0 properties
+ 'SOURCE' => VObject\Property\Uri::class,
+ 'XML' => VObject\Property\FlatText::class,
+ 'ANNIVERSARY' => VObject\Property\VCard\DateAndOrTime::class,
+ 'CLIENTPIDMAP' => VObject\Property\Text::class,
+ 'LANG' => VObject\Property\VCard\LanguageTag::class,
+ 'GENDER' => VObject\Property\Text::class,
+ 'KIND' => VObject\Property\FlatText::class,
+ 'MEMBER' => VObject\Property\Uri::class,
+ 'RELATED' => VObject\Property\Uri::class,
+
+ // rfc6474 properties
+ 'BIRTHPLACE' => VObject\Property\FlatText::class,
+ 'DEATHPLACE' => VObject\Property\FlatText::class,
+ 'DEATHDATE' => VObject\Property\VCard\DateAndOrTime::class,
+
+ // rfc6715 properties
+ 'EXPERTISE' => VObject\Property\FlatText::class,
+ 'HOBBY' => VObject\Property\FlatText::class,
+ 'INTEREST' => VObject\Property\FlatText::class,
+ 'ORG-DIRECTORY' => VObject\Property\FlatText::class,
+ ];
+
+ /**
+ * Returns the current document type.
+ *
+ * @return int
+ */
+ public function getDocumentType()
+ {
+ if (!$this->version) {
+ $version = (string) $this->VERSION;
+
+ switch ($version) {
+ case '2.1':
+ $this->version = self::VCARD21;
+ break;
+ case '3.0':
+ $this->version = self::VCARD30;
+ break;
+ case '4.0':
+ $this->version = self::VCARD40;
+ break;
+ default:
+ // We don't want to cache the version if it's unknown,
+ // because we might get a version property in a bit.
+ return self::UNKNOWN;
+ }
+ }
+
+ return $this->version;
+ }
+
+ /**
+ * Converts the document to a different vcard version.
+ *
+ * Use one of the VCARD constants for the target. This method will return
+ * a copy of the vcard in the new version.
+ *
+ * At the moment the only supported conversion is from 3.0 to 4.0.
+ *
+ * If input and output version are identical, a clone is returned.
+ *
+ * @param int $target
+ *
+ * @return VCard
+ */
+ public function convert($target)
+ {
+ $converter = new VObject\VCardConverter();
+
+ return $converter->convert($this, $target);
+ }
+
+ /**
+ * VCards with version 2.1, 3.0 and 4.0 are found.
+ *
+ * If the VCARD doesn't know its version, 2.1 is assumed.
+ */
+ const DEFAULT_VERSION = self::VCARD21;
+
+ /**
+ * Validates the node for correctness.
+ *
+ * The following options are supported:
+ * Node::REPAIR - May attempt to automatically repair the problem.
+ *
+ * This method returns an array with detected problems.
+ * Every element has the following properties:
+ *
+ * * level - problem level.
+ * * message - A human-readable string describing the issue.
+ * * node - A reference to the problematic node.
+ *
+ * The level means:
+ * 1 - The issue was repaired (only happens if REPAIR was turned on)
+ * 2 - An inconsequential issue
+ * 3 - A severe issue.
+ *
+ * @param int $options
+ *
+ * @return array
+ */
+ public function validate($options = 0)
+ {
+ $warnings = [];
+
+ $versionMap = [
+ self::VCARD21 => '2.1',
+ self::VCARD30 => '3.0',
+ self::VCARD40 => '4.0',
+ ];
+
+ $version = $this->select('VERSION');
+ if (1 === count($version)) {
+ $version = (string) $this->VERSION;
+ if ('2.1' !== $version && '3.0' !== $version && '4.0' !== $version) {
+ $warnings[] = [
+ 'level' => 3,
+ 'message' => 'Only vcard version 4.0 (RFC6350), version 3.0 (RFC2426) or version 2.1 (icm-vcard-2.1) are supported.',
+ 'node' => $this,
+ ];
+ if ($options & self::REPAIR) {
+ $this->VERSION = $versionMap[self::DEFAULT_VERSION];
+ }
+ }
+ if ('2.1' === $version && ($options & self::PROFILE_CARDDAV)) {
+ $warnings[] = [
+ 'level' => 3,
+ 'message' => 'CardDAV servers are not allowed to accept vCard 2.1.',
+ 'node' => $this,
+ ];
+ }
+ }
+ $uid = $this->select('UID');
+ if (0 === count($uid)) {
+ if ($options & self::PROFILE_CARDDAV) {
+ // Required for CardDAV
+ $warningLevel = 3;
+ $message = 'vCards on CardDAV servers MUST have a UID property.';
+ } else {
+ // Not required for regular vcards
+ $warningLevel = 2;
+ $message = 'Adding a UID to a vCard property is recommended.';
+ }
+ if ($options & self::REPAIR) {
+ $this->UID = VObject\UUIDUtil::getUUID();
+ $warningLevel = 1;
+ }
+ $warnings[] = [
+ 'level' => $warningLevel,
+ 'message' => $message,
+ 'node' => $this,
+ ];
+ }
+
+ $fn = $this->select('FN');
+ if (1 !== count($fn)) {
+ $repaired = false;
+ if (($options & self::REPAIR) && 0 === count($fn)) {
+ // We're going to try to see if we can use the contents of the
+ // N property.
+ if (isset($this->N)) {
+ $value = explode(';', (string) $this->N);
+ if (isset($value[1]) && $value[1]) {
+ $this->FN = $value[1].' '.$value[0];
+ } else {
+ $this->FN = $value[0];
+ }
+ $repaired = true;
+
+ // Otherwise, the ORG property may work
+ } elseif (isset($this->ORG)) {
+ $this->FN = (string) $this->ORG;
+ $repaired = true;
+
+ // Otherwise, the NICKNAME property may work
+ } elseif (isset($this->NICKNAME)) {
+ $this->FN = (string) $this->NICKNAME;
+ $repaired = true;
+
+ // Otherwise, the EMAIL property may work
+ } elseif (isset($this->EMAIL)) {
+ $this->FN = (string) $this->EMAIL;
+ $repaired = true;
+ }
+ }
+ $warnings[] = [
+ 'level' => $repaired ? 1 : 3,
+ 'message' => 'The FN property must appear in the VCARD component exactly 1 time',
+ 'node' => $this,
+ ];
+ }
+
+ return array_merge(
+ parent::validate($options),
+ $warnings
+ );
+ }
+
+ /**
+ * A simple list of validation rules.
+ *
+ * This is simply a list of properties, and how many times they either
+ * must or must not appear.
+ *
+ * Possible values per property:
+ * * 0 - Must not appear.
+ * * 1 - Must appear exactly once.
+ * * + - Must appear at least once.
+ * * * - Can appear any number of times.
+ * * ? - May appear, but not more than once.
+ *
+ * @var array
+ */
+ public function getValidationRules()
+ {
+ return [
+ 'ADR' => '*',
+ 'ANNIVERSARY' => '?',
+ 'BDAY' => '?',
+ 'CALADRURI' => '*',
+ 'CALURI' => '*',
+ 'CATEGORIES' => '*',
+ 'CLIENTPIDMAP' => '*',
+ 'EMAIL' => '*',
+ 'FBURL' => '*',
+ 'IMPP' => '*',
+ 'GENDER' => '?',
+ 'GEO' => '*',
+ 'KEY' => '*',
+ 'KIND' => '?',
+ 'LANG' => '*',
+ 'LOGO' => '*',
+ 'MEMBER' => '*',
+ 'N' => '?',
+ 'NICKNAME' => '*',
+ 'NOTE' => '*',
+ 'ORG' => '*',
+ 'PHOTO' => '*',
+ 'PRODID' => '?',
+ 'RELATED' => '*',
+ 'REV' => '?',
+ 'ROLE' => '*',
+ 'SOUND' => '*',
+ 'SOURCE' => '*',
+ 'TEL' => '*',
+ 'TITLE' => '*',
+ 'TZ' => '*',
+ 'URL' => '*',
+ 'VERSION' => '1',
+ 'XML' => '*',
+
+ // FN is commented out, because it's already handled by the
+ // validate function, which may also try to repair it.
+ // 'FN' => '+',
+ 'UID' => '?',
+ ];
+ }
+
+ /**
+ * Returns a preferred field.
+ *
+ * VCards can indicate whether a field such as ADR, TEL or EMAIL is
+ * preferred by specifying TYPE=PREF (vcard 2.1, 3) or PREF=x (vcard 4, x
+ * being a number between 1 and 100).
+ *
+ * If neither of those parameters are specified, the first is returned, if
+ * a field with that name does not exist, null is returned.
+ *
+ * @param string $fieldName
+ *
+ * @return VObject\Property|null
+ */
+ public function preferred($propertyName)
+ {
+ $preferred = null;
+ $lastPref = 101;
+ foreach ($this->select($propertyName) as $field) {
+ $pref = 101;
+ if (isset($field['TYPE']) && $field['TYPE']->has('PREF')) {
+ $pref = 1;
+ } elseif (isset($field['PREF'])) {
+ $pref = $field['PREF']->getValue();
+ }
+
+ if ($pref < $lastPref || is_null($preferred)) {
+ $preferred = $field;
+ $lastPref = $pref;
+ }
+ }
+
+ return $preferred;
+ }
+
+ /**
+ * Returns a property with a specific TYPE value (ADR, TEL, or EMAIL).
+ *
+ * This function will return null if the property does not exist. If there are
+ * multiple properties with the same TYPE value, only one will be returned.
+ *
+ * @param string $propertyName
+ * @param string $type
+ *
+ * @return VObject\Property|null
+ */
+ public function getByType($propertyName, $type)
+ {
+ foreach ($this->select($propertyName) as $field) {
+ if (isset($field['TYPE']) && $field['TYPE']->has($type)) {
+ return $field;
+ }
+ }
+ }
+
+ /**
+ * This method should return a list of default property values.
+ *
+ * @return array
+ */
+ protected function getDefaults()
+ {
+ return [
+ 'VERSION' => '4.0',
+ 'PRODID' => '-//Sabre//Sabre VObject '.VObject\Version::VERSION.'//EN',
+ 'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(),
+ ];
+ }
+
+ /**
+ * This method returns an array, with the representation as it should be
+ * encoded in json. This is used to create jCard or jCal documents.
+ *
+ * @return array
+ */
+ #[\ReturnTypeWillChange]
+ public function jsonSerialize()
+ {
+ // A vcard does not have sub-components, so we're overriding this
+ // method to remove that array element.
+ $properties = [];
+
+ foreach ($this->children() as $child) {
+ $properties[] = $child->jsonSerialize();
+ }
+
+ return [
+ strtolower($this->name),
+ $properties,
+ ];
+ }
+
+ /**
+ * This method serializes the data into XML. This is used to create xCard or
+ * xCal documents.
+ *
+ * @param Xml\Writer $writer XML writer
+ */
+ public function xmlSerialize(Xml\Writer $writer): void
+ {
+ $propertiesByGroup = [];
+
+ foreach ($this->children() as $property) {
+ $group = $property->group;
+
+ if (!isset($propertiesByGroup[$group])) {
+ $propertiesByGroup[$group] = [];
+ }
+
+ $propertiesByGroup[$group][] = $property;
+ }
+
+ $writer->startElement(strtolower($this->name));
+
+ foreach ($propertiesByGroup as $group => $properties) {
+ if (!empty($group)) {
+ $writer->startElement('group');
+ $writer->writeAttribute('name', strtolower($group));
+ }
+
+ foreach ($properties as $property) {
+ switch ($property->name) {
+ case 'VERSION':
+ break;
+
+ case 'XML':
+ $value = $property->getParts();
+ $fragment = new Xml\Element\XmlFragment($value[0]);
+ $writer->write($fragment);
+ break;
+
+ default:
+ $property->xmlSerialize($writer);
+ break;
+ }
+ }
+
+ if (!empty($group)) {
+ $writer->endElement();
+ }
+ }
+
+ $writer->endElement();
+ }
+
+ /**
+ * Returns the default class for a property name.
+ *
+ * @param string $propertyName
+ *
+ * @return string
+ */
+ public function getClassNameForPropertyName($propertyName)
+ {
+ $className = parent::getClassNameForPropertyName($propertyName);
+
+ // In vCard 4, BINARY no longer exists, and we need URI instead.
+ if (VObject\Property\Binary::class == $className && self::VCARD40 === $this->getDocumentType()) {
+ return VObject\Property\Uri::class;
+ }
+
+ return $className;
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Component/VEvent.php b/vendor/sabre/vobject/lib/Component/VEvent.php
new file mode 100644
index 0000000..a385a09
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Component/VEvent.php
@@ -0,0 +1,140 @@
+RRULE || $this->RDATE) {
+ try {
+ $it = new EventIterator($this, null, $start->getTimezone());
+ } catch (NoInstancesException $e) {
+ // If we've caught this exception, there are no instances
+ // for the event that fall into the specified time-range.
+ return false;
+ }
+
+ $it->fastForward($start);
+
+ // We fast-forwarded to a spot where the end-time of the
+ // recurrence instance exceeded the start of the requested
+ // time-range.
+ //
+ // If the starttime of the recurrence did not exceed the
+ // end of the time range as well, we have a match.
+ return $it->getDTStart() < $end && $it->getDTEnd() > $start;
+ }
+
+ $effectiveStart = $this->DTSTART->getDateTime($start->getTimezone());
+ if (isset($this->DTEND)) {
+ // The DTEND property is considered non inclusive. So for a 3 day
+ // event in july, dtstart and dtend would have to be July 1st and
+ // July 4th respectively.
+ //
+ // See:
+ // http://tools.ietf.org/html/rfc5545#page-54
+ $effectiveEnd = $this->DTEND->getDateTime($end->getTimezone());
+ } elseif (isset($this->DURATION)) {
+ $effectiveEnd = $effectiveStart->add(VObject\DateTimeParser::parseDuration($this->DURATION));
+ } elseif (!$this->DTSTART->hasTime()) {
+ $effectiveEnd = $effectiveStart->modify('+1 day');
+ } else {
+ $effectiveEnd = $effectiveStart;
+ }
+
+ return
+ ($start < $effectiveEnd) && ($end > $effectiveStart)
+ ;
+ }
+
+ /**
+ * This method should return a list of default property values.
+ *
+ * @return array
+ */
+ protected function getDefaults()
+ {
+ return [
+ 'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(),
+ 'DTSTAMP' => gmdate('Ymd\\THis\\Z'),
+ ];
+ }
+
+ /**
+ * A simple list of validation rules.
+ *
+ * This is simply a list of properties, and how many times they either
+ * must or must not appear.
+ *
+ * Possible values per property:
+ * * 0 - Must not appear.
+ * * 1 - Must appear exactly once.
+ * * + - Must appear at least once.
+ * * * - Can appear any number of times.
+ * * ? - May appear, but not more than once.
+ *
+ * @var array
+ */
+ public function getValidationRules()
+ {
+ $hasMethod = isset($this->parent->METHOD);
+
+ return [
+ 'UID' => 1,
+ 'DTSTAMP' => 1,
+ 'DTSTART' => $hasMethod ? '?' : '1',
+ 'CLASS' => '?',
+ 'CREATED' => '?',
+ 'DESCRIPTION' => '?',
+ 'GEO' => '?',
+ 'LAST-MODIFIED' => '?',
+ 'LOCATION' => '?',
+ 'ORGANIZER' => '?',
+ 'PRIORITY' => '?',
+ 'SEQUENCE' => '?',
+ 'STATUS' => '?',
+ 'SUMMARY' => '?',
+ 'TRANSP' => '?',
+ 'URL' => '?',
+ 'RECURRENCE-ID' => '?',
+ 'RRULE' => '?',
+ 'DTEND' => '?',
+ 'DURATION' => '?',
+
+ 'ATTACH' => '*',
+ 'ATTENDEE' => '*',
+ 'CATEGORIES' => '*',
+ 'COMMENT' => '*',
+ 'CONTACT' => '*',
+ 'EXDATE' => '*',
+ 'REQUEST-STATUS' => '*',
+ 'RELATED-TO' => '*',
+ 'RESOURCES' => '*',
+ 'RDATE' => '*',
+ ];
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Component/VFreeBusy.php b/vendor/sabre/vobject/lib/Component/VFreeBusy.php
new file mode 100644
index 0000000..fef418b
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Component/VFreeBusy.php
@@ -0,0 +1,93 @@
+select('FREEBUSY') as $freebusy) {
+ // We are only interested in FBTYPE=BUSY (the default),
+ // FBTYPE=BUSY-TENTATIVE or FBTYPE=BUSY-UNAVAILABLE.
+ if (isset($freebusy['FBTYPE']) && 'BUSY' !== strtoupper(substr((string) $freebusy['FBTYPE'], 0, 4))) {
+ continue;
+ }
+
+ // The freebusy component can hold more than 1 value, separated by
+ // commas.
+ $periods = explode(',', (string) $freebusy);
+
+ foreach ($periods as $period) {
+ // Every period is formatted as [start]/[end]. The start is an
+ // absolute UTC time, the end may be an absolute UTC time, or
+ // duration (relative) value.
+ list($busyStart, $busyEnd) = explode('/', $period);
+
+ $busyStart = VObject\DateTimeParser::parse($busyStart);
+ $busyEnd = VObject\DateTimeParser::parse($busyEnd);
+ if ($busyEnd instanceof \DateInterval) {
+ $busyEnd = $busyStart->add($busyEnd);
+ }
+
+ if ($start < $busyEnd && $end > $busyStart) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * A simple list of validation rules.
+ *
+ * This is simply a list of properties, and how many times they either
+ * must or must not appear.
+ *
+ * Possible values per property:
+ * * 0 - Must not appear.
+ * * 1 - Must appear exactly once.
+ * * + - Must appear at least once.
+ * * * - Can appear any number of times.
+ * * ? - May appear, but not more than once.
+ *
+ * @var array
+ */
+ public function getValidationRules()
+ {
+ return [
+ 'UID' => 1,
+ 'DTSTAMP' => 1,
+
+ 'CONTACT' => '?',
+ 'DTSTART' => '?',
+ 'DTEND' => '?',
+ 'ORGANIZER' => '?',
+ 'URL' => '?',
+
+ 'ATTENDEE' => '*',
+ 'COMMENT' => '*',
+ 'FREEBUSY' => '*',
+ 'REQUEST-STATUS' => '*',
+ ];
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Component/VJournal.php b/vendor/sabre/vobject/lib/Component/VJournal.php
new file mode 100644
index 0000000..9b7f1b8
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Component/VJournal.php
@@ -0,0 +1,101 @@
+DTSTART) ? $this->DTSTART->getDateTime() : null;
+ if ($dtstart) {
+ $effectiveEnd = $dtstart;
+ if (!$this->DTSTART->hasTime()) {
+ $effectiveEnd = $effectiveEnd->modify('+1 day');
+ }
+
+ return $start <= $effectiveEnd && $end > $dtstart;
+ }
+
+ return false;
+ }
+
+ /**
+ * A simple list of validation rules.
+ *
+ * This is simply a list of properties, and how many times they either
+ * must or must not appear.
+ *
+ * Possible values per property:
+ * * 0 - Must not appear.
+ * * 1 - Must appear exactly once.
+ * * + - Must appear at least once.
+ * * * - Can appear any number of times.
+ * * ? - May appear, but not more than once.
+ *
+ * @var array
+ */
+ public function getValidationRules()
+ {
+ return [
+ 'UID' => 1,
+ 'DTSTAMP' => 1,
+
+ 'CLASS' => '?',
+ 'CREATED' => '?',
+ 'DTSTART' => '?',
+ 'LAST-MODIFIED' => '?',
+ 'ORGANIZER' => '?',
+ 'RECURRENCE-ID' => '?',
+ 'SEQUENCE' => '?',
+ 'STATUS' => '?',
+ 'SUMMARY' => '?',
+ 'URL' => '?',
+
+ 'RRULE' => '?',
+
+ 'ATTACH' => '*',
+ 'ATTENDEE' => '*',
+ 'CATEGORIES' => '*',
+ 'COMMENT' => '*',
+ 'CONTACT' => '*',
+ 'DESCRIPTION' => '*',
+ 'EXDATE' => '*',
+ 'RELATED-TO' => '*',
+ 'RDATE' => '*',
+ ];
+ }
+
+ /**
+ * This method should return a list of default property values.
+ *
+ * @return array
+ */
+ protected function getDefaults()
+ {
+ return [
+ 'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(),
+ 'DTSTAMP' => gmdate('Ymd\\THis\\Z'),
+ ];
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Component/VTimeZone.php b/vendor/sabre/vobject/lib/Component/VTimeZone.php
new file mode 100644
index 0000000..21c0623
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Component/VTimeZone.php
@@ -0,0 +1,63 @@
+TZID, $this->root);
+ }
+
+ /**
+ * A simple list of validation rules.
+ *
+ * This is simply a list of properties, and how many times they either
+ * must or must not appear.
+ *
+ * Possible values per property:
+ * * 0 - Must not appear.
+ * * 1 - Must appear exactly once.
+ * * + - Must appear at least once.
+ * * * - Can appear any number of times.
+ * * ? - May appear, but not more than once.
+ *
+ * @var array
+ */
+ public function getValidationRules()
+ {
+ return [
+ 'TZID' => 1,
+
+ 'LAST-MODIFIED' => '?',
+ 'TZURL' => '?',
+
+ // At least 1 STANDARD or DAYLIGHT must appear.
+ //
+ // The validator is not specific yet to pick this up, so these
+ // rules are too loose.
+ 'STANDARD' => '*',
+ 'DAYLIGHT' => '*',
+ ];
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Component/VTodo.php b/vendor/sabre/vobject/lib/Component/VTodo.php
new file mode 100644
index 0000000..6f022ba
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Component/VTodo.php
@@ -0,0 +1,181 @@
+DTSTART) ? $this->DTSTART->getDateTime() : null;
+ $duration = isset($this->DURATION) ? VObject\DateTimeParser::parseDuration($this->DURATION) : null;
+ $due = isset($this->DUE) ? $this->DUE->getDateTime() : null;
+ $completed = isset($this->COMPLETED) ? $this->COMPLETED->getDateTime() : null;
+ $created = isset($this->CREATED) ? $this->CREATED->getDateTime() : null;
+
+ if ($dtstart) {
+ if ($duration) {
+ $effectiveEnd = $dtstart->add($duration);
+
+ return $start <= $effectiveEnd && $end > $dtstart;
+ } elseif ($due) {
+ return
+ ($start < $due || $start <= $dtstart) &&
+ ($end > $dtstart || $end >= $due);
+ } else {
+ return $start <= $dtstart && $end > $dtstart;
+ }
+ }
+ if ($due) {
+ return $start < $due && $end >= $due;
+ }
+ if ($completed && $created) {
+ return
+ ($start <= $created || $start <= $completed) &&
+ ($end >= $created || $end >= $completed);
+ }
+ if ($completed) {
+ return $start <= $completed && $end >= $completed;
+ }
+ if ($created) {
+ return $end > $created;
+ }
+
+ return true;
+ }
+
+ /**
+ * A simple list of validation rules.
+ *
+ * This is simply a list of properties, and how many times they either
+ * must or must not appear.
+ *
+ * Possible values per property:
+ * * 0 - Must not appear.
+ * * 1 - Must appear exactly once.
+ * * + - Must appear at least once.
+ * * * - Can appear any number of times.
+ * * ? - May appear, but not more than once.
+ *
+ * @var array
+ */
+ public function getValidationRules()
+ {
+ return [
+ 'UID' => 1,
+ 'DTSTAMP' => 1,
+
+ 'CLASS' => '?',
+ 'COMPLETED' => '?',
+ 'CREATED' => '?',
+ 'DESCRIPTION' => '?',
+ 'DTSTART' => '?',
+ 'GEO' => '?',
+ 'LAST-MODIFIED' => '?',
+ 'LOCATION' => '?',
+ 'ORGANIZER' => '?',
+ 'PERCENT' => '?',
+ 'PRIORITY' => '?',
+ 'RECURRENCE-ID' => '?',
+ 'SEQUENCE' => '?',
+ 'STATUS' => '?',
+ 'SUMMARY' => '?',
+ 'URL' => '?',
+
+ 'RRULE' => '?',
+ 'DUE' => '?',
+ 'DURATION' => '?',
+
+ 'ATTACH' => '*',
+ 'ATTENDEE' => '*',
+ 'CATEGORIES' => '*',
+ 'COMMENT' => '*',
+ 'CONTACT' => '*',
+ 'EXDATE' => '*',
+ 'REQUEST-STATUS' => '*',
+ 'RELATED-TO' => '*',
+ 'RESOURCES' => '*',
+ 'RDATE' => '*',
+ ];
+ }
+
+ /**
+ * Validates the node for correctness.
+ *
+ * The following options are supported:
+ * Node::REPAIR - May attempt to automatically repair the problem.
+ *
+ * This method returns an array with detected problems.
+ * Every element has the following properties:
+ *
+ * * level - problem level.
+ * * message - A human-readable string describing the issue.
+ * * node - A reference to the problematic node.
+ *
+ * The level means:
+ * 1 - The issue was repaired (only happens if REPAIR was turned on)
+ * 2 - An inconsequential issue
+ * 3 - A severe issue.
+ *
+ * @param int $options
+ *
+ * @return array
+ */
+ public function validate($options = 0)
+ {
+ $result = parent::validate($options);
+ if (isset($this->DUE) && isset($this->DTSTART)) {
+ $due = $this->DUE;
+ $dtStart = $this->DTSTART;
+
+ if ($due->getValueType() !== $dtStart->getValueType()) {
+ $result[] = [
+ 'level' => 3,
+ 'message' => 'The value type (DATE or DATE-TIME) must be identical for DUE and DTSTART',
+ 'node' => $due,
+ ];
+ } elseif ($due->getDateTime() < $dtStart->getDateTime()) {
+ $result[] = [
+ 'level' => 3,
+ 'message' => 'DUE must occur after DTSTART',
+ 'node' => $due,
+ ];
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * This method should return a list of default property values.
+ *
+ * @return array
+ */
+ protected function getDefaults()
+ {
+ return [
+ 'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(),
+ 'DTSTAMP' => date('Ymd\\THis\\Z'),
+ ];
+ }
+}
diff --git a/vendor/sabre/vobject/lib/DateTimeParser.php b/vendor/sabre/vobject/lib/DateTimeParser.php
new file mode 100644
index 0000000..69072ef
--- /dev/null
+++ b/vendor/sabre/vobject/lib/DateTimeParser.php
@@ -0,0 +1,560 @@
+\+|-)?P((?\d+)W)?((?\d+)D)?(T((?\d+)H)?((?\d+)M)?((?\d+)S)?)?$/', $duration, $matches);
+ if (!$result) {
+ throw new InvalidDataException('The supplied iCalendar duration value is incorrect: '.$duration);
+ }
+
+ if (!$asString) {
+ $invert = false;
+
+ if (isset($matches['plusminus']) && '-' === $matches['plusminus']) {
+ $invert = true;
+ }
+
+ $parts = [
+ 'week',
+ 'day',
+ 'hour',
+ 'minute',
+ 'second',
+ ];
+
+ foreach ($parts as $part) {
+ $matches[$part] = isset($matches[$part]) && $matches[$part] ? (int) $matches[$part] : 0;
+ }
+
+ // We need to re-construct the $duration string, because weeks and
+ // days are not supported by DateInterval in the same string.
+ $duration = 'P';
+ $days = $matches['day'];
+
+ if ($matches['week']) {
+ $days += $matches['week'] * 7;
+ }
+
+ if ($days) {
+ $duration .= $days.'D';
+ }
+
+ if ($matches['minute'] || $matches['second'] || $matches['hour']) {
+ $duration .= 'T';
+
+ if ($matches['hour']) {
+ $duration .= $matches['hour'].'H';
+ }
+
+ if ($matches['minute']) {
+ $duration .= $matches['minute'].'M';
+ }
+
+ if ($matches['second']) {
+ $duration .= $matches['second'].'S';
+ }
+ }
+
+ if ('P' === $duration) {
+ $duration = 'PT0S';
+ }
+
+ $iv = new DateInterval($duration);
+
+ if ($invert) {
+ $iv->invert = true;
+ }
+
+ return $iv;
+ }
+
+ $parts = [
+ 'week',
+ 'day',
+ 'hour',
+ 'minute',
+ 'second',
+ ];
+
+ $newDur = '';
+
+ foreach ($parts as $part) {
+ if (isset($matches[$part]) && $matches[$part]) {
+ $newDur .= ' '.$matches[$part].' '.$part.'s';
+ }
+ }
+
+ $newDur = ('-' === $matches['plusminus'] ? '-' : '+').trim($newDur);
+
+ if ('+' === $newDur) {
+ $newDur = '+0 seconds';
+ }
+
+ return $newDur;
+ }
+
+ /**
+ * Parses either a Date or DateTime, or Duration value.
+ *
+ * @param string $date
+ * @param DateTimeZone|string $referenceTz
+ *
+ * @return DateTimeImmutable|DateInterval
+ */
+ public static function parse($date, $referenceTz = null)
+ {
+ if ('P' === $date[0] || ('-' === $date[0] && 'P' === $date[1])) {
+ return self::parseDuration($date);
+ } elseif (8 === strlen($date)) {
+ return self::parseDate($date, $referenceTz);
+ } else {
+ return self::parseDateTime($date, $referenceTz);
+ }
+ }
+
+ /**
+ * This method parses a vCard date and or time value.
+ *
+ * This can be used for the DATE, DATE-TIME, TIMESTAMP and
+ * DATE-AND-OR-TIME value.
+ *
+ * This method returns an array, not a DateTime value.
+ *
+ * The elements in the array are in the following order:
+ * year, month, date, hour, minute, second, timezone
+ *
+ * Almost any part of the string may be omitted. It's for example legal to
+ * just specify seconds, leave out the year, etc.
+ *
+ * Timezone is either returned as 'Z' or as '+0800'
+ *
+ * For any non-specified values null is returned.
+ *
+ * List of date formats that are supported:
+ * YYYY
+ * YYYY-MM
+ * YYYYMMDD
+ * --MMDD
+ * ---DD
+ *
+ * YYYY-MM-DD
+ * --MM-DD
+ * ---DD
+ *
+ * List of supported time formats:
+ *
+ * HH
+ * HHMM
+ * HHMMSS
+ * -MMSS
+ * --SS
+ *
+ * HH
+ * HH:MM
+ * HH:MM:SS
+ * -MM:SS
+ * --SS
+ *
+ * A full basic-format date-time string looks like :
+ * 20130603T133901
+ *
+ * A full extended-format date-time string looks like :
+ * 2013-06-03T13:39:01
+ *
+ * Times may be postfixed by a timezone offset. This can be either 'Z' for
+ * UTC, or a string like -0500 or +1100.
+ *
+ * @param string $date
+ *
+ * @return array
+ */
+ public static function parseVCardDateTime($date)
+ {
+ $regex = '/^
+ (?: # date part
+ (?:
+ (?: (? [0-9]{4}) (?: -)?| --)
+ (? [0-9]{2})?
+ |---)
+ (? [0-9]{2})?
+ )?
+ (?:T # time part
+ (? [0-9]{2} | -)
+ (? [0-9]{2} | -)?
+ (? [0-9]{2})?
+
+ (?: \.[0-9]{3})? # milliseconds
+ (?P # timezone offset
+
+ Z | (?: \+|-)(?: [0-9]{4})
+
+ )?
+
+ )?
+ $/x';
+
+ if (!preg_match($regex, $date, $matches)) {
+ // Attempting to parse the extended format.
+ $regex = '/^
+ (?: # date part
+ (?: (? [0-9]{4}) - | -- )
+ (? [0-9]{2}) -
+ (? [0-9]{2})
+ )?
+ (?:T # time part
+
+ (?: (? [0-9]{2}) : | -)
+ (?: (? [0-9]{2}) : | -)?
+ (? [0-9]{2})?
+
+ (?: \.[0-9]{3})? # milliseconds
+ (?P # timezone offset
+
+ Z | (?: \+|-)(?: [0-9]{2}:[0-9]{2})
+
+ )?
+
+ )?
+ $/x';
+
+ if (!preg_match($regex, $date, $matches)) {
+ throw new InvalidDataException('Invalid vCard date-time string: '.$date);
+ }
+ }
+ $parts = [
+ 'year',
+ 'month',
+ 'date',
+ 'hour',
+ 'minute',
+ 'second',
+ 'timezone',
+ ];
+
+ $result = [];
+ foreach ($parts as $part) {
+ if (empty($matches[$part])) {
+ $result[$part] = null;
+ } elseif ('-' === $matches[$part] || '--' === $matches[$part]) {
+ $result[$part] = null;
+ } else {
+ $result[$part] = $matches[$part];
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * This method parses a vCard TIME value.
+ *
+ * This method returns an array, not a DateTime value.
+ *
+ * The elements in the array are in the following order:
+ * hour, minute, second, timezone
+ *
+ * Almost any part of the string may be omitted. It's for example legal to
+ * just specify seconds, leave out the hour etc.
+ *
+ * Timezone is either returned as 'Z' or as '+08:00'
+ *
+ * For any non-specified values null is returned.
+ *
+ * List of supported time formats:
+ *
+ * HH
+ * HHMM
+ * HHMMSS
+ * -MMSS
+ * --SS
+ *
+ * HH
+ * HH:MM
+ * HH:MM:SS
+ * -MM:SS
+ * --SS
+ *
+ * A full basic-format time string looks like :
+ * 133901
+ *
+ * A full extended-format time string looks like :
+ * 13:39:01
+ *
+ * Times may be postfixed by a timezone offset. This can be either 'Z' for
+ * UTC, or a string like -0500 or +11:00.
+ *
+ * @param string $date
+ *
+ * @return array
+ */
+ public static function parseVCardTime($date)
+ {
+ $regex = '/^
+ (? [0-9]{2} | -)
+ (? [0-9]{2} | -)?
+ (? [0-9]{2})?
+
+ (?: \.[0-9]{3})? # milliseconds
+ (?P # timezone offset
+
+ Z | (?: \+|-)(?: [0-9]{4})
+
+ )?
+ $/x';
+
+ if (!preg_match($regex, $date, $matches)) {
+ // Attempting to parse the extended format.
+ $regex = '/^
+ (?: (? [0-9]{2}) : | -)
+ (?: (? [0-9]{2}) : | -)?
+ (? [0-9]{2})?
+
+ (?: \.[0-9]{3})? # milliseconds
+ (?P # timezone offset
+
+ Z | (?: \+|-)(?: [0-9]{2}:[0-9]{2})
+
+ )?
+ $/x';
+
+ if (!preg_match($regex, $date, $matches)) {
+ throw new InvalidDataException('Invalid vCard time string: '.$date);
+ }
+ }
+ $parts = [
+ 'hour',
+ 'minute',
+ 'second',
+ 'timezone',
+ ];
+
+ $result = [];
+ foreach ($parts as $part) {
+ if (empty($matches[$part])) {
+ $result[$part] = null;
+ } elseif ('-' === $matches[$part]) {
+ $result[$part] = null;
+ } else {
+ $result[$part] = $matches[$part];
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * This method parses a vCard date and or time value.
+ *
+ * This can be used for the DATE, DATE-TIME and
+ * DATE-AND-OR-TIME value.
+ *
+ * This method returns an array, not a DateTime value.
+ * The elements in the array are in the following order:
+ * year, month, date, hour, minute, second, timezone
+ * Almost any part of the string may be omitted. It's for example legal to
+ * just specify seconds, leave out the year, etc.
+ *
+ * Timezone is either returned as 'Z' or as '+0800'
+ *
+ * For any non-specified values null is returned.
+ *
+ * List of date formats that are supported:
+ * 20150128
+ * 2015-01
+ * --01
+ * --0128
+ * ---28
+ *
+ * List of supported time formats:
+ * 13
+ * 1353
+ * 135301
+ * -53
+ * -5301
+ * --01 (unreachable, see the tests)
+ * --01Z
+ * --01+1234
+ *
+ * List of supported date-time formats:
+ * 20150128T13
+ * --0128T13
+ * ---28T13
+ * ---28T1353
+ * ---28T135301
+ * ---28T13Z
+ * ---28T13+1234
+ *
+ * See the regular expressions for all the possible patterns.
+ *
+ * Times may be postfixed by a timezone offset. This can be either 'Z' for
+ * UTC, or a string like -0500 or +1100.
+ *
+ * @param string $date
+ *
+ * @return array
+ */
+ public static function parseVCardDateAndOrTime($date)
+ {
+ // \d{8}|\d{4}-\d\d|--\d\d(\d\d)?|---\d\d
+ $valueDate = '/^(?J)(?:'.
+ '(?\d{4})(?\d\d)(?\d\d)'.
+ '|(?\d{4})-(?\d\d)'.
+ '|--(?\d\d)(?\d\d)?'.
+ '|---(?\d\d)'.
+ ')$/';
+
+ // (\d\d(\d\d(\d\d)?)?|-\d\d(\d\d)?|--\d\d)(Z|[+\-]\d\d(\d\d)?)?
+ $valueTime = '/^(?J)(?:'.
+ '((?\d\d)((?\d\d)(?\d\d)?)?'.
+ '|-(?\d\d)(?\d\d)?'.
+ '|--(?\d\d))'.
+ '(?(Z|[+\-]\d\d(\d\d)?))?'.
+ ')$/';
+
+ // (\d{8}|--\d{4}|---\d\d)T\d\d(\d\d(\d\d)?)?(Z|[+\-]\d\d(\d\d?)?
+ $valueDateTime = '/^(?:'.
+ '((?\d{4})(?\d\d)(?\d\d)'.
+ '|--(?\d\d)(?\d\d)'.
+ '|---(?\d\d))'.
+ 'T'.
+ '(?\d\d)((?\d\d)(?\d\d)?)?'.
+ '(?(Z|[+\-]\d\d(\d\d?)))?'.
+ ')$/';
+
+ // date-and-or-time is date | date-time | time
+ // in this strict order.
+
+ if (0 === preg_match($valueDate, $date, $matches)
+ && 0 === preg_match($valueDateTime, $date, $matches)
+ && 0 === preg_match($valueTime, $date, $matches)) {
+ throw new InvalidDataException('Invalid vCard date-time string: '.$date);
+ }
+
+ $parts = [
+ 'year' => null,
+ 'month' => null,
+ 'date' => null,
+ 'hour' => null,
+ 'minute' => null,
+ 'second' => null,
+ 'timezone' => null,
+ ];
+
+ // The $valueDateTime expression has a bug with (?J) so we simulate it.
+ $parts['date0'] = &$parts['date'];
+ $parts['date1'] = &$parts['date'];
+ $parts['date2'] = &$parts['date'];
+ $parts['month0'] = &$parts['month'];
+ $parts['month1'] = &$parts['month'];
+ $parts['year0'] = &$parts['year'];
+
+ foreach ($parts as $part => &$value) {
+ if (!empty($matches[$part])) {
+ $value = $matches[$part];
+ }
+ }
+
+ unset($parts['date0']);
+ unset($parts['date1']);
+ unset($parts['date2']);
+ unset($parts['month0']);
+ unset($parts['month1']);
+ unset($parts['year0']);
+
+ return $parts;
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Document.php b/vendor/sabre/vobject/lib/Document.php
new file mode 100644
index 0000000..36f20dd
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Document.php
@@ -0,0 +1,269 @@
+value syntax, in which case
+ * properties will automatically be created, or you can just pass a list of
+ * Component and Property object.
+ *
+ * By default, a set of sensible values will be added to the component. For
+ * an iCalendar object, this may be something like CALSCALE:GREGORIAN. To
+ * ensure that this does not happen, set $defaults to false.
+ *
+ * @param string $name
+ * @param array $children
+ * @param bool $defaults
+ *
+ * @return Component
+ */
+ public function createComponent($name, ?array $children = null, $defaults = true)
+ {
+ $name = strtoupper($name);
+ $class = Component::class;
+
+ if (isset(static::$componentMap[$name])) {
+ $class = static::$componentMap[$name];
+ }
+ if (is_null($children)) {
+ $children = [];
+ }
+
+ return new $class($this, $name, $children, $defaults);
+ }
+
+ /**
+ * Factory method for creating new properties.
+ *
+ * This method automatically searches for the correct property class, based
+ * on its name.
+ *
+ * You can specify the parameters either in key=>value syntax, in which case
+ * parameters will automatically be created, or you can just pass a list of
+ * Parameter objects.
+ *
+ * @param string $name
+ * @param mixed $value
+ * @param array $parameters
+ * @param string $valueType Force a specific valuetype, such as URI or TEXT
+ */
+ public function createProperty($name, $value = null, ?array $parameters = null, $valueType = null, ?int $lineIndex = null, ?string $lineString = null): Property
+ {
+ // If there's a . in the name, it means it's prefixed by a groupname.
+ if (false !== ($i = strpos($name, '.'))) {
+ $group = substr($name, 0, $i);
+ $name = strtoupper(substr($name, $i + 1));
+ } else {
+ $name = strtoupper($name);
+ $group = null;
+ }
+
+ $class = null;
+
+ // If a VALUE parameter is supplied, we have to use that
+ // According to https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.20
+ // If the property's value is the default value type, then this
+ // parameter need not be specified. However, if the property's
+ // default value type is overridden by some other allowable value
+ // type, then this parameter MUST be specified.
+ if (!$valueType) {
+ $valueType = $parameters['VALUE'] ?? null;
+ }
+
+ if ($valueType) {
+ // The valueType argument comes first to figure out the correct
+ // class.
+ $class = $this->getClassNameForPropertyValue($valueType);
+ }
+
+ // If the value parameter is not set or set to something we do not recognize
+ // we do not attempt to interpret or parse the datass value as specified in
+ // https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.20
+ // So when we so far did not get a class-name, we use the default for the property
+ if (is_null($class)) {
+ $class = $this->getClassNameForPropertyName($name);
+ }
+
+ if (is_null($parameters)) {
+ $parameters = [];
+ }
+
+ return new $class($this, $name, $value, $parameters, $group, $lineIndex, $lineString);
+ }
+
+ /**
+ * This method returns a full class-name for a value parameter.
+ *
+ * For instance, DTSTART may have VALUE=DATE. In that case we will look in
+ * our valueMap table and return the appropriate class name.
+ *
+ * This method returns null if we don't have a specialized class.
+ *
+ * @param string $valueParam
+ *
+ * @return string|null
+ */
+ public function getClassNameForPropertyValue($valueParam)
+ {
+ $valueParam = strtoupper($valueParam);
+ if (isset(static::$valueMap[$valueParam])) {
+ return static::$valueMap[$valueParam];
+ }
+ }
+
+ /**
+ * Returns the default class for a property name.
+ *
+ * @param string $propertyName
+ *
+ * @return string
+ */
+ public function getClassNameForPropertyName($propertyName)
+ {
+ if (isset(static::$propertyMap[$propertyName])) {
+ return static::$propertyMap[$propertyName];
+ } else {
+ return Property\Unknown::class;
+ }
+ }
+}
diff --git a/vendor/sabre/vobject/lib/ElementList.php b/vendor/sabre/vobject/lib/ElementList.php
new file mode 100644
index 0000000..e419d48
--- /dev/null
+++ b/vendor/sabre/vobject/lib/ElementList.php
@@ -0,0 +1,52 @@
+vevent where there's multiple VEVENT objects.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ElementList extends ArrayIterator
+{
+ /* {{{ ArrayAccess Interface */
+
+ /**
+ * Sets an item through ArrayAccess.
+ *
+ * @param int $offset
+ * @param mixed $value
+ *
+ * @return void
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetSet($offset, $value)
+ {
+ throw new LogicException('You can not add new objects to an ElementList');
+ }
+
+ /**
+ * Sets an item through ArrayAccess.
+ *
+ * This method just forwards the request to the inner iterator
+ *
+ * @param int $offset
+ *
+ * @return void
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetUnset($offset)
+ {
+ throw new LogicException('You can not remove objects from an ElementList');
+ }
+
+ /* }}} */
+}
diff --git a/vendor/sabre/vobject/lib/EofException.php b/vendor/sabre/vobject/lib/EofException.php
new file mode 100644
index 0000000..837af7e
--- /dev/null
+++ b/vendor/sabre/vobject/lib/EofException.php
@@ -0,0 +1,15 @@
+start = $start;
+ $this->end = $end;
+ $this->data = [];
+
+ $this->data[] = [
+ 'start' => $this->start,
+ 'end' => $this->end,
+ 'type' => 'FREE',
+ ];
+ }
+
+ /**
+ * Adds free or busytime to the data.
+ *
+ * @param int $start
+ * @param int $end
+ * @param string $type FREE, BUSY, BUSY-UNAVAILABLE or BUSY-TENTATIVE
+ */
+ public function add($start, $end, $type)
+ {
+ if ($start > $this->end || $end < $this->start) {
+ // This new data is outside our timerange.
+ return;
+ }
+
+ if ($start < $this->start) {
+ // The item starts before our requested time range
+ $start = $this->start;
+ }
+ if ($end > $this->end) {
+ // The item ends after our requested time range
+ $end = $this->end;
+ }
+
+ // Finding out where we need to insert the new item.
+ $currentIndex = 0;
+ while ($start > $this->data[$currentIndex]['end']) {
+ ++$currentIndex;
+ }
+
+ // The standard insertion point will be one _after_ the first
+ // overlapping item.
+ $insertStartIndex = $currentIndex + 1;
+
+ $newItem = [
+ 'start' => $start,
+ 'end' => $end,
+ 'type' => $type,
+ ];
+
+ $precedingItem = $this->data[$insertStartIndex - 1];
+ if ($this->data[$insertStartIndex - 1]['start'] === $start) {
+ // The old item starts at the exact same point as the new item.
+ --$insertStartIndex;
+ }
+
+ // Now we know where to insert the item, we need to know where it
+ // starts overlapping with items on the tail end. We need to start
+ // looking one item before the insertStartIndex, because it's possible
+ // that the new item 'sits inside' the previous old item.
+ if ($insertStartIndex > 0) {
+ $currentIndex = $insertStartIndex - 1;
+ } else {
+ $currentIndex = 0;
+ }
+
+ while ($end > $this->data[$currentIndex]['end']) {
+ ++$currentIndex;
+ }
+
+ // What we are about to insert into the array
+ $newItems = [
+ $newItem,
+ ];
+
+ // This is the amount of items that are completely overwritten by the
+ // new item.
+ $itemsToDelete = $currentIndex - $insertStartIndex;
+ if ($this->data[$currentIndex]['end'] <= $end) {
+ ++$itemsToDelete;
+ }
+
+ // If itemsToDelete was -1, it means that the newly inserted item is
+ // actually sitting inside an existing one. This means we need to split
+ // the item at the current position in two and insert the new item in
+ // between.
+ if (-1 === $itemsToDelete) {
+ $itemsToDelete = 0;
+ if ($newItem['end'] < $precedingItem['end']) {
+ $newItems[] = [
+ 'start' => $newItem['end'] + 1,
+ 'end' => $precedingItem['end'],
+ 'type' => $precedingItem['type'],
+ ];
+ }
+ }
+
+ array_splice(
+ $this->data,
+ $insertStartIndex,
+ $itemsToDelete,
+ $newItems
+ );
+
+ $doMerge = false;
+ $mergeOffset = $insertStartIndex;
+ $mergeItem = $newItem;
+ $mergeDelete = 1;
+
+ if (isset($this->data[$insertStartIndex - 1])) {
+ // Updating the start time of the previous item.
+ $this->data[$insertStartIndex - 1]['end'] = $start;
+
+ // If the previous and the current are of the same type, we can
+ // merge them into one item.
+ if ($this->data[$insertStartIndex - 1]['type'] === $this->data[$insertStartIndex]['type']) {
+ $doMerge = true;
+ --$mergeOffset;
+ ++$mergeDelete;
+ $mergeItem['start'] = $this->data[$insertStartIndex - 1]['start'];
+ }
+ }
+ if (isset($this->data[$insertStartIndex + 1])) {
+ // Updating the start time of the next item.
+ $this->data[$insertStartIndex + 1]['start'] = $end;
+
+ // If the next and the current are of the same type, we can
+ // merge them into one item.
+ if ($this->data[$insertStartIndex + 1]['type'] === $this->data[$insertStartIndex]['type']) {
+ $doMerge = true;
+ ++$mergeDelete;
+ $mergeItem['end'] = $this->data[$insertStartIndex + 1]['end'];
+ }
+ }
+ if ($doMerge) {
+ array_splice(
+ $this->data,
+ $mergeOffset,
+ $mergeDelete,
+ [$mergeItem]
+ );
+ }
+ }
+
+ public function getData()
+ {
+ return $this->data;
+ }
+}
diff --git a/vendor/sabre/vobject/lib/FreeBusyGenerator.php b/vendor/sabre/vobject/lib/FreeBusyGenerator.php
new file mode 100644
index 0000000..56ae166
--- /dev/null
+++ b/vendor/sabre/vobject/lib/FreeBusyGenerator.php
@@ -0,0 +1,549 @@
+setTimeRange($start, $end);
+
+ if ($objects) {
+ $this->setObjects($objects);
+ }
+ if (is_null($timeZone)) {
+ $timeZone = new DateTimeZone('UTC');
+ }
+ $this->setTimeZone($timeZone);
+ }
+
+ /**
+ * Sets the VCALENDAR object.
+ *
+ * If this is set, it will not be generated for you. You are responsible
+ * for setting things like the METHOD, CALSCALE, VERSION, etc..
+ *
+ * The VFREEBUSY object will be automatically added though.
+ */
+ public function setBaseObject(Document $vcalendar)
+ {
+ $this->baseObject = $vcalendar;
+ }
+
+ /**
+ * Sets a VAVAILABILITY document.
+ */
+ public function setVAvailability(Document $vcalendar)
+ {
+ $this->vavailability = $vcalendar;
+ }
+
+ /**
+ * Sets the input objects.
+ *
+ * You must either specify a vcalendar object as a string, or as the parse
+ * Component.
+ * It's also possible to specify multiple objects as an array.
+ *
+ * @param mixed $objects
+ */
+ public function setObjects($objects)
+ {
+ if (!is_array($objects)) {
+ $objects = [$objects];
+ }
+
+ $this->objects = [];
+ foreach ($objects as $object) {
+ if (is_string($object) || is_resource($object)) {
+ $this->objects[] = Reader::read($object);
+ } elseif ($object instanceof Component) {
+ $this->objects[] = $object;
+ } else {
+ throw new \InvalidArgumentException('You can only pass strings or \\Sabre\\VObject\\Component arguments to setObjects');
+ }
+ }
+ }
+
+ /**
+ * Sets the time range.
+ *
+ * Any freebusy object falling outside of this time range will be ignored.
+ *
+ * @param DateTimeInterface $start
+ * @param DateTimeInterface $end
+ */
+ public function setTimeRange(?DateTimeInterface $start = null, ?DateTimeInterface $end = null)
+ {
+ if (!$start) {
+ $start = new DateTimeImmutable(Settings::$minDate);
+ }
+ if (!$end) {
+ $end = new DateTimeImmutable(Settings::$maxDate);
+ }
+ $this->start = $start;
+ $this->end = $end;
+ }
+
+ /**
+ * Sets the reference timezone for floating times.
+ */
+ public function setTimeZone(DateTimeZone $timeZone)
+ {
+ $this->timeZone = $timeZone;
+ }
+
+ /**
+ * Parses the input data and returns a correct VFREEBUSY object, wrapped in
+ * a VCALENDAR.
+ *
+ * @return Component
+ */
+ public function getResult()
+ {
+ $fbData = new FreeBusyData(
+ $this->start->getTimeStamp(),
+ $this->end->getTimeStamp()
+ );
+ if ($this->vavailability) {
+ $this->calculateAvailability($fbData, $this->vavailability);
+ }
+
+ $this->calculateBusy($fbData, $this->objects);
+
+ return $this->generateFreeBusyCalendar($fbData);
+ }
+
+ /**
+ * This method takes a VAVAILABILITY component and figures out all the
+ * available times.
+ */
+ protected function calculateAvailability(FreeBusyData $fbData, VCalendar $vavailability)
+ {
+ $vavailComps = iterator_to_array($vavailability->VAVAILABILITY);
+ usort(
+ $vavailComps,
+ function ($a, $b) {
+ // We need to order the components by priority. Priority 1
+ // comes first, up until priority 9. Priority 0 comes after
+ // priority 9. No priority implies priority 0.
+ //
+ // Yes, I'm serious.
+ $priorityA = isset($a->PRIORITY) ? (int) $a->PRIORITY->getValue() : 0;
+ $priorityB = isset($b->PRIORITY) ? (int) $b->PRIORITY->getValue() : 0;
+
+ if (0 === $priorityA) {
+ $priorityA = 10;
+ }
+ if (0 === $priorityB) {
+ $priorityB = 10;
+ }
+
+ return $priorityA - $priorityB;
+ }
+ );
+
+ // Now we go over all the VAVAILABILITY components and figure if
+ // there's any we don't need to consider.
+ //
+ // This is can be because of one of two reasons: either the
+ // VAVAILABILITY component falls outside the time we are interested in,
+ // or a different VAVAILABILITY component with a higher priority has
+ // already completely covered the time-range.
+ $old = $vavailComps;
+ $new = [];
+
+ foreach ($old as $vavail) {
+ list($compStart, $compEnd) = $vavail->getEffectiveStartEnd();
+
+ // We don't care about datetimes that are earlier or later than the
+ // start and end of the freebusy report, so this gets normalized
+ // first.
+ if (is_null($compStart) || $compStart < $this->start) {
+ $compStart = $this->start;
+ }
+ if (is_null($compEnd) || $compEnd > $this->end) {
+ $compEnd = $this->end;
+ }
+
+ // If the item fell out of the timerange, we can just skip it.
+ if ($compStart > $this->end || $compEnd < $this->start) {
+ continue;
+ }
+
+ // Going through our existing list of components to see if there's
+ // a higher priority component that already fully covers this one.
+ foreach ($new as $higherVavail) {
+ list($higherStart, $higherEnd) = $higherVavail->getEffectiveStartEnd();
+ if (
+ (is_null($higherStart) || $higherStart < $compStart) &&
+ (is_null($higherEnd) || $higherEnd > $compEnd)
+ ) {
+ // Component is fully covered by a higher priority
+ // component. We can skip this component.
+ continue 2;
+ }
+ }
+
+ // We're keeping it!
+ $new[] = $vavail;
+ }
+
+ // Lastly, we need to traverse the remaining components and fill in the
+ // freebusydata slots.
+ //
+ // We traverse the components in reverse, because we want the higher
+ // priority components to override the lower ones.
+ foreach (array_reverse($new) as $vavail) {
+ $busyType = isset($vavail->BUSYTYPE) ? strtoupper($vavail->BUSYTYPE) : 'BUSY-UNAVAILABLE';
+ list($vavailStart, $vavailEnd) = $vavail->getEffectiveStartEnd();
+
+ // Making the component size no larger than the requested free-busy
+ // report range.
+ if (!$vavailStart || $vavailStart < $this->start) {
+ $vavailStart = $this->start;
+ }
+ if (!$vavailEnd || $vavailEnd > $this->end) {
+ $vavailEnd = $this->end;
+ }
+
+ // Marking the entire time range of the VAVAILABILITY component as
+ // busy.
+ $fbData->add(
+ $vavailStart->getTimeStamp(),
+ $vavailEnd->getTimeStamp(),
+ $busyType
+ );
+
+ // Looping over the AVAILABLE components.
+ if (isset($vavail->AVAILABLE)) {
+ foreach ($vavail->AVAILABLE as $available) {
+ list($availStart, $availEnd) = $available->getEffectiveStartEnd();
+ $fbData->add(
+ $availStart->getTimeStamp(),
+ $availEnd->getTimeStamp(),
+ 'FREE'
+ );
+
+ if ($available->RRULE) {
+ // Our favourite thing: recurrence!!
+
+ $rruleIterator = new Recur\RRuleIterator(
+ $available->RRULE->getValue(),
+ $availStart
+ );
+ $rruleIterator->fastForward($vavailStart);
+
+ $startEndDiff = $availStart->diff($availEnd);
+
+ while ($rruleIterator->valid()) {
+ $recurStart = $rruleIterator->current();
+ $recurEnd = $recurStart->add($startEndDiff);
+
+ if ($recurStart > $vavailEnd) {
+ // We're beyond the legal timerange.
+ break;
+ }
+
+ if ($recurEnd > $vavailEnd) {
+ // Truncating the end if it exceeds the
+ // VAVAILABILITY end.
+ $recurEnd = $vavailEnd;
+ }
+
+ $fbData->add(
+ $recurStart->getTimeStamp(),
+ $recurEnd->getTimeStamp(),
+ 'FREE'
+ );
+
+ $rruleIterator->next();
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * This method takes an array of iCalendar objects and applies its busy
+ * times on fbData.
+ *
+ * @param VCalendar[] $objects
+ */
+ protected function calculateBusy(FreeBusyData $fbData, array $objects)
+ {
+ foreach ($objects as $key => $object) {
+ foreach ($object->getBaseComponents() as $component) {
+ switch ($component->name) {
+ case 'VEVENT':
+ $FBTYPE = 'BUSY';
+ if (isset($component->TRANSP) && ('TRANSPARENT' === strtoupper($component->TRANSP))) {
+ break;
+ }
+ if (isset($component->STATUS)) {
+ $status = strtoupper($component->STATUS);
+ if ('CANCELLED' === $status) {
+ break;
+ }
+ if ('TENTATIVE' === $status) {
+ $FBTYPE = 'BUSY-TENTATIVE';
+ }
+ }
+
+ $times = [];
+
+ if ($component->RRULE) {
+ try {
+ $iterator = new EventIterator($object, (string) $component->UID, $this->timeZone);
+ } catch (NoInstancesException $e) {
+ // This event is recurring, but it doesn't have a single
+ // instance. We are skipping this event from the output
+ // entirely.
+ unset($this->objects[$key]);
+ break;
+ }
+
+ if ($this->start) {
+ $iterator->fastForward($this->start);
+ }
+
+ $maxRecurrences = Settings::$maxRecurrences;
+
+ while ($iterator->valid() && --$maxRecurrences) {
+ $startTime = $iterator->getDTStart();
+ if ($this->end && $startTime > $this->end) {
+ break;
+ }
+ $times[] = [
+ $iterator->getDTStart(),
+ $iterator->getDTEnd(),
+ ];
+
+ $iterator->next();
+ }
+ } else {
+ $startTime = $component->DTSTART->getDateTime($this->timeZone);
+ if ($this->end && $startTime > $this->end) {
+ break;
+ }
+ $endTime = null;
+ if (isset($component->DTEND)) {
+ $endTime = $component->DTEND->getDateTime($this->timeZone);
+ } elseif (isset($component->DURATION)) {
+ $duration = DateTimeParser::parseDuration((string) $component->DURATION);
+ $endTime = clone $startTime;
+ $endTime = $endTime->add($duration);
+ } elseif (!$component->DTSTART->hasTime()) {
+ $endTime = clone $startTime;
+ $endTime = $endTime->modify('+1 day');
+ } else {
+ // The event had no duration (0 seconds)
+ break;
+ }
+
+ $times[] = [$startTime, $endTime];
+ }
+
+ foreach ($times as $time) {
+ if ($this->end && $time[0] > $this->end) {
+ break;
+ }
+ if ($this->start && $time[1] < $this->start) {
+ break;
+ }
+
+ $fbData->add(
+ $time[0]->getTimeStamp(),
+ $time[1]->getTimeStamp(),
+ $FBTYPE
+ );
+ }
+ break;
+
+ case 'VFREEBUSY':
+ foreach ($component->FREEBUSY as $freebusy) {
+ $fbType = isset($freebusy['FBTYPE']) ? strtoupper($freebusy['FBTYPE']) : 'BUSY';
+
+ // Skipping intervals marked as 'free'
+ if ('FREE' === $fbType) {
+ continue;
+ }
+
+ $values = explode(',', $freebusy);
+ foreach ($values as $value) {
+ list($startTime, $endTime) = explode('/', $value);
+ $startTime = DateTimeParser::parseDateTime($startTime);
+
+ if ('P' === substr($endTime, 0, 1) || '-P' === substr($endTime, 0, 2)) {
+ $duration = DateTimeParser::parseDuration($endTime);
+ $endTime = clone $startTime;
+ $endTime = $endTime->add($duration);
+ } else {
+ $endTime = DateTimeParser::parseDateTime($endTime);
+ }
+
+ if ($this->start && $this->start > $endTime) {
+ continue;
+ }
+ if ($this->end && $this->end < $startTime) {
+ continue;
+ }
+ $fbData->add(
+ $startTime->getTimeStamp(),
+ $endTime->getTimeStamp(),
+ $fbType
+ );
+ }
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * This method takes a FreeBusyData object and generates the VCALENDAR
+ * object associated with it.
+ *
+ * @return VCalendar
+ */
+ protected function generateFreeBusyCalendar(FreeBusyData $fbData)
+ {
+ if ($this->baseObject) {
+ $calendar = $this->baseObject;
+ } else {
+ $calendar = new VCalendar();
+ }
+
+ $vfreebusy = $calendar->createComponent('VFREEBUSY');
+ $calendar->add($vfreebusy);
+
+ if ($this->start) {
+ $dtstart = $calendar->createProperty('DTSTART');
+ $dtstart->setDateTime($this->start);
+ $vfreebusy->add($dtstart);
+ }
+ if ($this->end) {
+ $dtend = $calendar->createProperty('DTEND');
+ $dtend->setDateTime($this->end);
+ $vfreebusy->add($dtend);
+ }
+
+ $tz = new \DateTimeZone('UTC');
+ $dtstamp = $calendar->createProperty('DTSTAMP');
+ $dtstamp->setDateTime(new DateTimeImmutable('now', $tz));
+ $vfreebusy->add($dtstamp);
+
+ foreach ($fbData->getData() as $busyTime) {
+ $busyType = strtoupper($busyTime['type']);
+
+ // Ignoring all the FREE parts, because those are already assumed.
+ if ('FREE' === $busyType) {
+ continue;
+ }
+
+ $busyTime[0] = new \DateTimeImmutable('@'.$busyTime['start'], $tz);
+ $busyTime[1] = new \DateTimeImmutable('@'.$busyTime['end'], $tz);
+
+ $prop = $calendar->createProperty(
+ 'FREEBUSY',
+ $busyTime[0]->format('Ymd\\THis\\Z').'/'.$busyTime[1]->format('Ymd\\THis\\Z')
+ );
+
+ // Only setting FBTYPE if it's not BUSY, because BUSY is the
+ // default anyway.
+ if ('BUSY' !== $busyType) {
+ $prop['FBTYPE'] = $busyType;
+ }
+ $vfreebusy->add($prop);
+ }
+
+ return $calendar;
+ }
+}
diff --git a/vendor/sabre/vobject/lib/ITip/Broker.php b/vendor/sabre/vobject/lib/ITip/Broker.php
new file mode 100644
index 0000000..e100c14
--- /dev/null
+++ b/vendor/sabre/vobject/lib/ITip/Broker.php
@@ -0,0 +1,1003 @@
+component) {
+ return false;
+ }
+
+ switch ($itipMessage->method) {
+ case 'REQUEST':
+ return $this->processMessageRequest($itipMessage, $existingObject);
+
+ case 'CANCEL':
+ return $this->processMessageCancel($itipMessage, $existingObject);
+
+ case 'REPLY':
+ return $this->processMessageReply($itipMessage, $existingObject);
+
+ default:
+ // Unsupported iTip message
+ return;
+ }
+
+ return $existingObject;
+ }
+
+ /**
+ * This function parses a VCALENDAR object and figure out if any messages
+ * need to be sent.
+ *
+ * A VCALENDAR object will be created from the perspective of either an
+ * attendee, or an organizer. You must pass a string identifying the
+ * current user, so we can figure out who in the list of attendees or the
+ * organizer we are sending this message on behalf of.
+ *
+ * It's possible to specify the current user as an array, in case the user
+ * has more than one identifying href (such as multiple emails).
+ *
+ * It $oldCalendar is specified, it is assumed that the operation is
+ * updating an existing event, which means that we need to look at the
+ * differences between events, and potentially send old attendees
+ * cancellations, and current attendees updates.
+ *
+ * If $calendar is null, but $oldCalendar is specified, we treat the
+ * operation as if the user has deleted an event. If the user was an
+ * organizer, this means that we need to send cancellation notices to
+ * people. If the user was an attendee, we need to make sure that the
+ * organizer gets the 'declined' message.
+ *
+ * @param VCalendar|string $calendar
+ * @param string|array $userHref
+ * @param VCalendar|string|null $oldCalendar
+ *
+ * @return array
+ */
+ public function parseEvent($calendar, $userHref, $oldCalendar = null)
+ {
+ if ($oldCalendar) {
+ if (is_string($oldCalendar)) {
+ $oldCalendar = Reader::read($oldCalendar);
+ }
+ if (!isset($oldCalendar->VEVENT)) {
+ // We only support events at the moment
+ return [];
+ }
+
+ $oldEventInfo = $this->parseEventInfo($oldCalendar);
+ } else {
+ $oldEventInfo = [
+ 'organizer' => null,
+ 'significantChangeHash' => '',
+ 'attendees' => [],
+ ];
+ }
+
+ $userHref = (array) $userHref;
+
+ if (!is_null($calendar)) {
+ if (is_string($calendar)) {
+ $calendar = Reader::read($calendar);
+ }
+ if (!isset($calendar->VEVENT)) {
+ // We only support events at the moment
+ return [];
+ }
+ $eventInfo = $this->parseEventInfo($calendar);
+ if (!$eventInfo['attendees'] && !$oldEventInfo['attendees']) {
+ // If there were no attendees on either side of the equation,
+ // we don't need to do anything.
+ return [];
+ }
+ if (!$eventInfo['organizer'] && !$oldEventInfo['organizer']) {
+ // There was no organizer before or after the change.
+ return [];
+ }
+
+ $baseCalendar = $calendar;
+
+ // If the new object didn't have an organizer, the organizer
+ // changed the object from a scheduling object to a non-scheduling
+ // object. We just copy the info from the old object.
+ if (!$eventInfo['organizer'] && $oldEventInfo['organizer']) {
+ $eventInfo['organizer'] = $oldEventInfo['organizer'];
+ $eventInfo['organizerName'] = $oldEventInfo['organizerName'];
+ }
+ } else {
+ // The calendar object got deleted, we need to process this as a
+ // cancellation / decline.
+ if (!$oldCalendar) {
+ // No old and no new calendar, there's no thing to do.
+ return [];
+ }
+
+ $eventInfo = $oldEventInfo;
+
+ if (in_array($eventInfo['organizer'], $userHref)) {
+ // This is an organizer deleting the event.
+ $eventInfo['attendees'] = [];
+ // Increasing the sequence, but only if the organizer deleted
+ // the event.
+ ++$eventInfo['sequence'];
+ } else {
+ // This is an attendee deleting the event.
+ foreach ($eventInfo['attendees'] as $key => $attendee) {
+ if (in_array($attendee['href'], $userHref)) {
+ $eventInfo['attendees'][$key]['instances'] = ['master' => ['id' => 'master', 'partstat' => 'DECLINED'],
+ ];
+ }
+ }
+ }
+ $baseCalendar = $oldCalendar;
+ }
+
+ // Check if the user is the organizer
+ if (in_array($eventInfo['organizer'], $userHref)) {
+ return $this->parseEventForOrganizer($baseCalendar, $eventInfo, $oldEventInfo);
+ }
+
+ // Check if the user is an attendee
+ foreach ($eventInfo['attendees'] as $attendee) {
+ if (in_array($attendee['href'], $userHref)) {
+ // If this is a event update, we always generate a reply
+ if ($oldCalendar) {
+ return $this->parseEventForAttendee($baseCalendar, $eventInfo, $oldEventInfo, $attendee['href']);
+ }
+
+ // If this is a new event, we only generate a reply if the participation status is set
+ foreach ($attendee['instances'] as $instance) {
+ if (isset($instance['partstat']) && 'NEEDS-ACTION' !== $instance['partstat']) {
+ // Attendee has responded (ACCEPTED/DECLINED/TENTATIVE) - generate REPLY
+ return $this->parseEventForAttendee($baseCalendar, $eventInfo, $oldEventInfo, $attendee['href']);
+ }
+ }
+
+ // User is attendee but no response to process
+ break;
+ }
+ }
+
+ return [];
+ }
+
+ /**
+ * Processes incoming REQUEST messages.
+ *
+ * This is message from an organizer, and is either a new event
+ * invite, or an update to an existing one.
+ *
+ * @param VCalendar $existingObject
+ *
+ * @return VCalendar|null
+ */
+ protected function processMessageRequest(Message $itipMessage, ?VCalendar $existingObject = null)
+ {
+ if (!$existingObject) {
+ // This is a new invite, and we're just going to copy over
+ // all the components from the invite.
+ $existingObject = new VCalendar();
+ foreach ($itipMessage->message->getComponents() as $component) {
+ $existingObject->add(clone $component);
+ }
+ } else {
+ // We need to update an existing object with all the new
+ // information. We can just remove all existing components
+ // and create new ones.
+ foreach ($existingObject->getComponents() as $component) {
+ $existingObject->remove($component);
+ }
+ foreach ($itipMessage->message->getComponents() as $component) {
+ $existingObject->add(clone $component);
+ }
+ }
+
+ return $existingObject;
+ }
+
+ /**
+ * Processes incoming CANCEL messages.
+ *
+ * This is a message from an organizer, and means that either an
+ * attendee got removed from an event, or an event got cancelled
+ * altogether.
+ *
+ * @param VCalendar $existingObject
+ *
+ * @return VCalendar|null
+ */
+ protected function processMessageCancel(Message $itipMessage, ?VCalendar $existingObject = null)
+ {
+ if (!$existingObject) {
+ // The event didn't exist in the first place, so we're just
+ // ignoring this message.
+ } else {
+ foreach ($existingObject->VEVENT as $vevent) {
+ $vevent->STATUS = 'CANCELLED';
+ $vevent->SEQUENCE = $itipMessage->sequence;
+ }
+ }
+
+ return $existingObject;
+ }
+
+ /**
+ * Processes incoming REPLY messages.
+ *
+ * The message is a reply. This is for example an attendee telling
+ * an organizer he accepted the invite, or declined it.
+ *
+ * @param VCalendar $existingObject
+ *
+ * @return VCalendar|null
+ */
+ protected function processMessageReply(Message $itipMessage, ?VCalendar $existingObject = null)
+ {
+ // A reply can only be processed based on an existing object.
+ // If the object is not available, the reply is ignored.
+ if (!$existingObject) {
+ return;
+ }
+ $instances = [];
+ $requestStatus = '2.0';
+
+ // Finding all the instances the attendee replied to.
+ foreach ($itipMessage->message->VEVENT as $vevent) {
+ // Use the Unix timestamp returned by getTimestamp as a unique identifier for the recurrence.
+ // The Unix timestamp will be the same for an event, even if the reply from the attendee
+ // used a different format/timezone to express the event date-time.
+ $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimestamp() : 'master';
+ $attendee = $vevent->ATTENDEE;
+ $instances[$recurId] = $attendee['PARTSTAT']->getValue();
+ if (isset($vevent->{'REQUEST-STATUS'})) {
+ $requestStatus = $vevent->{'REQUEST-STATUS'}->getValue();
+ list($requestStatus) = explode(';', $requestStatus);
+ }
+ }
+
+ // Now we need to loop through the original organizer event, to find
+ // all the instances where we have a reply for.
+ $masterObject = null;
+ foreach ($existingObject->VEVENT as $vevent) {
+ // Use the Unix timestamp returned by getTimestamp as a unique identifier for the recurrence.
+ $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimestamp() : 'master';
+ if ('master' === $recurId) {
+ $masterObject = $vevent;
+ }
+ if (isset($instances[$recurId])) {
+ $attendeeFound = false;
+ if (isset($vevent->ATTENDEE)) {
+ foreach ($vevent->ATTENDEE as $attendee) {
+ if ($attendee->getValue() === $itipMessage->sender) {
+ $attendeeFound = true;
+ $attendee['PARTSTAT'] = $instances[$recurId];
+ $attendee['SCHEDULE-STATUS'] = $requestStatus;
+ // Un-setting the RSVP status, because we now know
+ // that the attendee already replied.
+ unset($attendee['RSVP']);
+ break;
+ }
+ }
+ }
+ if (!$attendeeFound) {
+ // Adding a new attendee. The iTip documentation calls this
+ // a party crasher.
+ $attendee = $vevent->add('ATTENDEE', $itipMessage->sender, [
+ 'PARTSTAT' => $instances[$recurId],
+ ]);
+ if ($itipMessage->senderName) {
+ $attendee['CN'] = $itipMessage->senderName;
+ }
+ }
+ unset($instances[$recurId]);
+ }
+ }
+
+ if (!$masterObject) {
+ // No master object, we can't add new instances.
+ return;
+ }
+ // If we got replies to instances that did not exist in the
+ // original list, it means that new exceptions must be created.
+ foreach ($instances as $recurId => $partstat) {
+ $recurrenceIterator = new EventIterator($existingObject, $itipMessage->uid);
+ $found = false;
+ $iterations = 1000;
+ do {
+ $newObject = $recurrenceIterator->getEventObject();
+ $recurrenceIterator->next();
+
+ // Compare the Unix timestamp returned by getTimestamp with the previously calculated timestamp.
+ // If they are the same, then this is a matching recurrence, even though its date-time may have
+ // been expressed in a different format/timezone.
+ if (isset($newObject->{'RECURRENCE-ID'}) && $newObject->{'RECURRENCE-ID'}->getDateTime()->getTimestamp() === $recurId) {
+ $found = true;
+ }
+ --$iterations;
+ } while ($recurrenceIterator->valid() && !$found && $iterations);
+
+ // Invalid recurrence id. Skipping this object.
+ if (!$found) {
+ continue;
+ }
+
+ unset(
+ $newObject->RRULE,
+ $newObject->EXDATE,
+ $newObject->RDATE
+ );
+ $attendeeFound = false;
+ if (isset($newObject->ATTENDEE)) {
+ foreach ($newObject->ATTENDEE as $attendee) {
+ if ($attendee->getValue() === $itipMessage->sender) {
+ $attendeeFound = true;
+ $attendee['PARTSTAT'] = $partstat;
+ break;
+ }
+ }
+ }
+ if (!$attendeeFound) {
+ // Adding a new attendee
+ $attendee = $newObject->add('ATTENDEE', $itipMessage->sender, [
+ 'PARTSTAT' => $partstat,
+ ]);
+ if ($itipMessage->senderName) {
+ $attendee['CN'] = $itipMessage->senderName;
+ }
+ }
+ $existingObject->add($newObject);
+ }
+
+ return $existingObject;
+ }
+
+ /**
+ * This method is used in cases where an event got updated, and we
+ * potentially need to send emails to attendees to let them know of updates
+ * in the events.
+ *
+ * We will detect which attendees got added, which got removed and create
+ * specific messages for these situations.
+ *
+ * @return array
+ */
+ protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo, array $oldEventInfo)
+ {
+ // Merging attendee lists.
+ $attendees = [];
+ foreach ($oldEventInfo['attendees'] as $attendee) {
+ $attendees[$attendee['href']] = [
+ 'href' => $attendee['href'],
+ 'oldInstances' => $attendee['instances'],
+ 'newInstances' => [],
+ 'name' => $attendee['name'],
+ 'forceSend' => null,
+ ];
+ }
+ foreach ($eventInfo['attendees'] as $attendee) {
+ if (isset($attendees[$attendee['href']])) {
+ $attendees[$attendee['href']]['name'] = $attendee['name'];
+ $attendees[$attendee['href']]['newInstances'] = $attendee['instances'];
+ $attendees[$attendee['href']]['forceSend'] = $attendee['forceSend'];
+ } else {
+ $attendees[$attendee['href']] = [
+ 'href' => $attendee['href'],
+ 'oldInstances' => [],
+ 'newInstances' => $attendee['instances'],
+ 'name' => $attendee['name'],
+ 'forceSend' => $attendee['forceSend'],
+ ];
+ }
+ }
+
+ $messages = [];
+ foreach ($attendees as $attendee) {
+ // An organizer can also be an attendee. We should not generate any
+ // messages for those.
+ if ($attendee['href'] === $eventInfo['organizer']) {
+ continue;
+ }
+
+ $message = new Message();
+ $message->uid = $eventInfo['uid'];
+ $message->component = 'VEVENT';
+ $message->sequence = $eventInfo['sequence'];
+ $message->sender = $eventInfo['organizer'];
+ $message->senderName = $eventInfo['organizerName'];
+ $message->recipient = $attendee['href'];
+ $message->recipientName = $attendee['name'];
+
+ // Creating the new iCalendar body.
+ $icalMsg = new VCalendar();
+
+ foreach ($calendar->select('VTIMEZONE') as $timezone) {
+ $icalMsg->add(clone $timezone);
+ }
+
+ if (!$attendee['newInstances'] || 'CANCELLED' === $eventInfo['status']) {
+ // If there are no instances the attendee is a part of, it means
+ // the attendee was removed and we need to send them a CANCEL message.
+ // Also If the meeting STATUS property was changed to CANCELLED
+ // we need to send the attendee a CANCEL message.
+ $message->method = 'CANCEL';
+
+ $icalMsg->METHOD = $message->method;
+
+ $event = $icalMsg->add('VEVENT', [
+ 'UID' => $message->uid,
+ 'SEQUENCE' => $message->sequence,
+ 'DTSTAMP' => gmdate('Ymd\\THis\\Z'),
+ ]);
+ if (isset($calendar->VEVENT->SUMMARY)) {
+ $event->add('SUMMARY', $calendar->VEVENT->SUMMARY->getValue());
+ }
+ $event->add(clone $calendar->VEVENT->DTSTART);
+ if (isset($calendar->VEVENT->DTEND)) {
+ $event->add(clone $calendar->VEVENT->DTEND);
+ } elseif (isset($calendar->VEVENT->DURATION)) {
+ $event->add(clone $calendar->VEVENT->DURATION);
+ }
+ $org = $event->add('ORGANIZER', $eventInfo['organizer']);
+ if ($eventInfo['organizerName']) {
+ $org['CN'] = $eventInfo['organizerName'];
+ }
+ $event->add('ATTENDEE', $attendee['href'], [
+ 'CN' => $attendee['name'],
+ ]);
+ $message->significantChange = true;
+ } else {
+ // The attendee gets the updated event body
+ $message->method = 'REQUEST';
+
+ $icalMsg->METHOD = $message->method;
+
+ // We need to find out that this change is significant. If it's
+ // not, systems may opt to not send messages.
+ //
+ // We do this based on the 'significantChangeHash' which is
+ // some value that changes if there's a certain set of
+ // properties changed in the event, or simply if there's a
+ // difference in instances that the attendee is invited to.
+
+ $oldAttendeeInstances = array_keys($attendee['oldInstances']);
+ $newAttendeeInstances = array_keys($attendee['newInstances']);
+
+ $message->significantChange =
+ 'REQUEST' === $attendee['forceSend'] ||
+ count($oldAttendeeInstances) != count($newAttendeeInstances) ||
+ count(array_diff($oldAttendeeInstances, $newAttendeeInstances)) > 0 ||
+ $oldEventInfo['significantChangeHash'] !== $eventInfo['significantChangeHash'];
+
+ foreach ($attendee['newInstances'] as $instanceId => $instanceInfo) {
+ $currentEvent = clone $eventInfo['instances'][$instanceId];
+ if ('master' === $instanceId) {
+ // We need to find a list of events that the attendee
+ // is not a part of to add to the list of exceptions.
+ $exceptions = [];
+ foreach ($eventInfo['instances'] as $instanceId => $vevent) {
+ if (!isset($attendee['newInstances'][$instanceId])) {
+ $exceptions[] = $instanceId;
+ }
+ }
+
+ // If there were exceptions, we need to add it to an
+ // existing EXDATE property, if it exists.
+ if ($exceptions) {
+ if (isset($currentEvent->EXDATE)) {
+ $currentEvent->EXDATE->setParts(array_merge(
+ $currentEvent->EXDATE->getParts(),
+ $exceptions
+ ));
+ } else {
+ $currentEvent->EXDATE = $exceptions;
+ if ($currentEvent->DTSTART['TZID']) {
+ $currentEvent->EXDATE['TZID'] = clone $currentEvent->DTSTART['TZID'];
+ }
+ }
+ }
+
+ // Cleaning up any scheduling information that
+ // shouldn't be sent along.
+ unset($currentEvent->ORGANIZER['SCHEDULE-FORCE-SEND']);
+ unset($currentEvent->ORGANIZER['SCHEDULE-STATUS']);
+
+ foreach ($currentEvent->ATTENDEE as $currentEventAttendee) {
+ unset($currentEventAttendee['SCHEDULE-FORCE-SEND']);
+ unset($currentEventAttendee['SCHEDULE-STATUS']);
+
+ // We're adding PARTSTAT=NEEDS-ACTION to ensure that
+ // iOS shows an "Inbox Item"
+ if (!isset($currentEventAttendee['PARTSTAT'])) {
+ $currentEventAttendee['PARTSTAT'] = 'NEEDS-ACTION';
+ }
+ }
+ }
+
+ $currentEvent->DTSTAMP = gmdate('Ymd\\THis\\Z');
+ $icalMsg->add($currentEvent);
+ }
+ }
+
+ $message->message = $icalMsg;
+ $messages[] = $message;
+ }
+
+ return $messages;
+ }
+
+ /**
+ * Parse an event update for an attendee.
+ *
+ * This function figures out if we need to send a reply to an organizer.
+ *
+ * @param string $attendee
+ *
+ * @return Message[]
+ */
+ protected function parseEventForAttendee(VCalendar $calendar, array $eventInfo, array $oldEventInfo, $attendee)
+ {
+ if ($this->scheduleAgentServerRules && 'CLIENT' === $eventInfo['organizerScheduleAgent']) {
+ return [];
+ }
+
+ // Don't bother generating messages for events that have already been
+ // cancelled.
+ if ('CANCELLED' === $eventInfo['status']) {
+ return [];
+ }
+
+ $oldInstances = !empty($oldEventInfo['attendees'][$attendee]['instances']) ?
+ $oldEventInfo['attendees'][$attendee]['instances'] :
+ [];
+
+ $instances = [];
+ foreach ($oldInstances as $instance) {
+ $instances[$instance['id']] = [
+ 'id' => $instance['id'],
+ 'oldstatus' => $instance['partstat'],
+ 'newstatus' => null,
+ ];
+ }
+ foreach ($eventInfo['attendees'][$attendee]['instances'] as $instance) {
+ if (isset($instances[$instance['id']])) {
+ $instances[$instance['id']]['newstatus'] = $instance['partstat'];
+ } else {
+ $instances[$instance['id']] = [
+ 'id' => $instance['id'],
+ 'oldstatus' => null,
+ 'newstatus' => $instance['partstat'],
+ ];
+ }
+ }
+
+ // We need to also look for differences in EXDATE. If there are new
+ // items in EXDATE, it means that an attendee deleted instances of an
+ // event, which means we need to send DECLINED specifically for those
+ // instances.
+ // We only need to do that though, if the master event is not declined.
+ if (isset($instances['master']) && 'DECLINED' !== $instances['master']['newstatus']) {
+ foreach ($eventInfo['exdate'] as $exDate) {
+ if (!in_array($exDate, $oldEventInfo['exdate'])) {
+ if (isset($instances[$exDate])) {
+ $instances[$exDate]['newstatus'] = 'DECLINED';
+ } else {
+ $instances[$exDate] = [
+ 'id' => $exDate,
+ 'oldstatus' => null,
+ 'newstatus' => 'DECLINED',
+ ];
+ }
+ }
+ }
+ }
+
+ // Gathering a few extra properties for each instance.
+ foreach ($instances as $recurId => $instanceInfo) {
+ if (isset($eventInfo['instances'][$recurId])) {
+ $instances[$recurId]['dtstart'] = clone $eventInfo['instances'][$recurId]->DTSTART;
+ } else {
+ $instances[$recurId]['dtstart'] = $recurId;
+ }
+ }
+
+ $message = new Message();
+ $message->uid = $eventInfo['uid'];
+ $message->method = 'REPLY';
+ $message->component = 'VEVENT';
+ $message->sequence = $eventInfo['sequence'];
+ $message->sender = $attendee;
+ $message->senderName = $eventInfo['attendees'][$attendee]['name'];
+ $message->recipient = $eventInfo['organizer'];
+ $message->recipientName = $eventInfo['organizerName'];
+
+ $icalMsg = new VCalendar();
+ $icalMsg->METHOD = 'REPLY';
+
+ foreach ($calendar->select('VTIMEZONE') as $timezone) {
+ $icalMsg->add(clone $timezone);
+ }
+
+ $hasReply = false;
+
+ foreach ($instances as $instance) {
+ if ($instance['oldstatus'] == $instance['newstatus'] && 'REPLY' !== $eventInfo['organizerForceSend']) {
+ // Skip
+ continue;
+ }
+
+ $event = $icalMsg->add('VEVENT', [
+ 'UID' => $message->uid,
+ 'SEQUENCE' => $message->sequence,
+ ]);
+ $summary = isset($calendar->VEVENT->SUMMARY) ? $calendar->VEVENT->SUMMARY->getValue() : '';
+ // Adding properties from the correct source instance
+ if (isset($eventInfo['instances'][$instance['id']])) {
+ $instanceObj = $eventInfo['instances'][$instance['id']];
+ $event->add(clone $instanceObj->DTSTART);
+ if (isset($instanceObj->DTEND)) {
+ $event->add(clone $instanceObj->DTEND);
+ } elseif (isset($instanceObj->DURATION)) {
+ $event->add(clone $instanceObj->DURATION);
+ }
+ if (isset($instanceObj->SUMMARY)) {
+ $event->add('SUMMARY', $instanceObj->SUMMARY->getValue());
+ } elseif ($summary) {
+ $event->add('SUMMARY', $summary);
+ }
+ } else {
+ // This branch of the code is reached, when a reply is
+ // generated for an instance of a recurring event, through the
+ // fact that the instance has disappeared by showing up in
+ // EXDATE
+ $dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']);
+ // Treat is as a DATE field
+ if (strlen($instance['id']) <= 8) {
+ $event->add('DTSTART', $dt, ['VALUE' => 'DATE']);
+ } else {
+ $event->add('DTSTART', $dt);
+ }
+ if ($summary) {
+ $event->add('SUMMARY', $summary);
+ }
+ }
+ if ('master' !== $instance['id']) {
+ $dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']);
+ // Treat is as a DATE field
+ if (strlen($instance['id']) <= 8) {
+ $event->add('RECURRENCE-ID', $dt, ['VALUE' => 'DATE']);
+ } else {
+ $event->add('RECURRENCE-ID', $dt);
+ }
+ }
+ $organizer = $event->add('ORGANIZER', $message->recipient);
+ if ($message->recipientName) {
+ $organizer['CN'] = $message->recipientName;
+ }
+ $attendee = $event->add('ATTENDEE', $message->sender, [
+ 'PARTSTAT' => $instance['newstatus'],
+ ]);
+ if ($message->senderName) {
+ $attendee['CN'] = $message->senderName;
+ }
+ $hasReply = true;
+ }
+
+ if ($hasReply) {
+ $message->message = $icalMsg;
+
+ return [$message];
+ } else {
+ return [];
+ }
+ }
+
+ /**
+ * Returns attendee information and information about instances of an
+ * event.
+ *
+ * Returns an array with the following keys:
+ *
+ * 1. uid
+ * 2. organizer
+ * 3. organizerName
+ * 4. organizerScheduleAgent
+ * 5. organizerForceSend
+ * 6. instances
+ * 7. attendees
+ * 8. sequence
+ * 9. exdate
+ * 10. timezone - strictly the timezone on which the recurrence rule is
+ * based on.
+ * 11. significantChangeHash
+ * 12. status
+ *
+ * @param VCalendar $calendar
+ *
+ * @return array
+ */
+ protected function parseEventInfo(?VCalendar $calendar = null)
+ {
+ $uid = null;
+ $organizer = null;
+ $organizerName = null;
+ $organizerForceSend = null;
+ $sequence = null;
+ $timezone = null;
+ $status = null;
+ $organizerScheduleAgent = 'SERVER';
+
+ $significantChangeHash = '';
+
+ // Now we need to collect a list of attendees, and which instances they
+ // are a part of.
+ $attendees = [];
+
+ $instances = [];
+ $exdate = [];
+
+ $significantChangeEventProperties = [];
+
+ foreach ($calendar->VEVENT as $vevent) {
+ $eventSignificantChangeHash = '';
+ $rrule = [];
+
+ if (is_null($uid)) {
+ $uid = $vevent->UID->getValue();
+ } else {
+ if ($uid !== $vevent->UID->getValue()) {
+ throw new ITipException('If a calendar contained more than one event, they must have the same UID.');
+ }
+ }
+
+ if (!isset($vevent->DTSTART)) {
+ throw new ITipException('An event MUST have a DTSTART property.');
+ }
+
+ if (isset($vevent->ORGANIZER)) {
+ if (is_null($organizer)) {
+ $organizer = $vevent->ORGANIZER->getNormalizedValue();
+ $organizerName = isset($vevent->ORGANIZER['CN']) ? $vevent->ORGANIZER['CN'] : null;
+ } else {
+ if (strtoupper($organizer) !== strtoupper($vevent->ORGANIZER->getNormalizedValue())) {
+ throw new SameOrganizerForAllComponentsException('Every instance of the event must have the same organizer.');
+ }
+ }
+ $organizerForceSend =
+ isset($vevent->ORGANIZER['SCHEDULE-FORCE-SEND']) ?
+ strtoupper($vevent->ORGANIZER['SCHEDULE-FORCE-SEND']) :
+ null;
+ $organizerScheduleAgent =
+ isset($vevent->ORGANIZER['SCHEDULE-AGENT']) ?
+ strtoupper((string) $vevent->ORGANIZER['SCHEDULE-AGENT']) :
+ 'SERVER';
+ }
+ if (is_null($sequence) && isset($vevent->SEQUENCE)) {
+ $sequence = $vevent->SEQUENCE->getValue();
+ }
+ if (isset($vevent->EXDATE)) {
+ foreach ($vevent->select('EXDATE') as $val) {
+ $exdate = array_merge($exdate, $val->getParts());
+ }
+ sort($exdate);
+ }
+ if (isset($vevent->RRULE)) {
+ foreach ($vevent->select('RRULE') as $rr) {
+ foreach ($rr->getParts() as $key => $val) {
+ // ignore default values (https://github.com/sabre-io/vobject/issues/126)
+ if ('INTERVAL' === $key && 1 == $val) {
+ continue;
+ }
+ if (is_array($val)) {
+ $val = implode(',', $val);
+ }
+ $rrule[] = "$key=$val";
+ }
+ }
+ sort($rrule);
+ }
+ if (isset($vevent->STATUS)) {
+ $status = strtoupper($vevent->STATUS->getValue());
+ }
+
+ $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master';
+ if (is_null($timezone)) {
+ if ('master' === $recurId) {
+ $timezone = $vevent->DTSTART->getDateTime()->getTimeZone();
+ } else {
+ $timezone = $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimeZone();
+ }
+ }
+
+ $instances[$recurId] = $vevent;
+
+ if (isset($vevent->ATTENDEE)) {
+ foreach ($vevent->ATTENDEE as $attendee) {
+ if ($this->scheduleAgentServerRules &&
+ isset($attendee['SCHEDULE-AGENT']) &&
+ 'CLIENT' === strtoupper($attendee['SCHEDULE-AGENT']->getValue())
+ ) {
+ continue;
+ }
+ $partStat =
+ isset($attendee['PARTSTAT']) ?
+ strtoupper($attendee['PARTSTAT']) :
+ 'NEEDS-ACTION';
+
+ $forceSend =
+ isset($attendee['SCHEDULE-FORCE-SEND']) ?
+ strtoupper($attendee['SCHEDULE-FORCE-SEND']) :
+ null;
+
+ if (isset($attendees[$attendee->getNormalizedValue()])) {
+ $attendees[$attendee->getNormalizedValue()]['instances'][$recurId] = [
+ 'id' => $recurId,
+ 'partstat' => $partStat,
+ 'forceSend' => $forceSend,
+ ];
+ } else {
+ $attendees[$attendee->getNormalizedValue()] = [
+ 'href' => $attendee->getNormalizedValue(),
+ 'instances' => [
+ $recurId => [
+ 'id' => $recurId,
+ 'partstat' => $partStat,
+ ],
+ ],
+ 'name' => isset($attendee['CN']) ? (string) $attendee['CN'] : null,
+ 'forceSend' => $forceSend,
+ ];
+ }
+ }
+ }
+
+ foreach ($this->significantChangeProperties as $prop) {
+ if (isset($vevent->$prop)) {
+ $propertyValues = $vevent->select($prop);
+
+ $eventSignificantChangeHash .= $prop.':';
+
+ if ('EXDATE' === $prop) {
+ $eventSignificantChangeHash .= implode(',', $exdate).';';
+ } elseif ('RRULE' === $prop) {
+ $eventSignificantChangeHash .= implode(',', $rrule).';';
+ } else {
+ foreach ($propertyValues as $val) {
+ $eventSignificantChangeHash .= $val->getValue().';';
+ }
+ }
+ }
+ }
+ $significantChangeEventProperties[] = $eventSignificantChangeHash;
+ }
+
+ asort($significantChangeEventProperties);
+
+ foreach ($significantChangeEventProperties as $eventSignificantChangeHash) {
+ $significantChangeHash .= $eventSignificantChangeHash;
+ }
+ $significantChangeHash = md5($significantChangeHash);
+
+ return compact(
+ 'uid',
+ 'organizer',
+ 'organizerName',
+ 'organizerScheduleAgent',
+ 'organizerForceSend',
+ 'instances',
+ 'attendees',
+ 'sequence',
+ 'exdate',
+ 'timezone',
+ 'significantChangeHash',
+ 'status'
+ );
+ }
+}
diff --git a/vendor/sabre/vobject/lib/ITip/ITipException.php b/vendor/sabre/vobject/lib/ITip/ITipException.php
new file mode 100644
index 0000000..9495636
--- /dev/null
+++ b/vendor/sabre/vobject/lib/ITip/ITipException.php
@@ -0,0 +1,16 @@
+scheduleStatus) {
+ return false;
+ } else {
+ list($scheduleStatus) = explode(';', $this->scheduleStatus);
+
+ return $scheduleStatus;
+ }
+ }
+}
diff --git a/vendor/sabre/vobject/lib/ITip/SameOrganizerForAllComponentsException.php b/vendor/sabre/vobject/lib/ITip/SameOrganizerForAllComponentsException.php
new file mode 100644
index 0000000..4c48625
--- /dev/null
+++ b/vendor/sabre/vobject/lib/ITip/SameOrganizerForAllComponentsException.php
@@ -0,0 +1,18 @@
+parent = null;
+ $this->root = null;
+ }
+
+ /* {{{ IteratorAggregator interface */
+
+ /**
+ * Returns the iterator for this object.
+ *
+ * @return ElementList
+ */
+ #[\ReturnTypeWillChange]
+ public function getIterator()
+ {
+ if (!is_null($this->iterator)) {
+ return $this->iterator;
+ }
+
+ return new ElementList([$this]);
+ }
+
+ /**
+ * Sets the overridden iterator.
+ *
+ * Note that this is not actually part of the iterator interface
+ */
+ public function setIterator(ElementList $iterator)
+ {
+ $this->iterator = $iterator;
+ }
+
+ /**
+ * Validates the node for correctness.
+ *
+ * The following options are supported:
+ * Node::REPAIR - May attempt to automatically repair the problem.
+ *
+ * This method returns an array with detected problems.
+ * Every element has the following properties:
+ *
+ * * level - problem level.
+ * * message - A human-readable string describing the issue.
+ * * node - A reference to the problematic node.
+ *
+ * The level means:
+ * 1 - The issue was repaired (only happens if REPAIR was turned on)
+ * 2 - An inconsequential issue
+ * 3 - A severe issue.
+ *
+ * @param int $options
+ *
+ * @return array
+ */
+ public function validate($options = 0)
+ {
+ return [];
+ }
+
+ /* }}} */
+
+ /* {{{ Countable interface */
+
+ /**
+ * Returns the number of elements.
+ *
+ * @return int
+ */
+ #[\ReturnTypeWillChange]
+ public function count()
+ {
+ $it = $this->getIterator();
+
+ return $it->count();
+ }
+
+ /* }}} */
+
+ /* {{{ ArrayAccess Interface */
+
+ /**
+ * Checks if an item exists through ArrayAccess.
+ *
+ * This method just forwards the request to the inner iterator
+ *
+ * @param int $offset
+ *
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetExists($offset)
+ {
+ $iterator = $this->getIterator();
+
+ return $iterator->offsetExists($offset);
+ }
+
+ /**
+ * Gets an item through ArrayAccess.
+ *
+ * This method just forwards the request to the inner iterator
+ *
+ * @param int $offset
+ *
+ * @return mixed
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetGet($offset)
+ {
+ $iterator = $this->getIterator();
+
+ return $iterator->offsetGet($offset);
+ }
+
+ /**
+ * Sets an item through ArrayAccess.
+ *
+ * This method just forwards the request to the inner iterator
+ *
+ * @param int $offset
+ * @param mixed $value
+ *
+ * @return void
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetSet($offset, $value)
+ {
+ $iterator = $this->getIterator();
+ $iterator->offsetSet($offset, $value);
+
+ // @codeCoverageIgnoreStart
+ //
+ // This method always throws an exception, so we ignore the closing
+ // brace
+ }
+
+ // @codeCoverageIgnoreEnd
+
+ /**
+ * Sets an item through ArrayAccess.
+ *
+ * This method just forwards the request to the inner iterator
+ *
+ * @param int $offset
+ *
+ * @return void
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetUnset($offset)
+ {
+ $iterator = $this->getIterator();
+ $iterator->offsetUnset($offset);
+
+ // @codeCoverageIgnoreStart
+ //
+ // This method always throws an exception, so we ignore the closing
+ // brace
+ }
+
+ // @codeCoverageIgnoreEnd
+
+ /* }}} */
+}
diff --git a/vendor/sabre/vobject/lib/PHPUnitAssertions.php b/vendor/sabre/vobject/lib/PHPUnitAssertions.php
new file mode 100644
index 0000000..45c0a21
--- /dev/null
+++ b/vendor/sabre/vobject/lib/PHPUnitAssertions.php
@@ -0,0 +1,75 @@
+fail('Input must be a string, stream or VObject component');
+ }
+ unset($input->PRODID);
+ if ($input instanceof Component\VCalendar && 'GREGORIAN' === (string) $input->CALSCALE) {
+ unset($input->CALSCALE);
+ }
+
+ return $input;
+ };
+
+ $expected = $getObj($expected)->serialize();
+ $actual = $getObj($actual)->serialize();
+
+ // Finding wildcards in expected.
+ preg_match_all('|^([A-Z]+):\\*\\*ANY\\*\\*\r$|m', $expected, $matches, PREG_SET_ORDER);
+
+ foreach ($matches as $match) {
+ $actual = preg_replace(
+ '|^'.preg_quote($match[1], '|').':(.*)\r$|m',
+ $match[1].':**ANY**'."\r",
+ $actual
+ );
+ }
+
+ $this->assertEquals(
+ $expected,
+ $actual,
+ $message
+ );
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Parameter.php b/vendor/sabre/vobject/lib/Parameter.php
new file mode 100644
index 0000000..0f0b586
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Parameter.php
@@ -0,0 +1,368 @@
+root = $root;
+ if (is_null($name)) {
+ $this->noName = true;
+ $this->name = static::guessParameterNameByValue($value);
+ } else {
+ $this->name = strtoupper($name);
+ }
+
+ // If guessParameterNameByValue() returns an empty string
+ // above, we're actually dealing with a parameter that has no value.
+ // In that case we have to move the value to the name.
+ if ('' === $this->name) {
+ $this->noName = false;
+ $this->name = strtoupper($value);
+ } else {
+ $this->setValue($value);
+ }
+ }
+
+ /**
+ * Try to guess property name by value, can be used for vCard 2.1 nameless parameters.
+ *
+ * Figuring out what the name should have been. Note that a ton of
+ * these are rather silly in 2014 and would probably rarely be
+ * used, but we like to be complete.
+ *
+ * @param string $value
+ *
+ * @return string
+ */
+ public static function guessParameterNameByValue($value)
+ {
+ switch (strtoupper($value)) {
+ // Encodings
+ case '7-BIT':
+ case 'QUOTED-PRINTABLE':
+ case 'BASE64':
+ $name = 'ENCODING';
+ break;
+
+ // Common types
+ case 'WORK':
+ case 'HOME':
+ case 'PREF':
+ // Delivery Label Type
+ case 'DOM':
+ case 'INTL':
+ case 'POSTAL':
+ case 'PARCEL':
+ // Telephone types
+ case 'VOICE':
+ case 'FAX':
+ case 'MSG':
+ case 'CELL':
+ case 'PAGER':
+ case 'BBS':
+ case 'MODEM':
+ case 'CAR':
+ case 'ISDN':
+ case 'VIDEO':
+ // EMAIL types (lol)
+ case 'AOL':
+ case 'APPLELINK':
+ case 'ATTMAIL':
+ case 'CIS':
+ case 'EWORLD':
+ case 'INTERNET':
+ case 'IBMMAIL':
+ case 'MCIMAIL':
+ case 'POWERSHARE':
+ case 'PRODIGY':
+ case 'TLX':
+ case 'X400':
+ // Photo / Logo format types
+ case 'GIF':
+ case 'CGM':
+ case 'WMF':
+ case 'BMP':
+ case 'DIB':
+ case 'PICT':
+ case 'TIFF':
+ case 'PDF':
+ case 'PS':
+ case 'JPEG':
+ case 'MPEG':
+ case 'MPEG2':
+ case 'AVI':
+ case 'QTIME':
+ // Sound Digital Audio Type
+ case 'WAVE':
+ case 'PCM':
+ case 'AIFF':
+ // Key types
+ case 'X509':
+ case 'PGP':
+ $name = 'TYPE';
+ break;
+
+ // Value types
+ case 'INLINE':
+ case 'URL':
+ case 'CONTENT-ID':
+ case 'CID':
+ $name = 'VALUE';
+ break;
+
+ default:
+ $name = '';
+ }
+
+ return $name;
+ }
+
+ /**
+ * Updates the current value.
+ *
+ * This may be either a single, or multiple strings in an array.
+ *
+ * @param string|array $value
+ */
+ public function setValue($value)
+ {
+ $this->value = $value;
+ }
+
+ /**
+ * Returns the current value.
+ *
+ * This method will always return a string, or null. If there were multiple
+ * values, it will automatically concatenate them (separated by comma).
+ *
+ * @return string|null
+ */
+ public function getValue()
+ {
+ if (is_array($this->value)) {
+ return implode(',', $this->value);
+ } else {
+ return $this->value;
+ }
+ }
+
+ /**
+ * Sets multiple values for this parameter.
+ */
+ public function setParts(array $value)
+ {
+ $this->value = $value;
+ }
+
+ /**
+ * Returns all values for this parameter.
+ *
+ * If there were no values, an empty array will be returned.
+ *
+ * @return array
+ */
+ public function getParts()
+ {
+ if (is_array($this->value)) {
+ return $this->value;
+ } elseif (is_null($this->value)) {
+ return [];
+ } else {
+ return [$this->value];
+ }
+ }
+
+ /**
+ * Adds a value to this parameter.
+ *
+ * If the argument is specified as an array, all items will be added to the
+ * parameter value list.
+ *
+ * @param string|array $part
+ */
+ public function addValue($part)
+ {
+ if (is_null($this->value)) {
+ $this->value = $part;
+ } else {
+ $this->value = array_merge((array) $this->value, (array) $part);
+ }
+ }
+
+ /**
+ * Checks if this parameter contains the specified value.
+ *
+ * This is a case-insensitive match. It makes sense to call this for for
+ * instance the TYPE parameter, to see if it contains a keyword such as
+ * 'WORK' or 'FAX'.
+ *
+ * @param string $value
+ *
+ * @return bool
+ */
+ public function has($value)
+ {
+ return in_array(
+ strtolower($value),
+ array_map('strtolower', (array) $this->value)
+ );
+ }
+
+ /**
+ * Turns the object back into a serialized blob.
+ *
+ * @return string
+ */
+ public function serialize()
+ {
+ $value = $this->getParts();
+
+ if (0 === count($value)) {
+ return $this->name.'=';
+ }
+
+ if (Document::VCARD21 === $this->root->getDocumentType() && $this->noName) {
+ return implode(';', $value);
+ }
+
+ return $this->name.'='.array_reduce(
+ $value,
+ function ($out, $item) {
+ if (!is_null($out)) {
+ $out .= ',';
+ }
+
+ // If there's no special characters in the string, we'll use the simple
+ // format.
+ //
+ // The list of special characters is defined as:
+ //
+ // Any character except CONTROL, DQUOTE, ";", ":", ","
+ //
+ // by the iCalendar spec:
+ // https://tools.ietf.org/html/rfc5545#section-3.1
+ //
+ // And we add ^ to that because of:
+ // https://tools.ietf.org/html/rfc6868
+ //
+ // But we've found that iCal (7.0, shipped with OSX 10.9)
+ // severely trips on + characters not being quoted, so we
+ // added + as well.
+ if (!preg_match('#(?: [\n":;\^,\+] )#x', $item)) {
+ return $out.$item;
+ } else {
+ // Enclosing in double-quotes, and using RFC6868 for encoding any
+ // special characters
+ $out .= '"'.strtr(
+ $item,
+ [
+ '^' => '^^',
+ "\n" => '^n',
+ '"' => '^\'',
+ ]
+ ).'"';
+
+ return $out;
+ }
+ }
+ );
+ }
+
+ /**
+ * This method returns an array, with the representation as it should be
+ * encoded in JSON. This is used to create jCard or jCal documents.
+ *
+ * @return array
+ */
+ #[\ReturnTypeWillChange]
+ public function jsonSerialize()
+ {
+ return $this->value;
+ }
+
+ /**
+ * This method serializes the data into XML. This is used to create xCard or
+ * xCal documents.
+ *
+ * @param Xml\Writer $writer XML writer
+ */
+ public function xmlSerialize(Xml\Writer $writer): void
+ {
+ foreach (explode(',', $this->value) as $value) {
+ $writer->writeElement('text', $value);
+ }
+ }
+
+ /**
+ * Called when this object is being cast to a string.
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return (string) $this->getValue();
+ }
+
+ /**
+ * Returns the iterator for this object.
+ *
+ * @return ElementList
+ */
+ #[\ReturnTypeWillChange]
+ public function getIterator()
+ {
+ if (!is_null($this->iterator)) {
+ return $this->iterator;
+ }
+
+ return $this->iterator = new ArrayIterator((array) $this->value);
+ }
+}
diff --git a/vendor/sabre/vobject/lib/ParseException.php b/vendor/sabre/vobject/lib/ParseException.php
new file mode 100644
index 0000000..a8f497b
--- /dev/null
+++ b/vendor/sabre/vobject/lib/ParseException.php
@@ -0,0 +1,14 @@
+setInput($input);
+ }
+ if (is_null($this->input)) {
+ throw new EofException('End of input stream, or no input supplied');
+ }
+
+ if (0 !== $options) {
+ $this->options = $options;
+ }
+
+ switch ($this->input[0]) {
+ case 'vcalendar':
+ $this->root = new VCalendar([], false);
+ break;
+ case 'vcard':
+ $this->root = new VCard([], false);
+ break;
+ default:
+ throw new ParseException('The root component must either be a vcalendar, or a vcard');
+ }
+ foreach ($this->input[1] as $prop) {
+ $this->root->add($this->parseProperty($prop));
+ }
+ if (isset($this->input[2])) {
+ foreach ($this->input[2] as $comp) {
+ $this->root->add($this->parseComponent($comp));
+ }
+ }
+
+ // Resetting the input so we can throw an feof exception the next time.
+ $this->input = null;
+
+ return $this->root;
+ }
+
+ /**
+ * Parses a component.
+ *
+ * @return \Sabre\VObject\Component
+ */
+ public function parseComponent(array $jComp)
+ {
+ // We can remove $self from PHP 5.4 onward.
+ $self = $this;
+
+ $properties = array_map(
+ function ($jProp) use ($self) {
+ return $self->parseProperty($jProp);
+ },
+ $jComp[1]
+ );
+
+ if (isset($jComp[2])) {
+ $components = array_map(
+ function ($jComp) use ($self) {
+ return $self->parseComponent($jComp);
+ },
+ $jComp[2]
+ );
+ } else {
+ $components = [];
+ }
+
+ return $this->root->createComponent(
+ $jComp[0],
+ array_merge($properties, $components),
+ $defaults = false
+ );
+ }
+
+ /**
+ * Parses properties.
+ *
+ * @return \Sabre\VObject\Property
+ */
+ public function parseProperty(array $jProp)
+ {
+ list(
+ $propertyName,
+ $parameters,
+ $valueType
+ ) = $jProp;
+
+ $propertyName = strtoupper($propertyName);
+
+ // This is the default class we would be using if we didn't know the
+ // value type. We're using this value later in this function.
+ $defaultPropertyClass = $this->root->getClassNameForPropertyName($propertyName);
+
+ $parameters = (array) $parameters;
+
+ $value = array_slice($jProp, 3);
+
+ $valueType = strtoupper($valueType);
+
+ if (isset($parameters['group'])) {
+ $propertyName = $parameters['group'].'.'.$propertyName;
+ unset($parameters['group']);
+ }
+
+ $prop = $this->root->createProperty($propertyName, null, $parameters, $valueType);
+ $prop->setJsonValue($value);
+
+ // We have to do something awkward here. FlatText as well as Text
+ // represents TEXT values. We have to normalize these here. In the
+ // future we can get rid of FlatText once we're allowed to break BC
+ // again.
+ if (FlatText::class === $defaultPropertyClass) {
+ $defaultPropertyClass = Text::class;
+ }
+
+ // If the value type we received (e.g.: TEXT) was not the default value
+ // type for the given property (e.g.: BDAY), we need to add a VALUE=
+ // parameter.
+ if ($defaultPropertyClass !== get_class($prop)) {
+ $prop['VALUE'] = $valueType;
+ }
+
+ return $prop;
+ }
+
+ /**
+ * Sets the input data.
+ *
+ * @param resource|string|array $input
+ */
+ public function setInput($input)
+ {
+ if (is_resource($input)) {
+ $input = stream_get_contents($input);
+ }
+ if (is_string($input)) {
+ $input = json_decode($input);
+ }
+ $this->input = $input;
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Parser/MimeDir.php b/vendor/sabre/vobject/lib/Parser/MimeDir.php
new file mode 100644
index 0000000..5520530
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Parser/MimeDir.php
@@ -0,0 +1,710 @@
+root = null;
+
+ if (!is_null($input)) {
+ $this->setInput($input);
+ }
+
+ if (!\is_resource($this->input)) {
+ // Null was passed as input, but there was no existing input buffer
+ // There is nothing to parse.
+ throw new ParseException('No input provided to parse');
+ }
+
+ if (0 !== $options) {
+ $this->options = $options;
+ }
+
+ $this->parseDocument();
+
+ return $this->root;
+ }
+
+ /**
+ * By default all input will be assumed to be UTF-8.
+ *
+ * However, both iCalendar and vCard might be encoded using different
+ * character sets. The character set is usually set in the mime-type.
+ *
+ * If this is the case, use setEncoding to specify that a different
+ * encoding will be used. If this is set, the parser will automatically
+ * convert all incoming data to UTF-8.
+ *
+ * @param string $charset
+ */
+ public function setCharset($charset)
+ {
+ if (!in_array($charset, self::$SUPPORTED_CHARSETS)) {
+ throw new \InvalidArgumentException('Unsupported encoding. (Supported encodings: '.implode(', ', self::$SUPPORTED_CHARSETS).')');
+ }
+ $this->charset = $charset;
+ }
+
+ /**
+ * Sets the input buffer. Must be a string or stream.
+ *
+ * @param resource|string $input
+ */
+ public function setInput($input)
+ {
+ // Resetting the parser
+ $this->lineIndex = 0;
+ $this->startLine = 0;
+
+ if (is_string($input)) {
+ // Converting to a stream.
+ $stream = fopen('php://temp', 'r+');
+ fwrite($stream, $input);
+ rewind($stream);
+ $this->input = $stream;
+ } elseif (is_resource($input)) {
+ $this->input = $input;
+ } else {
+ throw new \InvalidArgumentException('This parser can only read from strings or streams.');
+ }
+ }
+
+ /**
+ * Parses an entire document.
+ */
+ protected function parseDocument()
+ {
+ $line = $this->readLine();
+
+ // BOM is ZERO WIDTH NO-BREAK SPACE (U+FEFF).
+ // It's 0xEF 0xBB 0xBF in UTF-8 hex.
+ if (3 <= strlen($line)
+ && 0xef === ord($line[0])
+ && 0xbb === ord($line[1])
+ && 0xbf === ord($line[2])) {
+ $line = substr($line, 3);
+ }
+
+ switch (strtoupper($line)) {
+ case 'BEGIN:VCALENDAR':
+ $class = VCalendar::$componentMap['VCALENDAR'];
+ break;
+ case 'BEGIN:VCARD':
+ $class = VCard::$componentMap['VCARD'];
+ break;
+ default:
+ throw new ParseException('This parser only supports VCARD and VCALENDAR files');
+ }
+
+ $this->root = new $class([], false);
+
+ while (true) {
+ // Reading until we hit END:
+ try {
+ $line = $this->readLine();
+ } catch (EofException $oEx) {
+ $line = 'END:'.$this->root->name;
+ }
+ if ('END:' === strtoupper(substr($line, 0, 4))) {
+ break;
+ }
+ $result = $this->parseLine($line);
+ if ($result) {
+ $this->root->add($result);
+ }
+ }
+
+ $name = strtoupper(substr($line, 4));
+ if ($name !== $this->root->name) {
+ throw new ParseException('Invalid MimeDir file. expected: "END:'.$this->root->name.'" got: "END:'.$name.'"');
+ }
+ }
+
+ /**
+ * Parses a line, and if it hits a component, it will also attempt to parse
+ * the entire component.
+ *
+ * @param string $line Unfolded line
+ *
+ * @return Node
+ */
+ protected function parseLine($line)
+ {
+ // Start of a new component
+ if ('BEGIN:' === strtoupper(substr($line, 0, 6))) {
+ if (substr($line, 6) === $this->root->name) {
+ throw new ParseException('Invalid MimeDir file. Unexpected component: "'.$line.'" in document type '.$this->root->name);
+ }
+ $component = $this->root->createComponent(substr($line, 6), [], false);
+
+ while (true) {
+ // Reading until we hit END:
+ $line = $this->readLine();
+ if ('END:' === strtoupper(substr($line, 0, 4))) {
+ break;
+ }
+ $result = $this->parseLine($line);
+ if ($result) {
+ $component->add($result);
+ }
+ }
+
+ $name = strtoupper(substr($line, 4));
+ if ($name !== $component->name) {
+ throw new ParseException('Invalid MimeDir file. expected: "END:'.$component->name.'" got: "END:'.$name.'"');
+ }
+
+ return $component;
+ } else {
+ // Property reader
+ $property = $this->readProperty($line);
+ if (!$property) {
+ // Ignored line
+ return false;
+ }
+
+ return $property;
+ }
+ }
+
+ /**
+ * We need to look ahead 1 line every time to see if we need to 'unfold'
+ * the next line.
+ *
+ * If that was not the case, we store it here.
+ *
+ * @var string|null
+ */
+ protected $lineBuffer;
+
+ /**
+ * The real current line number.
+ */
+ protected $lineIndex = 0;
+
+ /**
+ * In the case of unfolded lines, this property holds the line number for
+ * the start of the line.
+ *
+ * @var int
+ */
+ protected $startLine = 0;
+
+ /**
+ * Contains a 'raw' representation of the current line.
+ *
+ * @var string
+ */
+ protected $rawLine;
+
+ /**
+ * Reads a single line from the buffer.
+ *
+ * This method strips any newlines and also takes care of unfolding.
+ *
+ * @throws \Sabre\VObject\EofException
+ *
+ * @return string
+ */
+ protected function readLine()
+ {
+ if (!\is_null($this->lineBuffer)) {
+ $rawLine = $this->lineBuffer;
+ $this->lineBuffer = null;
+ } else {
+ do {
+ $eof = \feof($this->input);
+
+ $rawLine = \fgets($this->input);
+
+ if ($eof || (\feof($this->input) && false === $rawLine)) {
+ throw new EofException('End of document reached prematurely');
+ }
+ if (false === $rawLine) {
+ throw new ParseException('Error reading from input stream');
+ }
+ $rawLine = \rtrim($rawLine, "\r\n");
+ } while ('' === $rawLine); // Skipping empty lines
+ ++$this->lineIndex;
+ }
+ $line = $rawLine;
+
+ $this->startLine = $this->lineIndex;
+
+ // Looking ahead for folded lines.
+ while (true) {
+ $nextLine = \rtrim(\fgets($this->input), "\r\n");
+ ++$this->lineIndex;
+ if (!$nextLine) {
+ break;
+ }
+ if ("\t" === $nextLine[0] || ' ' === $nextLine[0]) {
+ $curLine = \substr($nextLine, 1);
+ $line .= $curLine;
+ $rawLine .= "\n ".$curLine;
+ } else {
+ $this->lineBuffer = $nextLine;
+ break;
+ }
+ }
+ $this->rawLine = $rawLine;
+
+ return $line;
+ }
+
+ /**
+ * Reads a property or component from a line.
+ */
+ protected function readProperty($line)
+ {
+ if ($this->options & self::OPTION_FORGIVING) {
+ $propNameToken = 'A-Z0-9\-\._\\/';
+ } else {
+ $propNameToken = 'A-Z0-9\-\.';
+ }
+
+ $paramNameToken = 'A-Z0-9\-';
+ $safeChar = '^";:,';
+ $qSafeChar = '^"';
+
+ $regex = "/
+ ^(?P [$propNameToken]+ ) (?=[;:]) # property name
+ |
+ (?<=:)(?P .+)$ # property value
+ |
+ ;(?P [$paramNameToken]+) (?=[=;:]) # parameter name
+ |
+ (=|,)(?P # parameter value
+ (?: [$safeChar]*) |
+ \"(?: [$qSafeChar]+)\"
+ ) (?=[;:,])
+ /xi";
+
+ //echo $regex, "\n"; exit();
+ preg_match_all($regex, $line, $matches, PREG_SET_ORDER);
+
+ $property = [
+ 'name' => null,
+ 'parameters' => [],
+ 'value' => null,
+ ];
+
+ /*
+ * Keep track on the last token we parsed in order to do
+ * better error checking
+ */
+ $lastToken = null;
+
+ $lastParam = null;
+
+ /*
+ * Looping through all the tokens.
+ *
+ * Note that we are looping through them in reverse order, because if a
+ * sub-pattern matched, the subsequent named patterns will not show up
+ * in the result.
+ */
+ foreach ($matches as $match) {
+ if (isset($match['paramValue'])) {
+ if ($match['paramValue'] && '"' === $match['paramValue'][0]) {
+ $value = substr($match['paramValue'], 1, -1);
+ } else {
+ $value = $match['paramValue'];
+ }
+
+ $value = $this->unescapeParam($value);
+
+ if (is_null($lastParam)) {
+ if ($this->options & self::OPTION_IGNORE_INVALID_LINES) {
+ // When the property can't be matched and the configuration
+ // option is set to ignore invalid lines, we ignore this line
+ // This can happen when servers provide faulty data as iCloud
+ // frequently does with X-APPLE-STRUCTURED-LOCATION
+ $lastToken = self::TOKEN_PARAMVALUE;
+ continue;
+ }
+ throw new ParseException('Invalid Mimedir file. Line starting at '.$this->startLine.' did not follow iCalendar/vCard conventions');
+ }
+
+ if ('=' == $match[0][0] && self::TOKEN_PARAMNAME != $lastToken) {
+ throw new ParseException('Invalid Mimedir file. Line starting at '.$this->startLine.': Missing parameter name for parameter value "'.$match['paramValue'].'"');
+ }
+
+ if (is_null($property['parameters'][$lastParam])) {
+ $property['parameters'][$lastParam] = $value;
+ } elseif (is_array($property['parameters'][$lastParam])) {
+ $property['parameters'][$lastParam][] = $value;
+ } elseif ($property['parameters'][$lastParam] === $value) {
+ // When the current value of the parameter is the same as the
+ // new one, then we can leave the current parameter as it is.
+ } else {
+ $property['parameters'][$lastParam] = [
+ $property['parameters'][$lastParam],
+ $value,
+ ];
+ }
+ $lastToken = self::TOKEN_PARAMVALUE;
+ continue;
+ }
+ if (isset($match['paramName'])) {
+ $lastParam = strtoupper($match['paramName']);
+ if (!isset($property['parameters'][$lastParam])) {
+ $property['parameters'][$lastParam] = null;
+ }
+ $lastToken = self::TOKEN_PARAMNAME;
+ continue;
+ }
+ if (isset($match['propValue'])) {
+ $property['value'] = $match['propValue'];
+ $lastToken = self::TOKEN_PROPVALUE;
+ continue;
+ }
+ if (isset($match['name']) && 0 < strlen($match['name'])) {
+ $property['name'] = strtoupper($match['name']);
+ $lastToken = self::TOKEN_PROPNAME;
+ continue;
+ }
+
+ // @codeCoverageIgnoreStart
+ throw new \LogicException('This code should not be reachable');
+ // @codeCoverageIgnoreEnd
+ }
+
+ if (is_null($property['value'])) {
+ $property['value'] = '';
+ }
+ if (!isset($property['name']) || 0 == strlen($property['name'])) {
+ if ($this->options & self::OPTION_IGNORE_INVALID_LINES) {
+ return false;
+ }
+ throw new ParseException('Invalid Mimedir file. Line starting at '.$this->startLine.' did not follow iCalendar/vCard conventions');
+ }
+
+ // vCard 2.1 states that parameters may appear without a name, and only
+ // a value. We can deduce the value based on its name.
+ //
+ // Our parser will get those as parameters without a value instead, so
+ // we're filtering these parameters out first.
+ $namedParameters = [];
+ $namelessParameters = [];
+
+ foreach ($property['parameters'] as $name => $value) {
+ if (!is_null($value)) {
+ $namedParameters[$name] = $value;
+ } else {
+ $namelessParameters[] = $name;
+ }
+ }
+
+ $propObj = $this->root->createProperty($property['name'], null, $namedParameters, null, $this->startLine, $line);
+
+ foreach ($namelessParameters as $namelessParameter) {
+ $propObj->add(null, $namelessParameter);
+ }
+
+ if (isset($propObj['ENCODING']) && 'QUOTED-PRINTABLE' === strtoupper($propObj['ENCODING'])) {
+ $propObj->setQuotedPrintableValue($this->extractQuotedPrintableValue());
+ } else {
+ $charset = $this->charset;
+ if (Document::VCARD21 === $this->root->getDocumentType() && isset($propObj['CHARSET'])) {
+ // vCard 2.1 allows the character set to be specified per property.
+ $charset = (string) $propObj['CHARSET'];
+ }
+ switch (strtolower($charset)) {
+ case 'utf-8':
+ break;
+ case 'windows-1252':
+ case 'iso-8859-1':
+ $property['value'] = mb_convert_encoding($property['value'], 'UTF-8', $charset);
+ break;
+ default:
+ throw new ParseException('Unsupported CHARSET: '.$propObj['CHARSET']);
+ }
+ $propObj->setRawMimeDirValue($property['value']);
+ }
+
+ return $propObj;
+ }
+
+ /**
+ * Unescapes a property value.
+ *
+ * vCard 2.1 says:
+ * * Semi-colons must be escaped in some property values, specifically
+ * ADR, ORG and N.
+ * * Semi-colons must be escaped in parameter values, because semi-colons
+ * are also use to separate values.
+ * * No mention of escaping backslashes with another backslash.
+ * * newlines are not escaped either, instead QUOTED-PRINTABLE is used to
+ * span values over more than 1 line.
+ *
+ * vCard 3.0 says:
+ * * (rfc2425) Backslashes, newlines (\n or \N) and comma's must be
+ * escaped, all time time.
+ * * Comma's are used for delimiters in multiple values
+ * * (rfc2426) Adds to to this that the semi-colon MUST also be escaped,
+ * as in some properties semi-colon is used for separators.
+ * * Properties using semi-colons: N, ADR, GEO, ORG
+ * * Both ADR and N's individual parts may be broken up further with a
+ * comma.
+ * * Properties using commas: NICKNAME, CATEGORIES
+ *
+ * vCard 4.0 (rfc6350) says:
+ * * Commas must be escaped.
+ * * Semi-colons may be escaped, an unescaped semi-colon _may_ be a
+ * delimiter, depending on the property.
+ * * Backslashes must be escaped
+ * * Newlines must be escaped as either \N or \n.
+ * * Some compound properties may contain multiple parts themselves, so a
+ * comma within a semi-colon delimited property may also be unescaped
+ * to denote multiple parts _within_ the compound property.
+ * * Text-properties using semi-colons: N, ADR, ORG, CLIENTPIDMAP.
+ * * Text-properties using commas: NICKNAME, RELATED, CATEGORIES, PID.
+ *
+ * Even though the spec says that commas must always be escaped, the
+ * example for GEO in Section 6.5.2 seems to violate this.
+ *
+ * iCalendar 2.0 (rfc5545) says:
+ * * Commas or semi-colons may be used as delimiters, depending on the
+ * property.
+ * * Commas, semi-colons, backslashes, newline (\N or \n) are always
+ * escaped, unless they are delimiters.
+ * * Colons shall not be escaped.
+ * * Commas can be considered the 'default delimiter' and is described as
+ * the delimiter in cases where the order of the multiple values is
+ * insignificant.
+ * * Semi-colons are described as the delimiter for 'structured values'.
+ * They are specifically used in Semi-colons are used as a delimiter in
+ * REQUEST-STATUS, RRULE, GEO and EXRULE. EXRULE is deprecated however.
+ *
+ * Now for the parameters
+ *
+ * If delimiter is not set (empty string) this method will just return a string.
+ * If it's a comma or a semi-colon the string will be split on those
+ * characters, and always return an array.
+ *
+ * @param string $input
+ * @param string $delimiter
+ *
+ * @return string|string[]
+ */
+ public static function unescapeValue($input, $delimiter = ';')
+ {
+ $regex = '# (?: (\\\\ (?: \\\\ | N | n | ; | , ) )';
+ if ($delimiter) {
+ $regex .= ' | ('.$delimiter.')';
+ }
+ $regex .= ') #x';
+
+ $matches = preg_split($regex, $input, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
+
+ $resultArray = [];
+ $result = '';
+
+ foreach ($matches as $match) {
+ switch ($match) {
+ case '\\\\':
+ $result .= '\\';
+ break;
+ case '\N':
+ case '\n':
+ $result .= "\n";
+ break;
+ case '\;':
+ $result .= ';';
+ break;
+ case '\,':
+ $result .= ',';
+ break;
+ case $delimiter:
+ $resultArray[] = $result;
+ $result = '';
+ break;
+ default:
+ $result .= $match;
+ break;
+ }
+ }
+
+ $resultArray[] = $result;
+
+ return $delimiter ? $resultArray : $result;
+ }
+
+ /**
+ * Unescapes a parameter value.
+ *
+ * vCard 2.1:
+ * * Does not mention a mechanism for this. In addition, double quotes
+ * are never used to wrap values.
+ * * This means that parameters can simply not contain colons or
+ * semi-colons.
+ *
+ * vCard 3.0 (rfc2425, rfc2426):
+ * * Parameters _may_ be surrounded by double quotes.
+ * * If this is not the case, semi-colon, colon and comma may simply not
+ * occur (the comma used for multiple parameter values though).
+ * * If it is surrounded by double-quotes, it may simply not contain
+ * double-quotes.
+ * * This means that a parameter can in no case encode double-quotes, or
+ * newlines.
+ *
+ * vCard 4.0 (rfc6350)
+ * * Behavior seems to be identical to vCard 3.0
+ *
+ * iCalendar 2.0 (rfc5545)
+ * * Behavior seems to be identical to vCard 3.0
+ *
+ * Parameter escaping mechanism (rfc6868) :
+ * * This rfc describes a new way to escape parameter values.
+ * * New-line is encoded as ^n
+ * * ^ is encoded as ^^.
+ * * " is encoded as ^'
+ *
+ * @param string $input
+ */
+ private function unescapeParam($input)
+ {
+ return
+ preg_replace_callback(
+ '#(\^(\^|n|\'))#',
+ function ($matches) {
+ switch ($matches[2]) {
+ case 'n':
+ return "\n";
+ case '^':
+ return '^';
+ case '\'':
+ return '"';
+
+ // @codeCoverageIgnoreStart
+ }
+ // @codeCoverageIgnoreEnd
+ },
+ $input
+ );
+ }
+
+ /**
+ * Gets the full quoted printable value.
+ *
+ * We need a special method for this, because newlines have both a meaning
+ * in vCards, and in QuotedPrintable.
+ *
+ * This method does not do any decoding.
+ *
+ * @return string
+ */
+ private function extractQuotedPrintableValue()
+ {
+ // We need to parse the raw line again to get the start of the value.
+ //
+ // We are basically looking for the first colon (:), but we need to
+ // skip over the parameters first, as they may contain one.
+ $regex = '/^
+ (?: [^:])+ # Anything but a colon
+ (?: "[^"]")* # A parameter in double quotes
+ : # start of the value we really care about
+ (.*)$
+ /xs';
+
+ preg_match($regex, $this->rawLine, $matches);
+
+ $value = $matches[1];
+ // Removing the first whitespace character from every line. Kind of
+ // like unfolding, but we keep the newline.
+ $value = str_replace("\n ", "\n", $value);
+
+ // Microsoft products don't always correctly fold lines, they may be
+ // missing a whitespace. So if 'forgiving' is turned on, we will take
+ // those as well.
+ if ($this->options & self::OPTION_FORGIVING) {
+ while ('=' === substr($value, -1) && $this->lineBuffer) {
+ // Reading the line
+ $this->readLine();
+ // Grabbing the raw form
+ $value .= "\n".$this->rawLine;
+ }
+ }
+
+ return $value;
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Parser/Parser.php b/vendor/sabre/vobject/lib/Parser/Parser.php
new file mode 100644
index 0000000..b7b6114
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Parser/Parser.php
@@ -0,0 +1,75 @@
+setInput($input);
+ }
+ $this->options = $options;
+ }
+
+ /**
+ * This method starts the parsing process.
+ *
+ * If the input was not supplied during construction, it's possible to pass
+ * it here instead.
+ *
+ * If either input or options are not supplied, the defaults will be used.
+ *
+ * @param mixed $input
+ * @param int $options
+ *
+ * @return array
+ */
+ abstract public function parse($input = null, $options = 0);
+
+ /**
+ * Sets the input data.
+ *
+ * @param mixed $input
+ */
+ abstract public function setInput($input);
+}
diff --git a/vendor/sabre/vobject/lib/Parser/XML.php b/vendor/sabre/vobject/lib/Parser/XML.php
new file mode 100644
index 0000000..7877317
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Parser/XML.php
@@ -0,0 +1,377 @@
+setInput($input);
+ }
+
+ if (0 !== $options) {
+ $this->options = $options;
+ }
+
+ if (is_null($this->input)) {
+ throw new EofException('End of input stream, or no input supplied');
+ }
+
+ switch ($this->input['name']) {
+ case '{'.self::XCAL_NAMESPACE.'}icalendar':
+ $this->root = new VCalendar([], false);
+ $this->pointer = &$this->input['value'][0];
+ $this->parseVCalendarComponents($this->root);
+ break;
+
+ case '{'.self::XCARD_NAMESPACE.'}vcards':
+ foreach ($this->input['value'] as &$vCard) {
+ $this->root = new VCard(['version' => '4.0'], false);
+ $this->pointer = &$vCard;
+ $this->parseVCardComponents($this->root);
+
+ // We just parse the first element.
+ break;
+ }
+ break;
+
+ default:
+ throw new ParseException('Unsupported XML standard');
+ }
+
+ return $this->root;
+ }
+
+ /**
+ * Parse a xCalendar component.
+ */
+ protected function parseVCalendarComponents(Component $parentComponent)
+ {
+ foreach ($this->pointer['value'] ?: [] as $children) {
+ switch (static::getTagName($children['name'])) {
+ case 'properties':
+ $this->pointer = &$children['value'];
+ $this->parseProperties($parentComponent);
+ break;
+
+ case 'components':
+ $this->pointer = &$children;
+ $this->parseComponent($parentComponent);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Parse a xCard component.
+ */
+ protected function parseVCardComponents(Component $parentComponent)
+ {
+ $this->pointer = &$this->pointer['value'];
+ $this->parseProperties($parentComponent);
+ }
+
+ /**
+ * Parse xCalendar and xCard properties.
+ *
+ * @param string $propertyNamePrefix
+ */
+ protected function parseProperties(Component $parentComponent, $propertyNamePrefix = '')
+ {
+ foreach ($this->pointer ?: [] as $xmlProperty) {
+ list($namespace, $tagName) = SabreXml\Service::parseClarkNotation($xmlProperty['name']);
+
+ $propertyName = $tagName;
+ $propertyValue = [];
+ $propertyParameters = [];
+ $propertyType = 'text';
+
+ // A property which is not part of the standard.
+ if (self::XCAL_NAMESPACE !== $namespace
+ && self::XCARD_NAMESPACE !== $namespace) {
+ $propertyName = 'xml';
+ $value = '<'.$tagName.' xmlns="'.$namespace.'"';
+
+ foreach ($xmlProperty['attributes'] as $attributeName => $attributeValue) {
+ $value .= ' '.$attributeName.'="'.str_replace('"', '\"', $attributeValue).'"';
+ }
+
+ $value .= '>'.$xmlProperty['value'].''.$tagName.'>';
+
+ $propertyValue = [$value];
+
+ $this->createProperty(
+ $parentComponent,
+ $propertyName,
+ $propertyParameters,
+ $propertyType,
+ $propertyValue
+ );
+
+ continue;
+ }
+
+ // xCard group.
+ if ('group' === $propertyName) {
+ if (!isset($xmlProperty['attributes']['name'])) {
+ continue;
+ }
+
+ $this->pointer = &$xmlProperty['value'];
+ $this->parseProperties(
+ $parentComponent,
+ strtoupper($xmlProperty['attributes']['name']).'.'
+ );
+
+ continue;
+ }
+
+ // Collect parameters.
+ foreach ($xmlProperty['value'] as $i => $xmlPropertyChild) {
+ if (!is_array($xmlPropertyChild)
+ || 'parameters' !== static::getTagName($xmlPropertyChild['name'])) {
+ continue;
+ }
+
+ $xmlParameters = $xmlPropertyChild['value'];
+
+ foreach ($xmlParameters as $xmlParameter) {
+ $propertyParameterValues = [];
+
+ foreach ($xmlParameter['value'] as $xmlParameterValues) {
+ $propertyParameterValues[] = $xmlParameterValues['value'];
+ }
+
+ $propertyParameters[static::getTagName($xmlParameter['name'])]
+ = implode(',', $propertyParameterValues);
+ }
+
+ array_splice($xmlProperty['value'], $i, 1);
+ }
+
+ $propertyNameExtended = ($this->root instanceof VCalendar
+ ? 'xcal'
+ : 'xcard').':'.$propertyName;
+
+ switch ($propertyNameExtended) {
+ case 'xcal:geo':
+ $propertyType = 'float';
+ $propertyValue['latitude'] = 0;
+ $propertyValue['longitude'] = 0;
+
+ foreach ($xmlProperty['value'] as $xmlRequestChild) {
+ $propertyValue[static::getTagName($xmlRequestChild['name'])]
+ = $xmlRequestChild['value'];
+ }
+ break;
+
+ case 'xcal:request-status':
+ $propertyType = 'text';
+
+ foreach ($xmlProperty['value'] as $xmlRequestChild) {
+ $propertyValue[static::getTagName($xmlRequestChild['name'])]
+ = $xmlRequestChild['value'];
+ }
+ break;
+
+ case 'xcal:freebusy':
+ $propertyType = 'freebusy';
+ // We don't break because we only want to set
+ // another property type.
+
+ // no break
+ case 'xcal:categories':
+ case 'xcal:resources':
+ case 'xcal:exdate':
+ foreach ($xmlProperty['value'] as $specialChild) {
+ $propertyValue[static::getTagName($specialChild['name'])]
+ = $specialChild['value'];
+ }
+ break;
+
+ case 'xcal:rdate':
+ $propertyType = 'date-time';
+
+ foreach ($xmlProperty['value'] as $specialChild) {
+ $tagName = static::getTagName($specialChild['name']);
+
+ if ('period' === $tagName) {
+ $propertyParameters['value'] = 'PERIOD';
+ $propertyValue[] = implode('/', $specialChild['value']);
+ } else {
+ $propertyValue[] = $specialChild['value'];
+ }
+ }
+ break;
+
+ default:
+ $propertyType = static::getTagName($xmlProperty['value'][0]['name']);
+
+ foreach ($xmlProperty['value'] as $value) {
+ $propertyValue[] = $value['value'];
+ }
+
+ if ('date' === $propertyType) {
+ $propertyParameters['value'] = 'DATE';
+ }
+ break;
+ }
+
+ $this->createProperty(
+ $parentComponent,
+ $propertyNamePrefix.$propertyName,
+ $propertyParameters,
+ $propertyType,
+ $propertyValue
+ );
+ }
+ }
+
+ /**
+ * Parse a component.
+ */
+ protected function parseComponent(Component $parentComponent)
+ {
+ $components = $this->pointer['value'] ?: [];
+
+ foreach ($components as $component) {
+ $componentName = static::getTagName($component['name']);
+ $currentComponent = $this->root->createComponent(
+ $componentName,
+ null,
+ false
+ );
+
+ $this->pointer = &$component;
+ $this->parseVCalendarComponents($currentComponent);
+
+ $parentComponent->add($currentComponent);
+ }
+ }
+
+ /**
+ * Create a property.
+ *
+ * @param string $name
+ * @param array $parameters
+ * @param string $type
+ * @param mixed $value
+ */
+ protected function createProperty(Component $parentComponent, $name, $parameters, $type, $value)
+ {
+ $property = $this->root->createProperty(
+ $name,
+ null,
+ $parameters,
+ $type
+ );
+ $parentComponent->add($property);
+ $property->setXmlValue($value);
+ }
+
+ /**
+ * Sets the input data.
+ *
+ * @param resource|string $input
+ */
+ public function setInput($input)
+ {
+ if (is_resource($input)) {
+ $input = stream_get_contents($input);
+ }
+
+ if (is_string($input)) {
+ $reader = new SabreXml\Reader();
+ $reader->elementMap['{'.self::XCAL_NAMESPACE.'}period']
+ = XML\Element\KeyValue::class;
+ $reader->elementMap['{'.self::XCAL_NAMESPACE.'}recur']
+ = XML\Element\KeyValue::class;
+ $reader->xml($input);
+ $input = $reader->parse();
+ }
+
+ $this->input = $input;
+ }
+
+ /**
+ * Get tag name from a Clark notation.
+ *
+ * @param string $clarkedTagName
+ *
+ * @return string
+ */
+ protected static function getTagName($clarkedTagName)
+ {
+ list(, $tagName) = SabreXml\Service::parseClarkNotation($clarkedTagName);
+
+ return $tagName;
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Parser/XML/Element/KeyValue.php b/vendor/sabre/vobject/lib/Parser/XML/Element/KeyValue.php
new file mode 100644
index 0000000..e017725
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Parser/XML/Element/KeyValue.php
@@ -0,0 +1,63 @@
+next();
+ *
+ * $reader->parseInnerTree() will parse the entire sub-tree, and advance to
+ * the next element.
+ *
+ * @param XML\Reader $reader
+ */
+ public static function xmlDeserialize(SabreXml\Reader $reader): array
+ {
+ // If there's no children, we don't do anything.
+ if ($reader->isEmptyElement) {
+ $reader->next();
+
+ return [];
+ }
+
+ $values = [];
+ $reader->read();
+
+ do {
+ if (SabreXml\Reader::ELEMENT === $reader->nodeType) {
+ $name = $reader->localName;
+ $values[$name] = $reader->parseCurrentElement()['value'];
+ } else {
+ $reader->read();
+ }
+ } while (SabreXml\Reader::END_ELEMENT !== $reader->nodeType);
+
+ $reader->read();
+
+ return $values;
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Property.php b/vendor/sabre/vobject/lib/Property.php
new file mode 100644
index 0000000..7cf5914
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Property.php
@@ -0,0 +1,646 @@
+value syntax.
+ *
+ * @param Component $root The root document
+ * @param string $name
+ * @param string|array|null $value
+ * @param array $parameters List of parameters
+ * @param string $group The vcard property group
+ */
+ public function __construct(Component $root, $name, $value = null, array $parameters = [], $group = null, ?int $lineIndex = null, ?string $lineString = null)
+ {
+ $this->name = $name;
+ $this->group = $group;
+
+ $this->root = $root;
+
+ foreach ($parameters as $k => $v) {
+ $this->add($k, $v);
+ }
+
+ if (!is_null($value)) {
+ $this->setValue($value);
+ }
+
+ if (!is_null($lineIndex)) {
+ $this->lineIndex = $lineIndex;
+ }
+
+ if (!is_null($lineString)) {
+ $this->lineString = $lineString;
+ }
+ }
+
+ /**
+ * Updates the current value.
+ *
+ * This may be either a single, or multiple strings in an array.
+ *
+ * @param string|array $value
+ */
+ public function setValue($value)
+ {
+ $this->value = $value;
+ }
+
+ /**
+ * Returns the current value.
+ *
+ * This method will always return a singular value. If this was a
+ * multi-value object, some decision will be made first on how to represent
+ * it as a string.
+ *
+ * To get the correct multi-value version, use getParts.
+ *
+ * @return string
+ */
+ public function getValue()
+ {
+ if (is_array($this->value)) {
+ if (0 == count($this->value)) {
+ return;
+ } elseif (1 === count($this->value)) {
+ return $this->value[0];
+ } else {
+ return $this->getRawMimeDirValue();
+ }
+ } else {
+ return $this->value;
+ }
+ }
+
+ /**
+ * Sets a multi-valued property.
+ */
+ public function setParts(array $parts)
+ {
+ $this->value = $parts;
+ }
+
+ /**
+ * Returns a multi-valued property.
+ *
+ * This method always returns an array, if there was only a single value,
+ * it will still be wrapped in an array.
+ *
+ * @return array
+ */
+ public function getParts()
+ {
+ if (is_null($this->value)) {
+ return [];
+ } elseif (is_array($this->value)) {
+ return $this->value;
+ } else {
+ return [$this->value];
+ }
+ }
+
+ /**
+ * Adds a new parameter.
+ *
+ * If a parameter with same name already existed, the values will be
+ * combined.
+ * If nameless parameter is added, we try to guess its name.
+ *
+ * @param string $name
+ * @param string|array|null $value
+ */
+ public function add($name, $value = null)
+ {
+ $noName = false;
+ if (null === $name) {
+ $name = Parameter::guessParameterNameByValue($value);
+ $noName = true;
+ }
+
+ if (isset($this->parameters[strtoupper($name)])) {
+ $this->parameters[strtoupper($name)]->addValue($value);
+ } else {
+ $param = new Parameter($this->root, $name, $value);
+ $param->noName = $noName;
+ $this->parameters[$param->name] = $param;
+ }
+ }
+
+ /**
+ * Returns an iterable list of children.
+ *
+ * @return array
+ */
+ public function parameters()
+ {
+ return $this->parameters;
+ }
+
+ /**
+ * Returns the type of value.
+ *
+ * This corresponds to the VALUE= parameter. Every property also has a
+ * 'default' valueType.
+ *
+ * @return string
+ */
+ abstract public function getValueType();
+
+ /**
+ * Sets a raw value coming from a mimedir (iCalendar/vCard) file.
+ *
+ * This has been 'unfolded', so only 1 line will be passed. Unescaping is
+ * not yet done, but parameters are not included.
+ *
+ * @param string $val
+ */
+ abstract public function setRawMimeDirValue($val);
+
+ /**
+ * Returns a raw mime-dir representation of the value.
+ *
+ * @return string
+ */
+ abstract public function getRawMimeDirValue();
+
+ /**
+ * Turns the object back into a serialized blob.
+ *
+ * @return string
+ */
+ public function serialize()
+ {
+ $str = $this->name;
+ if ($this->group) {
+ $str = $this->group.'.'.$this->name;
+ }
+
+ foreach ($this->parameters() as $param) {
+ $str .= ';'.$param->serialize();
+ }
+
+ $str .= ':'.$this->getRawMimeDirValue();
+
+ $str = \preg_replace(
+ '/(
+ (?:^.)? # 1 additional byte in first line because of missing single space (see next line)
+ .{1,74} # max 75 bytes per line (1 byte is used for a single space added after every CRLF)
+ (?![\x80-\xbf]) # prevent splitting multibyte characters
+ )/x',
+ "$1\r\n ",
+ $str
+ );
+
+ // remove single space after last CRLF
+ return \substr($str, 0, -1);
+ }
+
+ /**
+ * Returns the value, in the format it should be encoded for JSON.
+ *
+ * This method must always return an array.
+ *
+ * @return array
+ */
+ public function getJsonValue()
+ {
+ return $this->getParts();
+ }
+
+ /**
+ * Sets the JSON value, as it would appear in a jCard or jCal object.
+ *
+ * The value must always be an array.
+ */
+ public function setJsonValue(array $value)
+ {
+ if (1 === count($value)) {
+ $this->setValue(reset($value));
+ } else {
+ $this->setValue($value);
+ }
+ }
+
+ /**
+ * This method returns an array, with the representation as it should be
+ * encoded in JSON. This is used to create jCard or jCal documents.
+ *
+ * @return array
+ */
+ #[\ReturnTypeWillChange]
+ public function jsonSerialize()
+ {
+ $parameters = [];
+
+ foreach ($this->parameters as $parameter) {
+ if ('VALUE' === $parameter->name) {
+ continue;
+ }
+ $parameters[strtolower($parameter->name)] = $parameter->jsonSerialize();
+ }
+ // In jCard, we need to encode the property-group as a separate 'group'
+ // parameter.
+ if ($this->group) {
+ $parameters['group'] = $this->group;
+ }
+
+ return array_merge(
+ [
+ strtolower($this->name),
+ (object) $parameters,
+ strtolower($this->getValueType()),
+ ],
+ $this->getJsonValue()
+ );
+ }
+
+ /**
+ * Hydrate data from a XML subtree, as it would appear in a xCard or xCal
+ * object.
+ */
+ public function setXmlValue(array $value)
+ {
+ $this->setJsonValue($value);
+ }
+
+ /**
+ * This method serializes the data into XML. This is used to create xCard or
+ * xCal documents.
+ *
+ * @param Xml\Writer $writer XML writer
+ */
+ public function xmlSerialize(Xml\Writer $writer): void
+ {
+ $parameters = [];
+
+ foreach ($this->parameters as $parameter) {
+ if ('VALUE' === $parameter->name) {
+ continue;
+ }
+
+ $parameters[] = $parameter;
+ }
+
+ $writer->startElement(strtolower($this->name));
+
+ if (!empty($parameters)) {
+ $writer->startElement('parameters');
+
+ foreach ($parameters as $parameter) {
+ $writer->startElement(strtolower($parameter->name));
+ $writer->write($parameter);
+ $writer->endElement();
+ }
+
+ $writer->endElement();
+ }
+
+ $this->xmlSerializeValue($writer);
+ $writer->endElement();
+ }
+
+ /**
+ * This method serializes only the value of a property. This is used to
+ * create xCard or xCal documents.
+ *
+ * @param Xml\Writer $writer XML writer
+ */
+ protected function xmlSerializeValue(Xml\Writer $writer)
+ {
+ $valueType = strtolower($this->getValueType());
+
+ foreach ($this->getJsonValue() as $values) {
+ foreach ((array) $values as $value) {
+ $writer->writeElement($valueType, $value);
+ }
+ }
+ }
+
+ /**
+ * Called when this object is being cast to a string.
+ *
+ * If the property only had a single value, you will get just that. In the
+ * case the property had multiple values, the contents will be escaped and
+ * combined with ,.
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return (string) $this->getValue();
+ }
+
+ /* ArrayAccess interface {{{ */
+
+ /**
+ * Checks if an array element exists.
+ *
+ * @param mixed $name
+ *
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetExists($name)
+ {
+ if (is_int($name)) {
+ return parent::offsetExists($name);
+ }
+
+ $name = strtoupper($name);
+
+ foreach ($this->parameters as $parameter) {
+ if ($parameter->name == $name) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns a parameter.
+ *
+ * If the parameter does not exist, null is returned.
+ *
+ * @param string $name
+ *
+ * @return Node
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetGet($name)
+ {
+ if (is_int($name)) {
+ return parent::offsetGet($name);
+ }
+ $name = strtoupper($name);
+
+ if (!isset($this->parameters[$name])) {
+ return;
+ }
+
+ return $this->parameters[$name];
+ }
+
+ /**
+ * Creates a new parameter.
+ *
+ * @param string $name
+ * @param mixed $value
+ *
+ * @return void
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetSet($name, $value)
+ {
+ if (is_int($name)) {
+ parent::offsetSet($name, $value);
+ // @codeCoverageIgnoreStart
+ // This will never be reached, because an exception is always
+ // thrown.
+ return;
+ // @codeCoverageIgnoreEnd
+ }
+
+ $param = new Parameter($this->root, $name, $value);
+ $this->parameters[$param->name] = $param;
+ }
+
+ /**
+ * Removes one or more parameters with the specified name.
+ *
+ * @param string $name
+ *
+ * @return void
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetUnset($name)
+ {
+ if (is_int($name)) {
+ parent::offsetUnset($name);
+ // @codeCoverageIgnoreStart
+ // This will never be reached, because an exception is always
+ // thrown.
+ return;
+ // @codeCoverageIgnoreEnd
+ }
+
+ unset($this->parameters[strtoupper($name)]);
+ }
+
+ /* }}} */
+
+ /**
+ * This method is automatically called when the object is cloned.
+ * Specifically, this will ensure all child elements are also cloned.
+ */
+ public function __clone()
+ {
+ foreach ($this->parameters as $key => $child) {
+ $this->parameters[$key] = clone $child;
+ $this->parameters[$key]->parent = $this;
+ }
+ }
+
+ /**
+ * Validates the node for correctness.
+ *
+ * The following options are supported:
+ * - Node::REPAIR - If something is broken, and automatic repair may
+ * be attempted.
+ *
+ * An array is returned with warnings.
+ *
+ * Every item in the array has the following properties:
+ * * level - (number between 1 and 3 with severity information)
+ * * message - (human readable message)
+ * * node - (reference to the offending node)
+ *
+ * @param int $options
+ *
+ * @return array
+ */
+ public function validate($options = 0)
+ {
+ $warnings = [];
+
+ // Checking if our value is UTF-8
+ if (!StringUtil::isUTF8($this->getRawMimeDirValue())) {
+ $oldValue = $this->getRawMimeDirValue();
+ $level = 3;
+ if ($options & self::REPAIR) {
+ $newValue = StringUtil::convertToUTF8($oldValue);
+ if (true || StringUtil::isUTF8($newValue)) {
+ $this->setRawMimeDirValue($newValue);
+ $level = 1;
+ }
+ }
+
+ if (preg_match('%([\x00-\x08\x0B-\x0C\x0E-\x1F\x7F])%', $oldValue, $matches)) {
+ $message = 'Property contained a control character (0x'.bin2hex($matches[1]).')';
+ } else {
+ $message = 'Property is not valid UTF-8! '.$oldValue;
+ }
+
+ $warnings[] = [
+ 'level' => $level,
+ 'message' => $message,
+ 'node' => $this,
+ ];
+ }
+
+ // Checking if the propertyname does not contain any invalid bytes.
+ if (!preg_match('/^([A-Z0-9-]+)$/', $this->name)) {
+ $warnings[] = [
+ 'level' => $options & self::REPAIR ? 1 : 3,
+ 'message' => 'The propertyname: '.$this->name.' contains invalid characters. Only A-Z, 0-9 and - are allowed',
+ 'node' => $this,
+ ];
+ if ($options & self::REPAIR) {
+ // Uppercasing and converting underscores to dashes.
+ $this->name = strtoupper(
+ str_replace('_', '-', $this->name)
+ );
+ // Removing every other invalid character
+ $this->name = preg_replace('/([^A-Z0-9-])/u', '', $this->name);
+ }
+ }
+
+ if ($encoding = $this->offsetGet('ENCODING')) {
+ if (Document::VCARD40 === $this->root->getDocumentType()) {
+ $warnings[] = [
+ 'level' => 3,
+ 'message' => 'ENCODING parameter is not valid in vCard 4.',
+ 'node' => $this,
+ ];
+ } else {
+ $encoding = (string) $encoding;
+
+ $allowedEncoding = [];
+
+ switch ($this->root->getDocumentType()) {
+ case Document::ICALENDAR20:
+ $allowedEncoding = ['8BIT', 'BASE64'];
+ break;
+ case Document::VCARD21:
+ $allowedEncoding = ['QUOTED-PRINTABLE', 'BASE64', '8BIT'];
+ break;
+ case Document::VCARD30:
+ $allowedEncoding = ['B'];
+ //Repair vCard30 that use BASE64 encoding
+ if ($options & self::REPAIR) {
+ if ('BASE64' === strtoupper($encoding)) {
+ $encoding = 'B';
+ $this['ENCODING'] = $encoding;
+ $warnings[] = [
+ 'level' => 1,
+ 'message' => 'ENCODING=BASE64 has been transformed to ENCODING=B.',
+ 'node' => $this,
+ ];
+ }
+ }
+ break;
+ }
+ if ($allowedEncoding && !in_array(strtoupper($encoding), $allowedEncoding)) {
+ $warnings[] = [
+ 'level' => 3,
+ 'message' => 'ENCODING='.strtoupper($encoding).' is not valid for this document type.',
+ 'node' => $this,
+ ];
+ }
+ }
+ }
+
+ // Validating inner parameters
+ foreach ($this->parameters as $param) {
+ $warnings = array_merge($warnings, $param->validate($options));
+ }
+
+ return $warnings;
+ }
+
+ /**
+ * Call this method on a document if you're done using it.
+ *
+ * It's intended to remove all circular references, so PHP can easily clean
+ * it up.
+ */
+ public function destroy()
+ {
+ parent::destroy();
+ foreach ($this->parameters as $param) {
+ $param->destroy();
+ }
+ $this->parameters = [];
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Property/Binary.php b/vendor/sabre/vobject/lib/Property/Binary.php
new file mode 100644
index 0000000..1262dd0
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Property/Binary.php
@@ -0,0 +1,109 @@
+value = $value[0];
+ } else {
+ throw new \InvalidArgumentException('The argument must either be a string or an array with only one child');
+ }
+ } else {
+ $this->value = $value;
+ }
+ }
+
+ /**
+ * Sets a raw value coming from a mimedir (iCalendar/vCard) file.
+ *
+ * This has been 'unfolded', so only 1 line will be passed. Unescaping is
+ * not yet done, but parameters are not included.
+ *
+ * @param string $val
+ */
+ public function setRawMimeDirValue($val)
+ {
+ $this->value = base64_decode($val);
+ }
+
+ /**
+ * Returns a raw mime-dir representation of the value.
+ *
+ * @return string
+ */
+ public function getRawMimeDirValue()
+ {
+ return base64_encode($this->value);
+ }
+
+ /**
+ * Returns the type of value.
+ *
+ * This corresponds to the VALUE= parameter. Every property also has a
+ * 'default' valueType.
+ *
+ * @return string
+ */
+ public function getValueType()
+ {
+ return 'BINARY';
+ }
+
+ /**
+ * Returns the value, in the format it should be encoded for json.
+ *
+ * This method must always return an array.
+ *
+ * @return array
+ */
+ public function getJsonValue()
+ {
+ return [base64_encode($this->getValue())];
+ }
+
+ /**
+ * Sets the json value, as it would appear in a jCard or jCal object.
+ *
+ * The value must always be an array.
+ */
+ public function setJsonValue(array $value)
+ {
+ $value = array_map('base64_decode', $value);
+ parent::setJsonValue($value);
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Property/Boolean.php b/vendor/sabre/vobject/lib/Property/Boolean.php
new file mode 100644
index 0000000..4bd6ffd
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Property/Boolean.php
@@ -0,0 +1,72 @@
+setValue($val);
+ }
+
+ /**
+ * Returns a raw mime-dir representation of the value.
+ *
+ * @return string
+ */
+ public function getRawMimeDirValue()
+ {
+ return $this->value ? 'TRUE' : 'FALSE';
+ }
+
+ /**
+ * Returns the type of value.
+ *
+ * This corresponds to the VALUE= parameter. Every property also has a
+ * 'default' valueType.
+ *
+ * @return string
+ */
+ public function getValueType()
+ {
+ return 'BOOLEAN';
+ }
+
+ /**
+ * Hydrate data from a XML subtree, as it would appear in a xCard or xCal
+ * object.
+ */
+ public function setXmlValue(array $value)
+ {
+ $value = array_map(
+ function ($value) {
+ return 'true' === $value;
+ },
+ $value
+ );
+ parent::setXmlValue($value);
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Property/FlatText.php b/vendor/sabre/vobject/lib/Property/FlatText.php
new file mode 100644
index 0000000..d15cfe0
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Property/FlatText.php
@@ -0,0 +1,46 @@
+setValue($val);
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Property/FloatValue.php b/vendor/sabre/vobject/lib/Property/FloatValue.php
new file mode 100644
index 0000000..e780ae6
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Property/FloatValue.php
@@ -0,0 +1,124 @@
+delimiter, $val);
+ foreach ($val as &$item) {
+ $item = (float) $item;
+ }
+ $this->setParts($val);
+ }
+
+ /**
+ * Returns a raw mime-dir representation of the value.
+ *
+ * @return string
+ */
+ public function getRawMimeDirValue()
+ {
+ return implode(
+ $this->delimiter,
+ $this->getParts()
+ );
+ }
+
+ /**
+ * Returns the type of value.
+ *
+ * This corresponds to the VALUE= parameter. Every property also has a
+ * 'default' valueType.
+ *
+ * @return string
+ */
+ public function getValueType()
+ {
+ return 'FLOAT';
+ }
+
+ /**
+ * Returns the value, in the format it should be encoded for JSON.
+ *
+ * This method must always return an array.
+ *
+ * @return array
+ */
+ public function getJsonValue()
+ {
+ $val = array_map('floatval', $this->getParts());
+
+ // Special-casing the GEO property.
+ //
+ // See:
+ // http://tools.ietf.org/html/draft-ietf-jcardcal-jcal-04#section-3.4.1.2
+ if ('GEO' === $this->name) {
+ return [$val];
+ }
+
+ return $val;
+ }
+
+ /**
+ * Hydrate data from a XML subtree, as it would appear in a xCard or xCal
+ * object.
+ */
+ public function setXmlValue(array $value)
+ {
+ $value = array_map('floatval', $value);
+ parent::setXmlValue($value);
+ }
+
+ /**
+ * This method serializes only the value of a property. This is used to
+ * create xCard or xCal documents.
+ *
+ * @param Xml\Writer $writer XML writer
+ */
+ protected function xmlSerializeValue(Xml\Writer $writer)
+ {
+ // Special-casing the GEO property.
+ //
+ // See:
+ // http://tools.ietf.org/html/rfc6321#section-3.4.1.2
+ if ('GEO' === $this->name) {
+ $value = array_map('floatval', $this->getParts());
+
+ $writer->writeElement('latitude', $value[0]);
+ $writer->writeElement('longitude', $value[1]);
+ } else {
+ parent::xmlSerializeValue($writer);
+ }
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Property/ICalendar/CalAddress.php b/vendor/sabre/vobject/lib/Property/ICalendar/CalAddress.php
new file mode 100644
index 0000000..c90967d
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Property/ICalendar/CalAddress.php
@@ -0,0 +1,63 @@
+getValue();
+ if (!strpos($input, ':')) {
+ return $input;
+ }
+ list($schema, $everythingElse) = explode(':', $input, 2);
+ $schema = strtolower($schema);
+ if ('mailto' === $schema) {
+ $everythingElse = strtolower($everythingElse);
+ }
+
+ return $schema.':'.$everythingElse;
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Property/ICalendar/Date.php b/vendor/sabre/vobject/lib/Property/ICalendar/Date.php
new file mode 100644
index 0000000..d8e86d1
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Property/ICalendar/Date.php
@@ -0,0 +1,18 @@
+setDateTimes($parts);
+ } else {
+ parent::setParts($parts);
+ }
+ }
+
+ /**
+ * Updates the current value.
+ *
+ * This may be either a single, or multiple strings in an array.
+ *
+ * Instead of strings, you may also use DateTime here.
+ *
+ * @param string|array|DateTimeInterface $value
+ */
+ public function setValue($value)
+ {
+ if (is_array($value) && isset($value[0]) && $value[0] instanceof DateTimeInterface) {
+ $this->setDateTimes($value);
+ } elseif ($value instanceof DateTimeInterface) {
+ $this->setDateTimes([$value]);
+ } else {
+ parent::setValue($value);
+ }
+ }
+
+ /**
+ * Sets a raw value coming from a mimedir (iCalendar/vCard) file.
+ *
+ * This has been 'unfolded', so only 1 line will be passed. Unescaping is
+ * not yet done, but parameters are not included.
+ *
+ * @param string $val
+ */
+ public function setRawMimeDirValue($val)
+ {
+ $this->setValue(explode($this->delimiter, $val));
+ }
+
+ /**
+ * Returns a raw mime-dir representation of the value.
+ *
+ * @return string
+ */
+ public function getRawMimeDirValue()
+ {
+ return implode($this->delimiter, $this->getParts());
+ }
+
+ /**
+ * Returns true if this is a DATE-TIME value, false if it's a DATE.
+ *
+ * @return bool
+ */
+ public function hasTime()
+ {
+ return 'DATE' !== strtoupper((string) $this['VALUE']);
+ }
+
+ /**
+ * Returns true if this is a floating DATE or DATE-TIME.
+ *
+ * Note that DATE is always floating.
+ */
+ public function isFloating()
+ {
+ return
+ !$this->hasTime() ||
+ (
+ !isset($this['TZID']) &&
+ false === strpos($this->getValue(), 'Z')
+ );
+ }
+
+ /**
+ * Returns a date-time value.
+ *
+ * Note that if this property contained more than 1 date-time, only the
+ * first will be returned. To get an array with multiple values, call
+ * getDateTimes.
+ *
+ * If no timezone information is known, because it's either an all-day
+ * property or floating time, we will use the DateTimeZone argument to
+ * figure out the exact date.
+ *
+ * @param DateTimeZone $timeZone
+ *
+ * @return \DateTimeImmutable
+ */
+ public function getDateTime(?DateTimeZone $timeZone = null)
+ {
+ $dt = $this->getDateTimes($timeZone);
+ if (!$dt) {
+ return;
+ }
+
+ return $dt[0];
+ }
+
+ /**
+ * Returns multiple date-time values.
+ *
+ * If no timezone information is known, because it's either an all-day
+ * property or floating time, we will use the DateTimeZone argument to
+ * figure out the exact date.
+ *
+ * @param DateTimeZone $timeZone
+ *
+ * @return \DateTimeImmutable[]
+ * @return \DateTime[]
+ */
+ public function getDateTimes(?DateTimeZone $timeZone = null)
+ {
+ // Does the property have a TZID?
+ $tzid = $this['TZID'];
+
+ if ($tzid) {
+ $timeZone = TimeZoneUtil::getTimeZone((string) $tzid, $this->root);
+ }
+
+ $dts = [];
+ foreach ($this->getParts() as $part) {
+ $dts[] = DateTimeParser::parse($part, $timeZone);
+ }
+
+ return $dts;
+ }
+
+ /**
+ * Sets the property as a DateTime object.
+ *
+ * @param bool isFloating If set to true, timezones will be ignored
+ */
+ public function setDateTime(DateTimeInterface $dt, $isFloating = false)
+ {
+ $this->setDateTimes([$dt], $isFloating);
+ }
+
+ /**
+ * Sets the property as multiple date-time objects.
+ *
+ * The first value will be used as a reference for the timezones, and all
+ * the other values will be adjusted for that timezone
+ *
+ * @param DateTimeInterface[] $dt
+ * @param bool isFloating If set to true, timezones will be ignored
+ */
+ public function setDateTimes(array $dt, $isFloating = false)
+ {
+ $values = [];
+
+ if ($this->hasTime()) {
+ $tz = null;
+ $isUtc = false;
+
+ foreach ($dt as $d) {
+ if ($isFloating) {
+ $values[] = $d->format('Ymd\\THis');
+ continue;
+ }
+ if (is_null($tz)) {
+ $tz = $d->getTimeZone();
+ $isUtc = in_array($tz->getName(), ['UTC', 'GMT', 'Z', '+00:00']);
+ if (!$isUtc) {
+ $this->offsetSet('TZID', $tz->getName());
+ }
+ } else {
+ $d = $d->setTimeZone($tz);
+ }
+
+ if ($isUtc) {
+ $values[] = $d->format('Ymd\\THis\\Z');
+ } else {
+ $values[] = $d->format('Ymd\\THis');
+ }
+ }
+ if ($isUtc || $isFloating) {
+ $this->offsetUnset('TZID');
+ }
+ } else {
+ foreach ($dt as $d) {
+ $values[] = $d->format('Ymd');
+ }
+ $this->offsetUnset('TZID');
+ }
+
+ $this->value = $values;
+ }
+
+ /**
+ * Returns the type of value.
+ *
+ * This corresponds to the VALUE= parameter. Every property also has a
+ * 'default' valueType.
+ *
+ * @return string
+ */
+ public function getValueType()
+ {
+ return $this->hasTime() ? 'DATE-TIME' : 'DATE';
+ }
+
+ /**
+ * Returns the value, in the format it should be encoded for JSON.
+ *
+ * This method must always return an array.
+ *
+ * @return array
+ */
+ public function getJsonValue()
+ {
+ $dts = $this->getDateTimes();
+ $hasTime = $this->hasTime();
+ $isFloating = $this->isFloating();
+
+ $tz = $dts[0]->getTimeZone();
+ $isUtc = $isFloating ? false : in_array($tz->getName(), ['UTC', 'GMT', 'Z']);
+
+ return array_map(
+ function (DateTimeInterface $dt) use ($hasTime, $isUtc) {
+ if ($hasTime) {
+ return $dt->format('Y-m-d\\TH:i:s').($isUtc ? 'Z' : '');
+ } else {
+ return $dt->format('Y-m-d');
+ }
+ },
+ $dts
+ );
+ }
+
+ /**
+ * Sets the json value, as it would appear in a jCard or jCal object.
+ *
+ * The value must always be an array.
+ */
+ public function setJsonValue(array $value)
+ {
+ // dates and times in jCal have one difference to dates and times in
+ // iCalendar. In jCal date-parts are separated by dashes, and
+ // time-parts are separated by colons. It makes sense to just remove
+ // those.
+ $this->setValue(
+ array_map(
+ function ($item) {
+ return strtr($item, [':' => '', '-' => '']);
+ },
+ $value
+ )
+ );
+ }
+
+ /**
+ * We need to intercept offsetSet, because it may be used to alter the
+ * VALUE from DATE-TIME to DATE or vice-versa.
+ *
+ * @param string $name
+ * @param mixed $value
+ *
+ * @return void
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetSet($name, $value)
+ {
+ parent::offsetSet($name, $value);
+ if ('VALUE' !== strtoupper($name)) {
+ return;
+ }
+
+ // This will ensure that dates are correctly encoded.
+ $this->setDateTimes($this->getDateTimes());
+ }
+
+ /**
+ * Validates the node for correctness.
+ *
+ * The following options are supported:
+ * Node::REPAIR - May attempt to automatically repair the problem.
+ *
+ * This method returns an array with detected problems.
+ * Every element has the following properties:
+ *
+ * * level - problem level.
+ * * message - A human-readable string describing the issue.
+ * * node - A reference to the problematic node.
+ *
+ * The level means:
+ * 1 - The issue was repaired (only happens if REPAIR was turned on)
+ * 2 - An inconsequential issue
+ * 3 - A severe issue.
+ *
+ * @param int $options
+ *
+ * @return array
+ */
+ public function validate($options = 0)
+ {
+ $messages = parent::validate($options);
+ $valueType = $this->getValueType();
+ $values = $this->getParts();
+ foreach ($values as $value) {
+ try {
+ switch ($valueType) {
+ case 'DATE':
+ DateTimeParser::parseDate($value);
+ break;
+ case 'DATE-TIME':
+ DateTimeParser::parseDateTime($value);
+ break;
+ }
+ } catch (InvalidDataException $e) {
+ $messages[] = [
+ 'level' => 3,
+ 'message' => 'The supplied value ('.$value.') is not a correct '.$valueType,
+ 'node' => $this,
+ ];
+ break;
+ }
+ }
+
+ return $messages;
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Property/ICalendar/Duration.php b/vendor/sabre/vobject/lib/Property/ICalendar/Duration.php
new file mode 100644
index 0000000..e18fe19
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Property/ICalendar/Duration.php
@@ -0,0 +1,79 @@
+setValue(explode($this->delimiter, $val));
+ }
+
+ /**
+ * Returns a raw mime-dir representation of the value.
+ *
+ * @return string
+ */
+ public function getRawMimeDirValue()
+ {
+ return implode($this->delimiter, $this->getParts());
+ }
+
+ /**
+ * Returns the type of value.
+ *
+ * This corresponds to the VALUE= parameter. Every property also has a
+ * 'default' valueType.
+ *
+ * @return string
+ */
+ public function getValueType()
+ {
+ return 'DURATION';
+ }
+
+ /**
+ * Returns a DateInterval representation of the Duration property.
+ *
+ * If the property has more than one value, only the first is returned.
+ *
+ * @return \DateInterval
+ */
+ public function getDateInterval()
+ {
+ $parts = $this->getParts();
+ $value = $parts[0];
+
+ return DateTimeParser::parseDuration($value);
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Property/ICalendar/Period.php b/vendor/sabre/vobject/lib/Property/ICalendar/Period.php
new file mode 100644
index 0000000..ae8a789
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Property/ICalendar/Period.php
@@ -0,0 +1,135 @@
+setValue(explode($this->delimiter, $val));
+ }
+
+ /**
+ * Returns a raw mime-dir representation of the value.
+ *
+ * @return string
+ */
+ public function getRawMimeDirValue()
+ {
+ return implode($this->delimiter, $this->getParts());
+ }
+
+ /**
+ * Returns the type of value.
+ *
+ * This corresponds to the VALUE= parameter. Every property also has a
+ * 'default' valueType.
+ *
+ * @return string
+ */
+ public function getValueType()
+ {
+ return 'PERIOD';
+ }
+
+ /**
+ * Sets the json value, as it would appear in a jCard or jCal object.
+ *
+ * The value must always be an array.
+ */
+ public function setJsonValue(array $value)
+ {
+ $value = array_map(
+ function ($item) {
+ return strtr(implode('/', $item), [':' => '', '-' => '']);
+ },
+ $value
+ );
+ parent::setJsonValue($value);
+ }
+
+ /**
+ * Returns the value, in the format it should be encoded for json.
+ *
+ * This method must always return an array.
+ *
+ * @return array
+ */
+ public function getJsonValue()
+ {
+ $return = [];
+ foreach ($this->getParts() as $item) {
+ list($start, $end) = explode('/', $item, 2);
+
+ $start = DateTimeParser::parseDateTime($start);
+
+ // This is a duration value.
+ if ('P' === $end[0]) {
+ $return[] = [
+ $start->format('Y-m-d\\TH:i:s'),
+ $end,
+ ];
+ } else {
+ $end = DateTimeParser::parseDateTime($end);
+ $return[] = [
+ $start->format('Y-m-d\\TH:i:s'),
+ $end->format('Y-m-d\\TH:i:s'),
+ ];
+ }
+ }
+
+ return $return;
+ }
+
+ /**
+ * This method serializes only the value of a property. This is used to
+ * create xCard or xCal documents.
+ *
+ * @param Xml\Writer $writer XML writer
+ */
+ protected function xmlSerializeValue(Xml\Writer $writer)
+ {
+ $writer->startElement(strtolower($this->getValueType()));
+ $value = $this->getJsonValue();
+ $writer->writeElement('start', $value[0][0]);
+
+ if ('P' === $value[0][1][0]) {
+ $writer->writeElement('duration', $value[0][1]);
+ } else {
+ $writer->writeElement('end', $value[0][1]);
+ }
+
+ $writer->endElement();
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Property/ICalendar/Recur.php b/vendor/sabre/vobject/lib/Property/ICalendar/Recur.php
new file mode 100644
index 0000000..cd3d7a5
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Property/ICalendar/Recur.php
@@ -0,0 +1,344 @@
+value array that is accessible using
+ * getParts, and may be set using setParts.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Recur extends Property
+{
+ /**
+ * Updates the current value.
+ *
+ * This may be either a single, or multiple strings in an array.
+ *
+ * @param string|array $value
+ */
+ public function setValue($value)
+ {
+ // If we're getting the data from json, we'll be receiving an object
+ if ($value instanceof \StdClass) {
+ $value = (array) $value;
+ }
+
+ if (is_array($value)) {
+ $newVal = [];
+ foreach ($value as $k => $v) {
+ if (is_string($v)) {
+ $v = strtoupper($v);
+
+ // The value had multiple sub-values
+ if (false !== strpos($v, ',')) {
+ $v = explode(',', $v);
+ }
+ if (0 === strcmp($k, 'until')) {
+ $v = strtr($v, [':' => '', '-' => '']);
+ }
+ } elseif (is_array($v)) {
+ $v = array_map('strtoupper', $v);
+ }
+
+ $newVal[strtoupper($k)] = $v;
+ }
+ $this->value = $newVal;
+ } elseif (is_string($value)) {
+ $this->value = self::stringToArray($value);
+ } else {
+ throw new \InvalidArgumentException('You must either pass a string, or a key=>value array');
+ }
+ }
+
+ /**
+ * Returns the current value.
+ *
+ * This method will always return a singular value. If this was a
+ * multi-value object, some decision will be made first on how to represent
+ * it as a string.
+ *
+ * To get the correct multi-value version, use getParts.
+ *
+ * @return string
+ */
+ public function getValue()
+ {
+ $out = [];
+ foreach ($this->value as $key => $value) {
+ $out[] = $key.'='.(is_array($value) ? implode(',', $value) : $value);
+ }
+
+ return strtoupper(implode(';', $out));
+ }
+
+ /**
+ * Sets a multi-valued property.
+ */
+ public function setParts(array $parts)
+ {
+ $this->setValue($parts);
+ }
+
+ /**
+ * Returns a multi-valued property.
+ *
+ * This method always returns an array, if there was only a single value,
+ * it will still be wrapped in an array.
+ *
+ * @return array
+ */
+ public function getParts()
+ {
+ return $this->value;
+ }
+
+ /**
+ * Sets a raw value coming from a mimedir (iCalendar/vCard) file.
+ *
+ * This has been 'unfolded', so only 1 line will be passed. Unescaping is
+ * not yet done, but parameters are not included.
+ *
+ * @param string $val
+ */
+ public function setRawMimeDirValue($val)
+ {
+ $this->setValue($val);
+ }
+
+ /**
+ * Returns a raw mime-dir representation of the value.
+ *
+ * @return string
+ */
+ public function getRawMimeDirValue()
+ {
+ return $this->getValue();
+ }
+
+ /**
+ * Returns the type of value.
+ *
+ * This corresponds to the VALUE= parameter. Every property also has a
+ * 'default' valueType.
+ *
+ * @return string
+ */
+ public function getValueType()
+ {
+ return 'RECUR';
+ }
+
+ /**
+ * Returns the value, in the format it should be encoded for json.
+ *
+ * This method must always return an array.
+ *
+ * @return array
+ */
+ public function getJsonValue()
+ {
+ $values = [];
+ foreach ($this->getParts() as $k => $v) {
+ if (0 === strcmp($k, 'UNTIL')) {
+ $date = new DateTime($this->root, null, $v);
+ $values[strtolower($k)] = $date->getJsonValue()[0];
+ } elseif (0 === strcmp($k, 'COUNT')) {
+ $values[strtolower($k)] = intval($v);
+ } else {
+ $values[strtolower($k)] = $v;
+ }
+ }
+
+ return [$values];
+ }
+
+ /**
+ * This method serializes only the value of a property. This is used to
+ * create xCard or xCal documents.
+ *
+ * @param Xml\Writer $writer XML writer
+ */
+ protected function xmlSerializeValue(Xml\Writer $writer)
+ {
+ $valueType = strtolower($this->getValueType());
+
+ foreach ($this->getJsonValue() as $value) {
+ $writer->writeElement($valueType, $value);
+ }
+ }
+
+ /**
+ * Parses an RRULE value string, and turns it into a struct-ish array.
+ *
+ * @param string $value
+ *
+ * @return array
+ */
+ public static function stringToArray($value)
+ {
+ $value = strtoupper($value);
+ $newValue = [];
+ foreach (explode(';', $value) as $part) {
+ // Skipping empty parts.
+ if (empty($part)) {
+ continue;
+ }
+
+ $parts = explode('=', $part);
+
+ if (2 !== count($parts)) {
+ throw new InvalidDataException('The supplied iCalendar RRULE part is incorrect: '.$part);
+ }
+
+ list($partName, $partValue) = $parts;
+
+ // The value itself had multiple values..
+ if (false !== strpos($partValue, ',')) {
+ $partValue = explode(',', $partValue);
+ }
+ $newValue[$partName] = $partValue;
+ }
+
+ return $newValue;
+ }
+
+ /**
+ * Validates the node for correctness.
+ *
+ * The following options are supported:
+ * Node::REPAIR - May attempt to automatically repair the problem.
+ *
+ * This method returns an array with detected problems.
+ * Every element has the following properties:
+ *
+ * * level - problem level.
+ * * message - A human-readable string describing the issue.
+ * * node - A reference to the problematic node.
+ *
+ * The level means:
+ * 1 - The issue was repaired (only happens if REPAIR was turned on)
+ * 2 - An inconsequential issue
+ * 3 - A severe issue.
+ *
+ * @param int $options
+ *
+ * @return array
+ */
+ public function validate($options = 0)
+ {
+ $repair = ($options & self::REPAIR);
+
+ $warnings = parent::validate($options);
+ $values = $this->getParts();
+
+ foreach ($values as $key => $value) {
+ if ('' === $value) {
+ $warnings[] = [
+ 'level' => $repair ? 1 : 3,
+ 'message' => 'Invalid value for '.$key.' in '.$this->name,
+ 'node' => $this,
+ ];
+ if ($repair) {
+ unset($values[$key]);
+ }
+ } elseif ('BYMONTH' == $key) {
+ $byMonth = (array) $value;
+ foreach ($byMonth as $i => $v) {
+ if (!is_numeric($v) || (int) $v < 1 || (int) $v > 12) {
+ $warnings[] = [
+ 'level' => $repair ? 1 : 3,
+ 'message' => 'BYMONTH in RRULE must have value(s) between 1 and 12!',
+ 'node' => $this,
+ ];
+ if ($repair) {
+ if (is_array($value)) {
+ unset($values[$key][$i]);
+ } else {
+ unset($values[$key]);
+ }
+ }
+ }
+ }
+ // if there is no valid entry left, remove the whole value
+ if (is_array($value) && empty($values[$key])) {
+ unset($values[$key]);
+ }
+ } elseif ('BYWEEKNO' == $key) {
+ $byWeekNo = (array) $value;
+ foreach ($byWeekNo as $i => $v) {
+ if (!is_numeric($v) || (int) $v < -53 || 0 == (int) $v || (int) $v > 53) {
+ $warnings[] = [
+ 'level' => $repair ? 1 : 3,
+ 'message' => 'BYWEEKNO in RRULE must have value(s) from -53 to -1, or 1 to 53!',
+ 'node' => $this,
+ ];
+ if ($repair) {
+ if (is_array($value)) {
+ unset($values[$key][$i]);
+ } else {
+ unset($values[$key]);
+ }
+ }
+ }
+ }
+ // if there is no valid entry left, remove the whole value
+ if (is_array($value) && empty($values[$key])) {
+ unset($values[$key]);
+ }
+ } elseif ('BYYEARDAY' == $key) {
+ $byYearDay = (array) $value;
+ foreach ($byYearDay as $i => $v) {
+ if (!is_numeric($v) || (int) $v < -366 || 0 == (int) $v || (int) $v > 366) {
+ $warnings[] = [
+ 'level' => $repair ? 1 : 3,
+ 'message' => 'BYYEARDAY in RRULE must have value(s) from -366 to -1, or 1 to 366!',
+ 'node' => $this,
+ ];
+ if ($repair) {
+ if (is_array($value)) {
+ unset($values[$key][$i]);
+ } else {
+ unset($values[$key]);
+ }
+ }
+ }
+ }
+ // if there is no valid entry left, remove the whole value
+ if (is_array($value) && empty($values[$key])) {
+ unset($values[$key]);
+ }
+ }
+ }
+ if (!isset($values['FREQ'])) {
+ $warnings[] = [
+ 'level' => $repair ? 1 : 3,
+ 'message' => 'FREQ is required in '.$this->name,
+ 'node' => $this,
+ ];
+ if ($repair) {
+ $this->parent->remove($this);
+ }
+ }
+ if ($repair) {
+ $this->setValue($values);
+ }
+
+ return $warnings;
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Property/IntegerValue.php b/vendor/sabre/vobject/lib/Property/IntegerValue.php
new file mode 100644
index 0000000..3ae7752
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Property/IntegerValue.php
@@ -0,0 +1,76 @@
+setValue((int) $val);
+ }
+
+ /**
+ * Returns a raw mime-dir representation of the value.
+ *
+ * @return string
+ */
+ public function getRawMimeDirValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * Returns the type of value.
+ *
+ * This corresponds to the VALUE= parameter. Every property also has a
+ * 'default' valueType.
+ *
+ * @return string
+ */
+ public function getValueType()
+ {
+ return 'INTEGER';
+ }
+
+ /**
+ * Returns the value, in the format it should be encoded for json.
+ *
+ * This method must always return an array.
+ *
+ * @return array
+ */
+ public function getJsonValue()
+ {
+ return [(int) $this->getValue()];
+ }
+
+ /**
+ * Hydrate data from a XML subtree, as it would appear in a xCard or xCal
+ * object.
+ */
+ public function setXmlValue(array $value)
+ {
+ $value = array_map('intval', $value);
+ parent::setXmlValue($value);
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Property/Text.php b/vendor/sabre/vobject/lib/Property/Text.php
new file mode 100644
index 0000000..16d2c07
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Property/Text.php
@@ -0,0 +1,392 @@
+ 5,
+ 'ADR' => 7,
+ ];
+
+ /**
+ * Creates the property.
+ *
+ * You can specify the parameters either in key=>value syntax, in which case
+ * parameters will automatically be created, or you can just pass a list of
+ * Parameter objects.
+ *
+ * @param Component $root The root document
+ * @param string $name
+ * @param string|array|null $value
+ * @param array $parameters List of parameters
+ * @param string $group The vcard property group
+ */
+ public function __construct(Component $root, $name, $value = null, array $parameters = [], $group = null)
+ {
+ // There's two types of multi-valued text properties:
+ // 1. multivalue properties.
+ // 2. structured value properties
+ //
+ // The former is always separated by a comma, the latter by semi-colon.
+ if (in_array($name, $this->structuredValues)) {
+ $this->delimiter = ';';
+ }
+
+ parent::__construct($root, $name, $value, $parameters, $group);
+ }
+
+ /**
+ * Sets a raw value coming from a mimedir (iCalendar/vCard) file.
+ *
+ * This has been 'unfolded', so only 1 line will be passed. Unescaping is
+ * not yet done, but parameters are not included.
+ *
+ * @param string $val
+ */
+ public function setRawMimeDirValue($val)
+ {
+ $this->setValue(MimeDir::unescapeValue($val, $this->delimiter));
+ }
+
+ /**
+ * Sets the value as a quoted-printable encoded string.
+ *
+ * @param string $val
+ */
+ public function setQuotedPrintableValue($val)
+ {
+ $val = quoted_printable_decode($val);
+
+ // Quoted printable only appears in vCard 2.1, and the only character
+ // that may be escaped there is ;. So we are simply splitting on just
+ // that.
+ //
+ // We also don't have to unescape \\, so all we need to look for is a ;
+ // that's not preceded with a \.
+ $regex = '# (?setValue($matches);
+ }
+
+ /**
+ * Returns a raw mime-dir representation of the value.
+ *
+ * @return string
+ */
+ public function getRawMimeDirValue()
+ {
+ $val = $this->getParts();
+
+ if (isset($this->minimumPropertyValues[$this->name])) {
+ $val = array_pad($val, $this->minimumPropertyValues[$this->name], '');
+ }
+
+ foreach ($val as &$item) {
+ if (!is_array($item)) {
+ $item = [$item];
+ }
+
+ foreach ($item as &$subItem) {
+ if (!is_null($subItem)) {
+ $subItem = strtr(
+ $subItem,
+ [
+ '\\' => '\\\\',
+ ';' => '\;',
+ ',' => '\,',
+ "\n" => '\n',
+ "\r" => '',
+ ]
+ );
+ }
+ }
+ $item = implode(',', $item);
+ }
+
+ return implode($this->delimiter, $val);
+ }
+
+ /**
+ * Returns the value, in the format it should be encoded for json.
+ *
+ * This method must always return an array.
+ *
+ * @return array
+ */
+ public function getJsonValue()
+ {
+ // Structured text values should always be returned as a single
+ // array-item. Multi-value text should be returned as multiple items in
+ // the top-array.
+ if (in_array($this->name, $this->structuredValues)) {
+ return [$this->getParts()];
+ }
+
+ return $this->getParts();
+ }
+
+ /**
+ * Returns the type of value.
+ *
+ * This corresponds to the VALUE= parameter. Every property also has a
+ * 'default' valueType.
+ *
+ * @return string
+ */
+ public function getValueType()
+ {
+ return 'TEXT';
+ }
+
+ /**
+ * Turns the object back into a serialized blob.
+ *
+ * @return string
+ */
+ public function serialize()
+ {
+ // We need to kick in a special type of encoding, if it's a 2.1 vcard.
+ if (Document::VCARD21 !== $this->root->getDocumentType()) {
+ return parent::serialize();
+ }
+
+ $val = $this->getParts();
+
+ if (isset($this->minimumPropertyValues[$this->name])) {
+ $val = \array_pad($val, $this->minimumPropertyValues[$this->name], '');
+ }
+
+ // Imploding multiple parts into a single value, and splitting the
+ // values with ;.
+ if (\count($val) > 1) {
+ foreach ($val as $k => $v) {
+ $val[$k] = \str_replace(';', '\;', $v);
+ }
+ $val = \implode(';', $val);
+ } else {
+ $val = $val[0];
+ }
+
+ $str = $this->name;
+ if ($this->group) {
+ $str = $this->group.'.'.$this->name;
+ }
+ foreach ($this->parameters as $param) {
+ if ('QUOTED-PRINTABLE' === $param->getValue()) {
+ continue;
+ }
+ $str .= ';'.$param->serialize();
+ }
+
+ // If the resulting value contains a \n, we must encode it as
+ // quoted-printable.
+ if (false !== \strpos($val, "\n")) {
+ $str .= ';ENCODING=QUOTED-PRINTABLE:';
+ $lastLine = $str;
+ $out = null;
+
+ // The PHP built-in quoted-printable-encode does not correctly
+ // encode newlines for us. Specifically, the \r\n sequence must in
+ // vcards be encoded as =0D=OA and we must insert soft-newlines
+ // every 75 bytes.
+ for ($ii = 0; $ii < \strlen($val); ++$ii) {
+ $ord = \ord($val[$ii]);
+ // These characters are encoded as themselves.
+ if ($ord >= 32 && $ord <= 126) {
+ $lastLine .= $val[$ii];
+ } else {
+ $lastLine .= '='.\strtoupper(\bin2hex($val[$ii]));
+ }
+ if (\strlen($lastLine) >= 75) {
+ // Soft line break
+ $out .= $lastLine."=\r\n ";
+ $lastLine = null;
+ }
+ }
+ if (!\is_null($lastLine)) {
+ $out .= $lastLine."\r\n";
+ }
+
+ return $out;
+ } else {
+ $str .= ':'.$val;
+
+ $str = \preg_replace(
+ '/(
+ (?:^.)? # 1 additional byte in first line because of missing single space (see next line)
+ .{1,74} # max 75 bytes per line (1 byte is used for a single space added after every CRLF)
+ (?![\x80-\xbf]) # prevent splitting multibyte characters
+ )/x',
+ "$1\r\n ",
+ $str
+ );
+
+ // remove single space after last CRLF
+ return \substr($str, 0, -1);
+ }
+ }
+
+ /**
+ * This method serializes only the value of a property. This is used to
+ * create xCard or xCal documents.
+ *
+ * @param Xml\Writer $writer XML writer
+ */
+ protected function xmlSerializeValue(Xml\Writer $writer)
+ {
+ $values = $this->getParts();
+
+ $map = function ($items) use ($values, $writer) {
+ foreach ($items as $i => $item) {
+ $writer->writeElement(
+ $item,
+ !empty($values[$i]) ? $values[$i] : null
+ );
+ }
+ };
+
+ switch ($this->name) {
+ // Special-casing the REQUEST-STATUS property.
+ //
+ // See:
+ // http://tools.ietf.org/html/rfc6321#section-3.4.1.3
+ case 'REQUEST-STATUS':
+ $writer->writeElement('code', $values[0]);
+ $writer->writeElement('description', $values[1]);
+
+ if (isset($values[2])) {
+ $writer->writeElement('data', $values[2]);
+ }
+ break;
+
+ case 'N':
+ $map([
+ 'surname',
+ 'given',
+ 'additional',
+ 'prefix',
+ 'suffix',
+ ]);
+ break;
+
+ case 'GENDER':
+ $map([
+ 'sex',
+ 'text',
+ ]);
+ break;
+
+ case 'ADR':
+ $map([
+ 'pobox',
+ 'ext',
+ 'street',
+ 'locality',
+ 'region',
+ 'code',
+ 'country',
+ ]);
+ break;
+
+ case 'CLIENTPIDMAP':
+ $map([
+ 'sourceid',
+ 'uri',
+ ]);
+ break;
+
+ default:
+ parent::xmlSerializeValue($writer);
+ }
+ }
+
+ /**
+ * Validates the node for correctness.
+ *
+ * The following options are supported:
+ * - Node::REPAIR - If something is broken, and automatic repair may
+ * be attempted.
+ *
+ * An array is returned with warnings.
+ *
+ * Every item in the array has the following properties:
+ * * level - (number between 1 and 3 with severity information)
+ * * message - (human readable message)
+ * * node - (reference to the offending node)
+ *
+ * @param int $options
+ *
+ * @return array
+ */
+ public function validate($options = 0)
+ {
+ $warnings = parent::validate($options);
+
+ if (isset($this->minimumPropertyValues[$this->name])) {
+ $minimum = $this->minimumPropertyValues[$this->name];
+ $parts = $this->getParts();
+ if (count($parts) < $minimum) {
+ $warnings[] = [
+ 'level' => $options & self::REPAIR ? 1 : 3,
+ 'message' => 'The '.$this->name.' property must have at least '.$minimum.' values. It only has '.count($parts),
+ 'node' => $this,
+ ];
+ if ($options & self::REPAIR) {
+ $parts = array_pad($parts, $minimum, '');
+ $this->setParts($parts);
+ }
+ }
+ }
+
+ return $warnings;
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Property/Time.php b/vendor/sabre/vobject/lib/Property/Time.php
new file mode 100644
index 0000000..1b81609
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Property/Time.php
@@ -0,0 +1,131 @@
+setValue(reset($value));
+ } else {
+ $this->setValue($value);
+ }
+ }
+
+ /**
+ * Returns the value, in the format it should be encoded for json.
+ *
+ * This method must always return an array.
+ *
+ * @return array
+ */
+ public function getJsonValue()
+ {
+ $parts = DateTimeParser::parseVCardTime($this->getValue());
+ $timeStr = '';
+
+ // Hour
+ if (!is_null($parts['hour'])) {
+ $timeStr .= $parts['hour'];
+
+ if (!is_null($parts['minute'])) {
+ $timeStr .= ':';
+ }
+ } else {
+ // We know either minute or second _must_ be set, so we insert a
+ // dash for an empty value.
+ $timeStr .= '-';
+ }
+
+ // Minute
+ if (!is_null($parts['minute'])) {
+ $timeStr .= $parts['minute'];
+
+ if (!is_null($parts['second'])) {
+ $timeStr .= ':';
+ }
+ } else {
+ if (isset($parts['second'])) {
+ // Dash for empty minute
+ $timeStr .= '-';
+ }
+ }
+
+ // Second
+ if (!is_null($parts['second'])) {
+ $timeStr .= $parts['second'];
+ }
+
+ // Timezone
+ if (!is_null($parts['timezone'])) {
+ if ('Z' === $parts['timezone']) {
+ $timeStr .= 'Z';
+ } else {
+ $timeStr .=
+ preg_replace('/([0-9]{2})([0-9]{2})$/', '$1:$2', $parts['timezone']);
+ }
+ }
+
+ return [$timeStr];
+ }
+
+ /**
+ * Hydrate data from a XML subtree, as it would appear in a xCard or xCal
+ * object.
+ */
+ public function setXmlValue(array $value)
+ {
+ $value = array_map(
+ function ($value) {
+ return str_replace(':', '', $value);
+ },
+ $value
+ );
+ parent::setXmlValue($value);
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Property/Unknown.php b/vendor/sabre/vobject/lib/Property/Unknown.php
new file mode 100644
index 0000000..6f404c2
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Property/Unknown.php
@@ -0,0 +1,41 @@
+getRawMimeDirValue()];
+ }
+
+ /**
+ * Returns the type of value.
+ *
+ * This corresponds to the VALUE= parameter. Every property also has a
+ * 'default' valueType.
+ *
+ * @return string
+ */
+ public function getValueType()
+ {
+ return 'UNKNOWN';
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Property/Uri.php b/vendor/sabre/vobject/lib/Property/Uri.php
new file mode 100644
index 0000000..1ad1fb1
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Property/Uri.php
@@ -0,0 +1,116 @@
+name, ['URL', 'PHOTO'])) {
+ // If we are encoding a URI value, and this URI value has no
+ // VALUE=URI parameter, we add it anyway.
+ //
+ // This is not required by any spec, but both Apple iCal and Apple
+ // AddressBook (at least in version 10.8) will trip over this if
+ // this is not set, and so it improves compatibility.
+ //
+ // See Issue #227 and #235
+ $parameters['VALUE'] = new Parameter($this->root, 'VALUE', 'URI');
+ }
+
+ return $parameters;
+ }
+
+ /**
+ * Sets a raw value coming from a mimedir (iCalendar/vCard) file.
+ *
+ * This has been 'unfolded', so only 1 line will be passed. Unescaping is
+ * not yet done, but parameters are not included.
+ *
+ * @param string $val
+ */
+ public function setRawMimeDirValue($val)
+ {
+ // Normally we don't need to do any type of unescaping for these
+ // properties, however.. we've noticed that Google Contacts
+ // specifically escapes the colon (:) with a backslash. While I have
+ // no clue why they thought that was a good idea, I'm unescaping it
+ // anyway.
+ //
+ // Good thing backslashes are not allowed in urls. Makes it easy to
+ // assume that a backslash is always intended as an escape character.
+ if ('URL' === $this->name) {
+ $regex = '# (?: (\\\\ (?: \\\\ | : ) ) ) #x';
+ $matches = preg_split($regex, $val, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
+ $newVal = '';
+ foreach ($matches as $match) {
+ switch ($match) {
+ case '\:':
+ $newVal .= ':';
+ break;
+ default:
+ $newVal .= $match;
+ break;
+ }
+ }
+ $this->value = $newVal;
+ } else {
+ $this->value = strtr($val, ['\,' => ',']);
+ }
+ }
+
+ /**
+ * Returns a raw mime-dir representation of the value.
+ *
+ * @return string
+ */
+ public function getRawMimeDirValue()
+ {
+ if (is_array($this->value)) {
+ $value = $this->value[0];
+ } else {
+ $value = $this->value;
+ }
+
+ return strtr($value, [',' => '\,']);
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Property/UtcOffset.php b/vendor/sabre/vobject/lib/Property/UtcOffset.php
new file mode 100644
index 0000000..04b8844
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Property/UtcOffset.php
@@ -0,0 +1,70 @@
+value = $dt->format('Ymd');
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Property/VCard/DateAndOrTime.php b/vendor/sabre/vobject/lib/Property/VCard/DateAndOrTime.php
new file mode 100644
index 0000000..7bf79c4
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Property/VCard/DateAndOrTime.php
@@ -0,0 +1,367 @@
+ 1) {
+ throw new \InvalidArgumentException('Only one value allowed');
+ }
+ if (isset($parts[0]) && $parts[0] instanceof DateTimeInterface) {
+ $this->setDateTime($parts[0]);
+ } else {
+ parent::setParts($parts);
+ }
+ }
+
+ /**
+ * Updates the current value.
+ *
+ * This may be either a single, or multiple strings in an array.
+ *
+ * Instead of strings, you may also use DateTimeInterface here.
+ *
+ * @param string|array|DateTimeInterface $value
+ */
+ public function setValue($value)
+ {
+ if ($value instanceof DateTimeInterface) {
+ $this->setDateTime($value);
+ } else {
+ parent::setValue($value);
+ }
+ }
+
+ /**
+ * Sets the property as a DateTime object.
+ */
+ public function setDateTime(DateTimeInterface $dt)
+ {
+ $tz = $dt->getTimeZone();
+ $isUtc = in_array($tz->getName(), ['UTC', 'GMT', 'Z']);
+
+ if ($isUtc) {
+ $value = $dt->format('Ymd\\THis\\Z');
+ } else {
+ // Calculating the offset.
+ $value = $dt->format('Ymd\\THisO');
+ }
+
+ $this->value = $value;
+ }
+
+ /**
+ * Returns a date-time value.
+ *
+ * Note that if this property contained more than 1 date-time, only the
+ * first will be returned. To get an array with multiple values, call
+ * getDateTimes.
+ *
+ * If no time was specified, we will always use midnight (in the default
+ * timezone) as the time.
+ *
+ * If parts of the date were omitted, such as the year, we will grab the
+ * current values for those. So at the time of writing, if the year was
+ * omitted, we would have filled in 2014.
+ *
+ * @return DateTimeImmutable
+ */
+ public function getDateTime()
+ {
+ $now = new DateTime();
+
+ $tzFormat = 0 === $now->getTimezone()->getOffset($now) ? '\\Z' : 'O';
+ $nowParts = DateTimeParser::parseVCardDateTime($now->format('Ymd\\This'.$tzFormat));
+
+ $dateParts = DateTimeParser::parseVCardDateTime($this->getValue());
+
+ // This sets all the missing parts to the current date/time.
+ // So if the year was missing for a birthday, we're making it 'this
+ // year'.
+ foreach ($dateParts as $k => $v) {
+ if (is_null($v)) {
+ $dateParts[$k] = $nowParts[$k];
+ }
+ }
+
+ return new DateTimeImmutable("$dateParts[year]-$dateParts[month]-$dateParts[date] $dateParts[hour]:$dateParts[minute]:$dateParts[second] $dateParts[timezone]");
+ }
+
+ /**
+ * Returns the value, in the format it should be encoded for json.
+ *
+ * This method must always return an array.
+ *
+ * @return array
+ */
+ public function getJsonValue()
+ {
+ $parts = DateTimeParser::parseVCardDateTime($this->getValue());
+
+ $dateStr = '';
+
+ // Year
+ if (!is_null($parts['year'])) {
+ $dateStr .= $parts['year'];
+
+ if (!is_null($parts['month'])) {
+ // If a year and a month is set, we need to insert a separator
+ // dash.
+ $dateStr .= '-';
+ }
+ } else {
+ if (!is_null($parts['month']) || !is_null($parts['date'])) {
+ // Inserting two dashes
+ $dateStr .= '--';
+ }
+ }
+
+ // Month
+ if (!is_null($parts['month'])) {
+ $dateStr .= $parts['month'];
+
+ if (isset($parts['date'])) {
+ // If month and date are set, we need the separator dash.
+ $dateStr .= '-';
+ }
+ } elseif (isset($parts['date'])) {
+ // If the month is empty, and a date is set, we need a 'empty
+ // dash'
+ $dateStr .= '-';
+ }
+
+ // Date
+ if (!is_null($parts['date'])) {
+ $dateStr .= $parts['date'];
+ }
+
+ // Early exit if we don't have a time string.
+ if (is_null($parts['hour']) && is_null($parts['minute']) && is_null($parts['second'])) {
+ return [$dateStr];
+ }
+
+ $dateStr .= 'T';
+
+ // Hour
+ if (!is_null($parts['hour'])) {
+ $dateStr .= $parts['hour'];
+
+ if (!is_null($parts['minute'])) {
+ $dateStr .= ':';
+ }
+ } else {
+ // We know either minute or second _must_ be set, so we insert a
+ // dash for an empty value.
+ $dateStr .= '-';
+ }
+
+ // Minute
+ if (!is_null($parts['minute'])) {
+ $dateStr .= $parts['minute'];
+
+ if (!is_null($parts['second'])) {
+ $dateStr .= ':';
+ }
+ } elseif (isset($parts['second'])) {
+ // Dash for empty minute
+ $dateStr .= '-';
+ }
+
+ // Second
+ if (!is_null($parts['second'])) {
+ $dateStr .= $parts['second'];
+ }
+
+ // Timezone
+ if (!is_null($parts['timezone'])) {
+ $dateStr .= $parts['timezone'];
+ }
+
+ return [$dateStr];
+ }
+
+ /**
+ * This method serializes only the value of a property. This is used to
+ * create xCard or xCal documents.
+ *
+ * @param Xml\Writer $writer XML writer
+ */
+ protected function xmlSerializeValue(Xml\Writer $writer)
+ {
+ $valueType = strtolower($this->getValueType());
+ $parts = DateTimeParser::parseVCardDateAndOrTime($this->getValue());
+ $value = '';
+
+ // $d = defined
+ $d = function ($part) use ($parts) {
+ return !is_null($parts[$part]);
+ };
+
+ // $r = read
+ $r = function ($part) use ($parts) {
+ return $parts[$part];
+ };
+
+ // From the Relax NG Schema.
+ //
+ // # 4.3.1
+ // value-date = element date {
+ // xsd:string { pattern = "\d{8}|\d{4}-\d\d|--\d\d(\d\d)?|---\d\d" }
+ // }
+ if (($d('year') || $d('month') || $d('date'))
+ && (!$d('hour') && !$d('minute') && !$d('second') && !$d('timezone'))) {
+ if ($d('year') && $d('month') && $d('date')) {
+ $value .= $r('year').$r('month').$r('date');
+ } elseif ($d('year') && $d('month') && !$d('date')) {
+ $value .= $r('year').'-'.$r('month');
+ } elseif (!$d('year') && $d('month')) {
+ $value .= '--'.$r('month').$r('date');
+ } elseif (!$d('year') && !$d('month') && $d('date')) {
+ $value .= '---'.$r('date');
+ }
+
+ // # 4.3.2
+ // value-time = element time {
+ // xsd:string { pattern = "(\d\d(\d\d(\d\d)?)?|-\d\d(\d\d?)|--\d\d)"
+ // ~ "(Z|[+\-]\d\d(\d\d)?)?" }
+ // }
+ } elseif ((!$d('year') && !$d('month') && !$d('date'))
+ && ($d('hour') || $d('minute') || $d('second'))) {
+ if ($d('hour')) {
+ $value .= $r('hour').$r('minute').$r('second');
+ } elseif ($d('minute')) {
+ $value .= '-'.$r('minute').$r('second');
+ } elseif ($d('second')) {
+ $value .= '--'.$r('second');
+ }
+
+ $value .= $r('timezone');
+
+ // # 4.3.3
+ // value-date-time = element date-time {
+ // xsd:string { pattern = "(\d{8}|--\d{4}|---\d\d)T\d\d(\d\d(\d\d)?)?"
+ // ~ "(Z|[+\-]\d\d(\d\d)?)?" }
+ // }
+ } elseif ($d('date') && $d('hour')) {
+ if ($d('year') && $d('month') && $d('date')) {
+ $value .= $r('year').$r('month').$r('date');
+ } elseif (!$d('year') && $d('month') && $d('date')) {
+ $value .= '--'.$r('month').$r('date');
+ } elseif (!$d('year') && !$d('month') && $d('date')) {
+ $value .= '---'.$r('date');
+ }
+
+ $value .= 'T'.$r('hour').$r('minute').$r('second').
+ $r('timezone');
+ }
+
+ $writer->writeElement($valueType, $value);
+ }
+
+ /**
+ * Sets a raw value coming from a mimedir (iCalendar/vCard) file.
+ *
+ * This has been 'unfolded', so only 1 line will be passed. Unescaping is
+ * not yet done, but parameters are not included.
+ *
+ * @param string $val
+ */
+ public function setRawMimeDirValue($val)
+ {
+ $this->setValue($val);
+ }
+
+ /**
+ * Returns a raw mime-dir representation of the value.
+ *
+ * @return string
+ */
+ public function getRawMimeDirValue()
+ {
+ return implode($this->delimiter, $this->getParts());
+ }
+
+ /**
+ * Validates the node for correctness.
+ *
+ * The following options are supported:
+ * Node::REPAIR - May attempt to automatically repair the problem.
+ *
+ * This method returns an array with detected problems.
+ * Every element has the following properties:
+ *
+ * * level - problem level.
+ * * message - A human-readable string describing the issue.
+ * * node - A reference to the problematic node.
+ *
+ * The level means:
+ * 1 - The issue was repaired (only happens if REPAIR was turned on)
+ * 2 - An inconsequential issue
+ * 3 - A severe issue.
+ *
+ * @param int $options
+ *
+ * @return array
+ */
+ public function validate($options = 0)
+ {
+ $messages = parent::validate($options);
+ $value = $this->getValue();
+
+ try {
+ DateTimeParser::parseVCardDateTime($value);
+ } catch (InvalidDataException $e) {
+ $messages[] = [
+ 'level' => 3,
+ 'message' => 'The supplied value ('.$value.') is not a correct DATE-AND-OR-TIME property',
+ 'node' => $this,
+ ];
+ }
+
+ return $messages;
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Property/VCard/DateTime.php b/vendor/sabre/vobject/lib/Property/VCard/DateTime.php
new file mode 100644
index 0000000..49c1f35
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Property/VCard/DateTime.php
@@ -0,0 +1,28 @@
+setValue($val);
+ }
+
+ /**
+ * Returns a raw mime-dir representation of the value.
+ *
+ * @return string
+ */
+ public function getRawMimeDirValue()
+ {
+ return $this->getValue();
+ }
+
+ /**
+ * Returns the type of value.
+ *
+ * This corresponds to the VALUE= parameter. Every property also has a
+ * 'default' valueType.
+ *
+ * @return string
+ */
+ public function getValueType()
+ {
+ return 'LANGUAGE-TAG';
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Property/VCard/PhoneNumber.php b/vendor/sabre/vobject/lib/Property/VCard/PhoneNumber.php
new file mode 100644
index 0000000..b714ffd
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Property/VCard/PhoneNumber.php
@@ -0,0 +1,30 @@
+
+ */
+class PhoneNumber extends Property\Text
+{
+ protected $structuredValues = [];
+
+ /**
+ * Returns the type of value.
+ *
+ * This corresponds to the VALUE= parameter. Every property also has a
+ * 'default' valueType.
+ *
+ * @return string
+ */
+ public function getValueType()
+ {
+ return 'PHONE-NUMBER';
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Property/VCard/TimeStamp.php b/vendor/sabre/vobject/lib/Property/VCard/TimeStamp.php
new file mode 100644
index 0000000..da6ea3d
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Property/VCard/TimeStamp.php
@@ -0,0 +1,81 @@
+getValue());
+
+ $dateStr =
+ $parts['year'].'-'.
+ $parts['month'].'-'.
+ $parts['date'].'T'.
+ $parts['hour'].':'.
+ $parts['minute'].':'.
+ $parts['second'];
+
+ // Timezone
+ if (!is_null($parts['timezone'])) {
+ $dateStr .= $parts['timezone'];
+ }
+
+ return [$dateStr];
+ }
+
+ /**
+ * This method serializes only the value of a property. This is used to
+ * create xCard or xCal documents.
+ *
+ * @param Xml\Writer $writer XML writer
+ */
+ protected function xmlSerializeValue(Xml\Writer $writer)
+ {
+ // xCard is the only XML and JSON format that has the same date and time
+ // format than vCard.
+ $valueType = strtolower($this->getValueType());
+ $writer->writeElement($valueType, $this->getValue());
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Reader.php b/vendor/sabre/vobject/lib/Reader.php
new file mode 100644
index 0000000..055d546
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Reader.php
@@ -0,0 +1,95 @@
+setCharset($charset);
+ $result = $parser->parse($data, $options);
+
+ return $result;
+ }
+
+ /**
+ * Parses a jCard or jCal object, and returns the top component.
+ *
+ * The options argument is a bitfield. Pass any of the OPTIONS constant to
+ * alter the parsers' behaviour.
+ *
+ * You can either a string, a readable stream, or an array for its input.
+ * Specifying the array is useful if json_decode was already called on the
+ * input.
+ *
+ * @param string|resource|array $data
+ * @param int $options
+ *
+ * @return Document
+ */
+ public static function readJson($data, $options = 0)
+ {
+ $parser = new Parser\Json();
+ $result = $parser->parse($data, $options);
+
+ return $result;
+ }
+
+ /**
+ * Parses a xCard or xCal object, and returns the top component.
+ *
+ * The options argument is a bitfield. Pass any of the OPTIONS constant to
+ * alter the parsers' behaviour.
+ *
+ * You can either supply a string, or a readable stream for input.
+ *
+ * @param string|resource $data
+ * @param int $options
+ *
+ * @return Document
+ */
+ public static function readXML($data, $options = 0)
+ {
+ $parser = new Parser\XML();
+ $result = $parser->parse($data, $options);
+
+ return $result;
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Recur/EventIterator.php b/vendor/sabre/vobject/lib/Recur/EventIterator.php
new file mode 100644
index 0000000..212c147
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Recur/EventIterator.php
@@ -0,0 +1,501 @@
+timeZone = $timeZone;
+
+ if (is_array($input)) {
+ $events = $input;
+ } elseif ($input instanceof VEvent) {
+ // Single instance mode.
+ $events = [$input];
+ } else {
+ // Calendar + UID mode.
+ $uid = (string) $uid;
+ if (!$uid) {
+ throw new InvalidArgumentException('The UID argument is required when a VCALENDAR is passed to this constructor');
+ }
+ if (!isset($input->VEVENT)) {
+ throw new InvalidArgumentException('No events found in this calendar');
+ }
+ $events = $input->getByUID($uid);
+ }
+
+ foreach ($events as $vevent) {
+ if (!isset($vevent->{'RECURRENCE-ID'})) {
+ $this->masterEvent = $vevent;
+ } else {
+ $this->exceptions[
+ $vevent->{'RECURRENCE-ID'}->getDateTime($this->timeZone)->getTimeStamp()
+ ] = true;
+ $this->overriddenEvents[] = $vevent;
+ }
+ }
+
+ if (!$this->masterEvent) {
+ // No base event was found. CalDAV does allow cases where only
+ // overridden instances are stored.
+ //
+ // In this particular case, we're just going to grab the first
+ // event and use that instead. This may not always give the
+ // desired result.
+ if (!count($this->overriddenEvents)) {
+ throw new InvalidArgumentException('This VCALENDAR did not have an event with UID: '.$uid);
+ }
+ $this->masterEvent = array_shift($this->overriddenEvents);
+ }
+
+ $this->startDate = $this->masterEvent->DTSTART->getDateTime($this->timeZone);
+ $this->allDay = !$this->masterEvent->DTSTART->hasTime();
+
+ if (isset($this->masterEvent->EXDATE)) {
+ foreach ($this->masterEvent->EXDATE as $exDate) {
+ foreach ($exDate->getDateTimes($this->timeZone) as $dt) {
+ $this->exceptions[$dt->getTimeStamp()] = true;
+ }
+ }
+ }
+
+ if (isset($this->masterEvent->DTEND)) {
+ $this->eventDuration =
+ $this->masterEvent->DTEND->getDateTime($this->timeZone)->getTimeStamp() -
+ $this->startDate->getTimeStamp();
+ } elseif (isset($this->masterEvent->DURATION)) {
+ $duration = $this->masterEvent->DURATION->getDateInterval();
+ $end = clone $this->startDate;
+ $end = $end->add($duration);
+ $this->eventDuration = $end->getTimeStamp() - $this->startDate->getTimeStamp();
+ } elseif ($this->allDay) {
+ $this->eventDuration = 3600 * 24;
+ } else {
+ $this->eventDuration = 0;
+ }
+
+ if (isset($this->masterEvent->RDATE)) {
+ $rdateValues = [];
+ foreach ($this->masterEvent->RDATE as $rdate) {
+ $rdateValues = array_merge($rdateValues, $rdate->getParts());
+ }
+ $this->recurIterator = new RDateIterator(
+ $rdateValues,
+ $this->startDate
+ );
+ } elseif (isset($this->masterEvent->RRULE)) {
+ $this->recurIterator = new RRuleIterator(
+ $this->masterEvent->RRULE->getParts(),
+ $this->startDate
+ );
+ } else {
+ $this->recurIterator = new RRuleIterator(
+ [
+ 'FREQ' => 'DAILY',
+ 'COUNT' => 1,
+ ],
+ $this->startDate
+ );
+ }
+
+ $this->rewind();
+ if (!$this->valid()) {
+ throw new NoInstancesException('This recurrence rule does not generate any valid instances');
+ }
+ }
+
+ /**
+ * Returns the date for the current position of the iterator.
+ *
+ * @return DateTimeImmutable
+ */
+ #[\ReturnTypeWillChange]
+ public function current()
+ {
+ if ($this->currentDate) {
+ return clone $this->currentDate;
+ }
+ }
+
+ /**
+ * This method returns the start date for the current iteration of the
+ * event.
+ *
+ * @return DateTimeImmutable
+ */
+ public function getDtStart()
+ {
+ if ($this->currentDate) {
+ return clone $this->currentDate;
+ }
+ }
+
+ /**
+ * This method returns the end date for the current iteration of the
+ * event.
+ *
+ * @return DateTimeImmutable
+ */
+ public function getDtEnd()
+ {
+ if (!$this->valid()) {
+ return;
+ }
+ if ($this->currentOverriddenEvent && $this->currentOverriddenEvent->DTEND) {
+ return $this->currentOverriddenEvent->DTEND->getDateTime($this->timeZone);
+ } else {
+ $end = clone $this->currentDate;
+
+ return $end->modify('+'.$this->eventDuration.' seconds');
+ }
+ }
+
+ /**
+ * Returns a VEVENT for the current iterations of the event.
+ *
+ * This VEVENT will have a recurrence id, and its DTSTART and DTEND
+ * altered.
+ *
+ * @return VEvent
+ */
+ public function getEventObject()
+ {
+ if ($this->currentOverriddenEvent) {
+ return $this->currentOverriddenEvent;
+ }
+
+ $event = clone $this->masterEvent;
+
+ // Ignoring the following block, because PHPUnit's code coverage
+ // ignores most of these lines, and this messes with our stats.
+ //
+ // @codeCoverageIgnoreStart
+ unset(
+ $event->RRULE,
+ $event->EXDATE,
+ $event->RDATE,
+ $event->EXRULE,
+ $event->{'RECURRENCE-ID'}
+ );
+ // @codeCoverageIgnoreEnd
+
+ $event->DTSTART->setDateTime($this->getDtStart(), $event->DTSTART->isFloating());
+ if (isset($event->DTEND)) {
+ $event->DTEND->setDateTime($this->getDtEnd(), $event->DTEND->isFloating());
+ }
+ $recurid = clone $event->DTSTART;
+ $recurid->name = 'RECURRENCE-ID';
+ $event->add($recurid);
+
+ return $event;
+ }
+
+ /**
+ * Returns the current position of the iterator.
+ *
+ * This is for us simply a 0-based index.
+ *
+ * @return int
+ */
+ #[\ReturnTypeWillChange]
+ public function key()
+ {
+ // The counter is always 1 ahead.
+ return $this->counter - 1;
+ }
+
+ /**
+ * This is called after next, to see if the iterator is still at a valid
+ * position, or if it's at the end.
+ *
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function valid()
+ {
+ if ($this->counter > Settings::$maxRecurrences && -1 !== Settings::$maxRecurrences) {
+ throw new MaxInstancesExceededException('Recurring events are only allowed to generate '.Settings::$maxRecurrences);
+ }
+
+ return (bool) $this->currentDate;
+ }
+
+ /**
+ * Sets the iterator back to the starting point.
+ *
+ * @return void
+ */
+ #[\ReturnTypeWillChange]
+ public function rewind()
+ {
+ $this->recurIterator->rewind();
+ // re-creating overridden event index.
+ $index = [];
+ foreach ($this->overriddenEvents as $key => $event) {
+ $stamp = $event->DTSTART->getDateTime($this->timeZone)->getTimeStamp();
+ $index[$stamp][] = $key;
+ }
+ krsort($index);
+ $this->counter = 0;
+ $this->overriddenEventsIndex = $index;
+ $this->currentOverriddenEvent = null;
+
+ $this->nextDate = null;
+ $this->currentDate = clone $this->startDate;
+
+ $this->next();
+ }
+
+ /**
+ * Advances the iterator with one step.
+ *
+ * @return void
+ */
+ #[\ReturnTypeWillChange]
+ public function next()
+ {
+ $this->currentOverriddenEvent = null;
+ ++$this->counter;
+ if ($this->nextDate) {
+ // We had a stored value.
+ $nextDate = $this->nextDate;
+ $this->nextDate = null;
+ } else {
+ // We need to ask rruleparser for the next date.
+ // We need to do this until we find a date that's not in the
+ // exception list.
+ do {
+ if (!$this->recurIterator->valid()) {
+ $nextDate = null;
+ break;
+ }
+ $nextDate = $this->recurIterator->current();
+ $this->recurIterator->next();
+ } while (isset($this->exceptions[$nextDate->getTimeStamp()]));
+ }
+
+ // $nextDate now contains what rrule thinks is the next one, but an
+ // overridden event may cut ahead.
+ if ($this->overriddenEventsIndex) {
+ $offsets = end($this->overriddenEventsIndex);
+ $timestamp = key($this->overriddenEventsIndex);
+ $offset = end($offsets);
+ if (!$nextDate || $timestamp < $nextDate->getTimeStamp()) {
+ // Overridden event comes first.
+ $this->currentOverriddenEvent = $this->overriddenEvents[$offset];
+
+ // Putting the rrule next date aside.
+ $this->nextDate = $nextDate;
+ $this->currentDate = $this->currentOverriddenEvent->DTSTART->getDateTime($this->timeZone);
+
+ // Ensuring that this item will only be used once.
+ array_pop($this->overriddenEventsIndex[$timestamp]);
+ if (!$this->overriddenEventsIndex[$timestamp]) {
+ array_pop($this->overriddenEventsIndex);
+ }
+
+ // Exit point!
+ return;
+ }
+ }
+
+ $this->currentDate = $nextDate;
+ }
+
+ /**
+ * Quickly jump to a date in the future.
+ */
+ public function fastForward(DateTimeInterface $dateTime)
+ {
+ while ($this->valid() && $this->getDtEnd() <= $dateTime) {
+ $this->next();
+ }
+ }
+
+ /**
+ * Returns true if this recurring event never ends.
+ *
+ * @return bool
+ */
+ public function isInfinite()
+ {
+ return $this->recurIterator->isInfinite();
+ }
+
+ /**
+ * RRULE parser.
+ *
+ * @var RRuleIterator
+ */
+ protected $recurIterator;
+
+ /**
+ * The duration, in seconds, of the master event.
+ *
+ * We use this to calculate the DTEND for subsequent events.
+ */
+ protected $eventDuration;
+
+ /**
+ * A reference to the main (master) event.
+ *
+ * @var VEVENT
+ */
+ protected $masterEvent;
+
+ /**
+ * List of overridden events.
+ *
+ * @var array
+ */
+ protected $overriddenEvents = [];
+
+ /**
+ * Overridden event index.
+ *
+ * Key is timestamp, value is the list of indexes of the item in the $overriddenEvent
+ * property.
+ *
+ * @var array
+ */
+ protected $overriddenEventsIndex;
+
+ /**
+ * A list of recurrence-id's that are either part of EXDATE, or are
+ * overridden.
+ *
+ * @var array
+ */
+ protected $exceptions = [];
+
+ /**
+ * Internal event counter.
+ *
+ * @var int
+ */
+ protected $counter;
+
+ /**
+ * The very start of the iteration process.
+ *
+ * @var DateTimeImmutable
+ */
+ protected $startDate;
+
+ /**
+ * Where we are currently in the iteration process.
+ *
+ * @var DateTimeImmutable
+ */
+ protected $currentDate;
+
+ /**
+ * The next date from the rrule parser.
+ *
+ * Sometimes we need to temporary store the next date, because an
+ * overridden event came before.
+ *
+ * @var DateTimeImmutable
+ */
+ protected $nextDate;
+
+ /**
+ * The event that overwrites the current iteration.
+ *
+ * @var VEVENT
+ */
+ protected $currentOverriddenEvent;
+}
diff --git a/vendor/sabre/vobject/lib/Recur/MaxInstancesExceededException.php b/vendor/sabre/vobject/lib/Recur/MaxInstancesExceededException.php
new file mode 100644
index 0000000..cb08358
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Recur/MaxInstancesExceededException.php
@@ -0,0 +1,17 @@
+startDate = $start;
+ $this->parseRDate($rrule);
+ $this->currentDate = clone $this->startDate;
+ }
+
+ /* Implementation of the Iterator interface {{{ */
+
+ #[\ReturnTypeWillChange]
+ public function current()
+ {
+ if (!$this->valid()) {
+ return;
+ }
+
+ return clone $this->currentDate;
+ }
+
+ /**
+ * Returns the current item number.
+ *
+ * @return int
+ */
+ #[\ReturnTypeWillChange]
+ public function key()
+ {
+ return $this->counter;
+ }
+
+ /**
+ * Returns whether the current item is a valid item for the recurrence
+ * iterator.
+ *
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function valid()
+ {
+ return $this->counter <= count($this->dates);
+ }
+
+ /**
+ * Resets the iterator.
+ *
+ * @return void
+ */
+ #[\ReturnTypeWillChange]
+ public function rewind()
+ {
+ $this->currentDate = clone $this->startDate;
+ $this->counter = 0;
+ }
+
+ /**
+ * Goes on to the next iteration.
+ *
+ * @return void
+ */
+ #[\ReturnTypeWillChange]
+ public function next()
+ {
+ ++$this->counter;
+ if (!$this->valid()) {
+ return;
+ }
+
+ $this->currentDate =
+ DateTimeParser::parse(
+ $this->dates[$this->counter - 1],
+ $this->startDate->getTimezone()
+ );
+ }
+
+ /* End of Iterator implementation }}} */
+
+ /**
+ * Returns true if this recurring event never ends.
+ *
+ * @return bool
+ */
+ public function isInfinite()
+ {
+ return false;
+ }
+
+ /**
+ * This method allows you to quickly go to the next occurrence after the
+ * specified date.
+ */
+ public function fastForward(DateTimeInterface $dt)
+ {
+ while ($this->valid() && $this->currentDate < $dt) {
+ $this->next();
+ }
+ }
+
+ /**
+ * The reference start date/time for the rrule.
+ *
+ * All calculations are based on this initial date.
+ *
+ * @var DateTimeInterface
+ */
+ protected $startDate;
+
+ /**
+ * The date of the current iteration. You can get this by calling
+ * ->current().
+ *
+ * @var DateTimeInterface
+ */
+ protected $currentDate;
+
+ /**
+ * The current item in the list.
+ *
+ * You can get this number with the key() method.
+ *
+ * @var int
+ */
+ protected $counter = 0;
+
+ /* }}} */
+
+ /**
+ * This method receives a string from an RRULE property, and populates this
+ * class with all the values.
+ *
+ * @param string|array $rrule
+ */
+ protected function parseRDate($rdate)
+ {
+ if (is_string($rdate)) {
+ $rdate = explode(',', $rdate);
+ }
+
+ $this->dates = $rdate;
+ }
+
+ /**
+ * Array with the RRULE dates.
+ *
+ * @var array
+ */
+ protected $dates = [];
+}
diff --git a/vendor/sabre/vobject/lib/Recur/RRuleIterator.php b/vendor/sabre/vobject/lib/Recur/RRuleIterator.php
new file mode 100644
index 0000000..ca53b63
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Recur/RRuleIterator.php
@@ -0,0 +1,1079 @@
+startDate = $start;
+ $this->parseRRule($rrule);
+ $this->currentDate = clone $this->startDate;
+ }
+
+ /* Implementation of the Iterator interface {{{ */
+
+ #[\ReturnTypeWillChange]
+ public function current()
+ {
+ if (!$this->valid()) {
+ return;
+ }
+
+ return clone $this->currentDate;
+ }
+
+ /**
+ * Returns the current item number.
+ *
+ * @return int
+ */
+ #[\ReturnTypeWillChange]
+ public function key()
+ {
+ return $this->counter;
+ }
+
+ /**
+ * Returns whether the current item is a valid item for the recurrence
+ * iterator. This will return false if we've gone beyond the UNTIL or COUNT
+ * statements.
+ *
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function valid()
+ {
+ if (null === $this->currentDate) {
+ return false;
+ }
+ if (!is_null($this->count)) {
+ return $this->counter < $this->count;
+ }
+
+ return is_null($this->until) || $this->currentDate <= $this->until;
+ }
+
+ /**
+ * Resets the iterator.
+ *
+ * @return void
+ */
+ #[\ReturnTypeWillChange]
+ public function rewind()
+ {
+ $this->currentDate = clone $this->startDate;
+ $this->counter = 0;
+ }
+
+ /**
+ * Goes on to the next iteration.
+ *
+ * @return void
+ */
+ #[\ReturnTypeWillChange]
+ public function next()
+ {
+ // Otherwise, we find the next event in the normal RRULE
+ // sequence.
+ switch ($this->frequency) {
+ case 'hourly':
+ $this->nextHourly();
+ break;
+
+ case 'daily':
+ $this->nextDaily();
+ break;
+
+ case 'weekly':
+ $this->nextWeekly();
+ break;
+
+ case 'monthly':
+ $this->nextMonthly();
+ break;
+
+ case 'yearly':
+ $this->nextYearly();
+ break;
+ }
+ ++$this->counter;
+ }
+
+ /* End of Iterator implementation }}} */
+
+ /**
+ * Returns true if this recurring event never ends.
+ *
+ * @return bool
+ */
+ public function isInfinite()
+ {
+ return !$this->count && !$this->until;
+ }
+
+ /**
+ * This method allows you to quickly go to the next occurrence after the
+ * specified date.
+ */
+ public function fastForward(DateTimeInterface $dt)
+ {
+ while ($this->valid() && $this->currentDate < $dt) {
+ $this->next();
+ }
+ }
+
+ /**
+ * The reference start date/time for the rrule.
+ *
+ * All calculations are based on this initial date.
+ *
+ * @var DateTimeInterface
+ */
+ protected $startDate;
+
+ /**
+ * The date of the current iteration. You can get this by calling
+ * ->current().
+ *
+ * @var DateTimeInterface
+ */
+ protected $currentDate;
+
+ /**
+ * The number of hours that the next occurrence of an event
+ * jumped forward, usually because summer time started and
+ * the requested time-of-day like 0230 did not exist on that
+ * day. And so the event was scheduled 1 hour later at 0330.
+ */
+ protected $hourJump = 0;
+
+ /**
+ * Frequency is one of: secondly, minutely, hourly, daily, weekly, monthly,
+ * yearly.
+ *
+ * @var string
+ */
+ protected $frequency;
+
+ /**
+ * The number of recurrences, or 'null' if infinitely recurring.
+ *
+ * @var int
+ */
+ protected $count;
+
+ /**
+ * The interval.
+ *
+ * If for example frequency is set to daily, interval = 2 would mean every
+ * 2 days.
+ *
+ * @var int
+ */
+ protected $interval = 1;
+
+ /**
+ * The last instance of this recurrence, inclusively.
+ *
+ * @var DateTimeInterface|null
+ */
+ protected $until;
+
+ /**
+ * Which seconds to recur.
+ *
+ * This is an array of integers (between 0 and 60)
+ *
+ * @var array
+ */
+ protected $bySecond;
+
+ /**
+ * Which minutes to recur.
+ *
+ * This is an array of integers (between 0 and 59)
+ *
+ * @var array
+ */
+ protected $byMinute;
+
+ /**
+ * Which hours to recur.
+ *
+ * This is an array of integers (between 0 and 23)
+ *
+ * @var array
+ */
+ protected $byHour;
+
+ /**
+ * The current item in the list.
+ *
+ * You can get this number with the key() method.
+ *
+ * @var int
+ */
+ protected $counter = 0;
+
+ /**
+ * Which weekdays to recur.
+ *
+ * This is an array of weekdays
+ *
+ * This may also be preceded by a positive or negative integer. If present,
+ * this indicates the nth occurrence of a specific day within the monthly or
+ * yearly rrule. For instance, -2TU indicates the second-last tuesday of
+ * the month, or year.
+ *
+ * @var array
+ */
+ protected $byDay;
+
+ /**
+ * Which days of the month to recur.
+ *
+ * This is an array of days of the months (1-31). The value can also be
+ * negative. -5 for instance means the 5th last day of the month.
+ *
+ * @var array
+ */
+ protected $byMonthDay;
+
+ /**
+ * Which days of the year to recur.
+ *
+ * This is an array with days of the year (1 to 366). The values can also
+ * be negative. For instance, -1 will always represent the last day of the
+ * year. (December 31st).
+ *
+ * @var array
+ */
+ protected $byYearDay;
+
+ /**
+ * Which week numbers to recur.
+ *
+ * This is an array of integers from 1 to 53. The values can also be
+ * negative. -1 will always refer to the last week of the year.
+ *
+ * @var array
+ */
+ protected $byWeekNo;
+
+ /**
+ * Which months to recur.
+ *
+ * This is an array of integers from 1 to 12.
+ *
+ * @var array
+ */
+ protected $byMonth;
+
+ /**
+ * Which items in an existing st to recur.
+ *
+ * These numbers work together with an existing by* rule. It specifies
+ * exactly which items of the existing by-rule to filter.
+ *
+ * Valid values are 1 to 366 and -1 to -366. As an example, this can be
+ * used to recur the last workday of the month.
+ *
+ * This would be done by setting frequency to 'monthly', byDay to
+ * 'MO,TU,WE,TH,FR' and bySetPos to -1.
+ *
+ * @var array
+ */
+ protected $bySetPos;
+
+ /**
+ * When the week starts.
+ *
+ * @var string
+ */
+ protected $weekStart = 'MO';
+
+ /* Functions that advance the iterator {{{ */
+
+ /**
+ * Gets the original start time of the RRULE.
+ *
+ * The value is formatted as a string with 24-hour:minute:second
+ */
+ protected function startTime(): string
+ {
+ return $this->startDate->format('H:i:s');
+ }
+
+ /**
+ * Advances currentDate by the interval.
+ * The time is set from the original startDate.
+ * If the recurrence is on a day when summer time started, then the
+ * time on that day may have jumped forward, for example, from 0230 to 0330.
+ * Using the original time means that the next recurrence will be calculated
+ * based on the original start time and the day/week/month/year interval.
+ * So the start time of the next occurrence can correctly revert to 0230.
+ */
+ protected function advanceTheDate(string $interval): void
+ {
+ $this->currentDate = $this->currentDate->modify($interval.' '.$this->startTime());
+ }
+
+ /**
+ * Does the processing for adjusting the time of multi-hourly events when summer time starts.
+ */
+ protected function adjustForTimeJumpsOfHourlyEvent(DateTimeInterface $previousEventDateTime): void
+ {
+ if (0 === $this->hourJump) {
+ // Remember if the clock time jumped forward on the next occurrence.
+ // That happens if the next event time is on a day when summer time starts
+ // and the event time is in the non-existent hour of the day.
+ // For example, an event that normally starts at 02:30 will
+ // have to start at 03:30 on that day.
+ // If the interval is just 1 hour, then there is no "jumping back" to do.
+ // The events that day will happen, for example, at 0030 0130 0330 0430 0530...
+ if ($this->interval > 1) {
+ $expectedHourOfNextDate = ((int) $previousEventDateTime->format('G') + $this->interval) % 24;
+ $actualHourOfNextDate = (int) $this->currentDate->format('G');
+ $this->hourJump = $actualHourOfNextDate - $expectedHourOfNextDate;
+ }
+ } else {
+ // The hour "jumped" for the previous occurrence, to avoid the non-existent time.
+ // currentDate got set ahead by (usually) 1 hour on that day.
+ // Adjust it back for this next occurrence.
+ $this->currentDate = $this->currentDate->sub(new \DateInterval('PT'.$this->hourJump.'H'));
+ $this->hourJump = 0;
+ }
+ }
+
+ /**
+ * Does the processing for advancing the iterator for hourly frequency.
+ */
+ protected function nextHourly()
+ {
+ $previousEventDateTime = clone $this->currentDate;
+ $this->currentDate = $this->currentDate->modify('+'.$this->interval.' hours');
+ $this->adjustForTimeJumpsOfHourlyEvent($previousEventDateTime);
+ }
+
+ /**
+ * Does the processing for advancing the iterator for daily frequency.
+ */
+ protected function nextDaily()
+ {
+ if (!$this->byHour && !$this->byDay) {
+ $this->advanceTheDate('+'.$this->interval.' days');
+
+ return;
+ }
+
+ $recurrenceHours = [];
+ if (!empty($this->byHour)) {
+ $recurrenceHours = $this->getHours();
+ }
+
+ $recurrenceDays = [];
+ if (!empty($this->byDay)) {
+ $recurrenceDays = $this->getDays();
+ }
+
+ $recurrenceMonths = [];
+ if (!empty($this->byMonth)) {
+ $recurrenceMonths = $this->getMonths();
+ }
+
+ do {
+ if ($this->byHour) {
+ if ('23' == $this->currentDate->format('G')) {
+ // to obey the interval rule
+ $this->currentDate = $this->currentDate->modify('+'.($this->interval - 1).' days');
+ }
+
+ $this->currentDate = $this->currentDate->modify('+1 hours');
+ } else {
+ $this->currentDate = $this->currentDate->modify('+'.$this->interval.' days');
+ }
+
+ // Current month of the year
+ $currentMonth = $this->currentDate->format('n');
+
+ // Current day of the week
+ $currentDay = $this->currentDate->format('w');
+
+ // Current hour of the day
+ $currentHour = $this->currentDate->format('G');
+
+ if ($this->currentDate->getTimestamp() > self::dateUpperLimit) {
+ $this->currentDate = null;
+
+ return;
+ }
+ } while (
+ ($this->byDay && !in_array($currentDay, $recurrenceDays)) ||
+ ($this->byHour && !in_array($currentHour, $recurrenceHours)) ||
+ ($this->byMonth && !in_array($currentMonth, $recurrenceMonths))
+ );
+ }
+
+ /**
+ * Does the processing for advancing the iterator for weekly frequency.
+ */
+ protected function nextWeekly()
+ {
+ if (!$this->byHour && !$this->byDay) {
+ $this->advanceTheDate('+'.$this->interval.' weeks');
+
+ return;
+ }
+
+ $recurrenceHours = [];
+ if ($this->byHour) {
+ $recurrenceHours = $this->getHours();
+ }
+
+ $recurrenceDays = [];
+ if ($this->byDay) {
+ $recurrenceDays = $this->getDays();
+ }
+
+ // First day of the week:
+ $firstDay = $this->dayMap[$this->weekStart];
+
+ do {
+ if ($this->byHour) {
+ $this->currentDate = $this->currentDate->modify('+1 hours');
+ } else {
+ $this->advanceTheDate('+1 days');
+ }
+
+ // Current day of the week
+ $currentDay = (int) $this->currentDate->format('w');
+
+ // Current hour of the day
+ $currentHour = (int) $this->currentDate->format('G');
+
+ // We need to roll over to the next week
+ if ($currentDay === $firstDay && (!$this->byHour || '0' == $currentHour)) {
+ $this->currentDate = $this->currentDate->modify('+'.($this->interval - 1).' weeks');
+
+ // We need to go to the first day of this week, but only if we
+ // are not already on this first day of this week.
+ if ($this->currentDate->format('w') != $firstDay) {
+ $this->currentDate = $this->currentDate->modify('last '.$this->dayNames[$this->dayMap[$this->weekStart]]);
+ }
+ }
+
+ // We have a match
+ } while (($this->byDay && !in_array($currentDay, $recurrenceDays)) || ($this->byHour && !in_array($currentHour, $recurrenceHours)));
+ }
+
+ /**
+ * Does the processing for advancing the iterator for monthly frequency.
+ */
+ protected function nextMonthly()
+ {
+ $currentDayOfMonth = $this->currentDate->format('j');
+ if (!$this->byMonthDay && !$this->byDay) {
+ // If the current day is higher than the 28th, rollover can
+ // occur to the next month. We Must skip these invalid
+ // entries.
+ if ($currentDayOfMonth < 29) {
+ $this->advanceTheDate('+'.$this->interval.' months');
+ } else {
+ $increase = 0;
+ do {
+ ++$increase;
+ $tempDate = clone $this->currentDate;
+ $tempDate = $tempDate->modify('+ '.($this->interval * $increase).' months '.$this->startTime());
+ } while ($tempDate->format('j') != $currentDayOfMonth);
+ $this->currentDate = $tempDate;
+ }
+
+ return;
+ }
+
+ $occurrence = -1;
+ while (true) {
+ $occurrences = $this->getMonthlyOccurrences();
+
+ foreach ($occurrences as $occurrence) {
+ // The first occurrence thats higher than the current
+ // day of the month wins.
+ if ($occurrence > $currentDayOfMonth) {
+ break 2;
+ }
+ }
+
+ // If we made it all the way here, it means there were no
+ // valid occurrences, and we need to advance to the next
+ // month.
+ //
+ // This line does not currently work in hhvm. Temporary workaround
+ // follows:
+ // $this->currentDate->modify('first day of this month');
+ $this->currentDate = new DateTimeImmutable($this->currentDate->format('Y-m-1 H:i:s'), $this->currentDate->getTimezone());
+ // end of workaround
+ $this->currentDate = $this->currentDate->modify('+ '.$this->interval.' months');
+
+ // This goes to 0 because we need to start counting at the
+ // beginning.
+ $currentDayOfMonth = 0;
+
+ // For some reason the "until" parameter was not being used here,
+ // that's why the workaround of the 10000 year bug was needed at all
+ // let's stop it before the "until" parameter date
+ if ($this->until && $this->currentDate->getTimestamp() >= $this->until->getTimestamp()) {
+ return;
+ }
+
+ // To prevent running this forever (better: until we hit the max date of DateTimeImmutable) we simply
+ // stop at 9999-12-31. Looks like the year 10000 problem is not solved in php ....
+ if ($this->currentDate->getTimestamp() > self::dateUpperLimit) {
+ $this->currentDate = null;
+
+ return;
+ }
+ }
+
+ // Set the currentDate to the year and month that we are in, and the day of the month that we have selected.
+ // That day could be a day when summer time starts, and if the time of the event is, for example, 0230,
+ // then 0230 will not be a valid time on that day. So always apply the start time from the original startDate.
+ // The "modify" method will set the time forward to 0330, for example, if needed.
+ $this->currentDate = $this->currentDate->setDate(
+ (int) $this->currentDate->format('Y'),
+ (int) $this->currentDate->format('n'),
+ (int) $occurrence
+ )->modify($this->startTime());
+ }
+
+ /**
+ * Does the processing for advancing the iterator for yearly frequency.
+ */
+ protected function nextYearly()
+ {
+ $currentMonth = $this->currentDate->format('n');
+ $currentYear = $this->currentDate->format('Y');
+ $currentDayOfMonth = $this->currentDate->format('j');
+
+ // No sub-rules, so we just advance by year
+ if (empty($this->byMonth)) {
+ // Unless it was a leap day!
+ if (2 == $currentMonth && 29 == $currentDayOfMonth) {
+ $counter = 0;
+ do {
+ ++$counter;
+ // Here we increase the year count by the interval, until
+ // we hit a date that's also in a leap year.
+ //
+ // We could just find the next interval that's dividable by
+ // 4, but that would ignore the rule that there's no leap
+ // year every year that's dividable by a 100, but not by
+ // 400. (1800, 1900, 2100). So we just rely on the datetime
+ // functions instead.
+ $nextDate = clone $this->currentDate;
+ $nextDate = $nextDate->modify('+ '.($this->interval * $counter).' years');
+ } while (2 != $nextDate->format('n'));
+
+ $this->currentDate = $nextDate;
+
+ return;
+ }
+
+ if (null !== $this->byWeekNo) { // byWeekNo is an array with values from -53 to -1, or 1 to 53
+ $dayOffsets = [];
+ if ($this->byDay) {
+ foreach ($this->byDay as $byDay) {
+ $dayOffsets[] = $this->dayMap[$byDay];
+ }
+ } else { // default is Monday
+ $dayOffsets[] = 1;
+ }
+
+ $currentYear = $this->currentDate->format('Y');
+
+ while (true) {
+ $checkDates = [];
+
+ // loop through all WeekNo and Days to check all the combinations
+ foreach ($this->byWeekNo as $byWeekNo) {
+ foreach ($dayOffsets as $dayOffset) {
+ $date = clone $this->currentDate;
+ $date = $date->setISODate($currentYear, $byWeekNo, $dayOffset);
+
+ if ($date > $this->currentDate) {
+ $checkDates[] = $date;
+ }
+ }
+ }
+
+ if (count($checkDates) > 0) {
+ $this->currentDate = min($checkDates);
+
+ return;
+ }
+
+ // if there is no date found, check the next year
+ $currentYear += $this->interval;
+ }
+ }
+
+ if (null !== $this->byYearDay) { // byYearDay is an array with values from -366 to -1, or 1 to 366
+ $dayOffsets = [];
+ if ($this->byDay) {
+ foreach ($this->byDay as $byDay) {
+ $dayOffsets[] = $this->dayMap[$byDay];
+ }
+ } else { // default is Monday-Sunday
+ $dayOffsets = [1, 2, 3, 4, 5, 6, 7];
+ }
+
+ $currentYear = $this->currentDate->format('Y');
+
+ while (true) {
+ $checkDates = [];
+
+ // loop through all YearDay and Days to check all the combinations
+ foreach ($this->byYearDay as $byYearDay) {
+ $date = clone $this->currentDate;
+ if ($byYearDay > 0) {
+ $date = $date->setDate($currentYear, 1, 1);
+ $date = $date->add(new \DateInterval('P'.($byYearDay - 1).'D'));
+ } else {
+ $date = $date->setDate($currentYear, 12, 31);
+ $date = $date->sub(new \DateInterval('P'.abs($byYearDay + 1).'D'));
+ }
+
+ if ($date > $this->currentDate && in_array($date->format('N'), $dayOffsets)) {
+ $checkDates[] = $date;
+ }
+ }
+
+ if (count($checkDates) > 0) {
+ $this->currentDate = min($checkDates);
+
+ return;
+ }
+
+ // if there is no date found, check the next year
+ $currentYear += $this->interval;
+ }
+ }
+
+ // The easiest form
+ $this->advanceTheDate('+'.$this->interval.' years');
+
+ return;
+ }
+
+ $currentMonth = $this->currentDate->format('n');
+ $currentYear = $this->currentDate->format('Y');
+ $currentDayOfMonth = $this->currentDate->format('j');
+
+ $advancedToNewMonth = false;
+
+ // If we got a byDay or getMonthDay filter, we must first expand
+ // further.
+ if ($this->byDay || $this->byMonthDay) {
+ $occurrence = -1;
+ while (true) {
+ $occurrences = $this->getMonthlyOccurrences();
+
+ foreach ($occurrences as $occurrence) {
+ // The first occurrence that's higher than the current
+ // day of the month wins.
+ // If we advanced to the next month or year, the first
+ // occurrence is always correct.
+ if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) {
+ // only consider byMonth matches,
+ // otherwise, we don't follow RRule correctly
+ if (in_array($currentMonth, $this->byMonth)) {
+ break 2;
+ }
+ }
+ }
+
+ // If we made it here, it means we need to advance to
+ // the next month or year.
+ $currentDayOfMonth = 1;
+ $advancedToNewMonth = true;
+ do {
+ ++$currentMonth;
+ if ($currentMonth > 12) {
+ $currentYear += $this->interval;
+ $currentMonth = 1;
+ }
+ } while (!in_array($currentMonth, $this->byMonth));
+
+ $this->currentDate = $this->currentDate->setDate(
+ (int) $currentYear,
+ (int) $currentMonth,
+ (int) $currentDayOfMonth
+ );
+
+ // To prevent running this forever (better: until we hit the max date of DateTimeImmutable) we simply
+ // stop at 9999-12-31. Looks like the year 10000 problem is not solved in php ....
+ if ($this->currentDate->getTimestamp() > self::dateUpperLimit) {
+ $this->currentDate = null;
+
+ return;
+ }
+ }
+
+ // If we made it here, it means we got a valid occurrence
+ $this->currentDate = $this->currentDate->setDate(
+ (int) $currentYear,
+ (int) $currentMonth,
+ (int) $occurrence
+ )->modify($this->startTime());
+
+ return;
+ } else {
+ // These are the 'byMonth' rules, if there are no byDay or
+ // byMonthDay sub-rules.
+ do {
+ ++$currentMonth;
+ if ($currentMonth > 12) {
+ $currentYear += $this->interval;
+ $currentMonth = 1;
+ }
+ } while (!in_array($currentMonth, $this->byMonth));
+ $this->currentDate = $this->currentDate->setDate(
+ (int) $currentYear,
+ (int) $currentMonth,
+ (int) $currentDayOfMonth
+ )->modify($this->startTime());
+
+ return;
+ }
+ }
+
+ /* }}} */
+
+ /**
+ * This method receives a string from an RRULE property, and populates this
+ * class with all the values.
+ *
+ * @param string|array $rrule
+ */
+ protected function parseRRule($rrule)
+ {
+ if (is_string($rrule)) {
+ $rrule = Property\ICalendar\Recur::stringToArray($rrule);
+ }
+
+ foreach ($rrule as $key => $value) {
+ $key = strtoupper($key);
+ switch ($key) {
+ case 'FREQ':
+ $value = strtolower($value);
+ if (!in_array(
+ $value,
+ ['secondly', 'minutely', 'hourly', 'daily', 'weekly', 'monthly', 'yearly']
+ )) {
+ throw new InvalidDataException('Unknown value for FREQ='.strtoupper($value));
+ }
+ $this->frequency = $value;
+ break;
+
+ case 'UNTIL':
+ $this->until = DateTimeParser::parse($value, $this->startDate->getTimezone());
+
+ // In some cases events are generated with an UNTIL=
+ // parameter before the actual start of the event.
+ //
+ // Not sure why this is happening. We assume that the
+ // intention was that the event only recurs once.
+ //
+ // So we are modifying the parameter so our code doesn't
+ // break.
+ if ($this->until < $this->startDate) {
+ $this->until = $this->startDate;
+ }
+ break;
+
+ case 'INTERVAL':
+ case 'COUNT':
+ $val = (int) $value;
+ if ($val < 1) {
+ throw new InvalidDataException(strtoupper($key).' in RRULE must be a positive integer!');
+ }
+ $key = strtolower($key);
+ $this->$key = $val;
+ break;
+
+ case 'BYSECOND':
+ $this->bySecond = (array) $value;
+ break;
+
+ case 'BYMINUTE':
+ $this->byMinute = (array) $value;
+ break;
+
+ case 'BYHOUR':
+ $this->byHour = (array) $value;
+ break;
+
+ case 'BYDAY':
+ $value = (array) $value;
+ foreach ($value as $part) {
+ if (!preg_match('#^ (-|\+)? ([1-5])? (MO|TU|WE|TH|FR|SA|SU) $# xi', $part)) {
+ throw new InvalidDataException('Invalid part in BYDAY clause: '.$part);
+ }
+ }
+ $this->byDay = $value;
+ break;
+
+ case 'BYMONTHDAY':
+ $this->byMonthDay = (array) $value;
+ break;
+
+ case 'BYYEARDAY':
+ $this->byYearDay = (array) $value;
+ foreach ($this->byYearDay as $byYearDay) {
+ if (!is_numeric($byYearDay) || (int) $byYearDay < -366 || 0 == (int) $byYearDay || (int) $byYearDay > 366) {
+ throw new InvalidDataException('BYYEARDAY in RRULE must have value(s) from 1 to 366, or -366 to -1!');
+ }
+ }
+ break;
+
+ case 'BYWEEKNO':
+ $this->byWeekNo = (array) $value;
+ foreach ($this->byWeekNo as $byWeekNo) {
+ if (!is_numeric($byWeekNo) || (int) $byWeekNo < -53 || 0 == (int) $byWeekNo || (int) $byWeekNo > 53) {
+ throw new InvalidDataException('BYWEEKNO in RRULE must have value(s) from 1 to 53, or -53 to -1!');
+ }
+ }
+ break;
+
+ case 'BYMONTH':
+ $this->byMonth = (array) $value;
+ foreach ($this->byMonth as $byMonth) {
+ if (!is_numeric($byMonth) || (int) $byMonth < 1 || (int) $byMonth > 12) {
+ throw new InvalidDataException('BYMONTH in RRULE must have value(s) between 1 and 12!');
+ }
+ }
+ break;
+
+ case 'BYSETPOS':
+ $this->bySetPos = (array) $value;
+ break;
+
+ case 'WKST':
+ $this->weekStart = strtoupper($value);
+ break;
+
+ default:
+ throw new InvalidDataException('Not supported: '.strtoupper($key));
+ }
+ }
+ }
+
+ /**
+ * Mappings between the day number and english day name.
+ *
+ * @var array
+ */
+ protected $dayNames = [
+ 0 => 'Sunday',
+ 1 => 'Monday',
+ 2 => 'Tuesday',
+ 3 => 'Wednesday',
+ 4 => 'Thursday',
+ 5 => 'Friday',
+ 6 => 'Saturday',
+ ];
+
+ /**
+ * Returns all the occurrences for a monthly frequency with a 'byDay' or
+ * 'byMonthDay' expansion for the current month.
+ *
+ * The returned list is an array of integers with the day of month (1-31).
+ *
+ * @return array
+ */
+ protected function getMonthlyOccurrences()
+ {
+ $startDate = clone $this->currentDate;
+
+ $byDayResults = [];
+
+ // Our strategy is to simply go through the byDays, advance the date to
+ // that point and add it to the results.
+ if ($this->byDay) {
+ foreach ($this->byDay as $day) {
+ $dayName = $this->dayNames[$this->dayMap[substr($day, -2)]];
+
+ // Dayname will be something like 'wednesday'. Now we need to find
+ // all wednesdays in this month.
+ $dayHits = [];
+
+ // workaround for missing 'first day of the month' support in hhvm
+ $checkDate = new \DateTime($startDate->format('Y-m-1'));
+ // workaround modify always advancing the date even if the current day is a $dayName in hhvm
+ if ($checkDate->format('l') !== $dayName) {
+ $checkDate = $checkDate->modify($dayName);
+ }
+
+ do {
+ $dayHits[] = $checkDate->format('j');
+ $checkDate = $checkDate->modify('next '.$dayName);
+ } while ($checkDate->format('n') === $startDate->format('n'));
+
+ // So now we have 'all wednesdays' for month. It is however
+ // possible that the user only really wanted the 1st, 2nd or last
+ // wednesday.
+ if (strlen($day) > 2) {
+ $offset = (int) substr($day, 0, -2);
+
+ if ($offset > 0) {
+ // It is possible that the day does not exist, such as a
+ // 5th or 6th wednesday of the month.
+ if (isset($dayHits[$offset - 1])) {
+ $byDayResults[] = $dayHits[$offset - 1];
+ }
+ } else {
+ // if it was negative we count from the end of the array
+ // might not exist, fx. -5th tuesday
+ if (isset($dayHits[count($dayHits) + $offset])) {
+ $byDayResults[] = $dayHits[count($dayHits) + $offset];
+ }
+ }
+ } else {
+ // There was no counter (first, second, last wednesdays), so we
+ // just need to add the all to the list).
+ $byDayResults = array_merge($byDayResults, $dayHits);
+ }
+ }
+ }
+
+ $byMonthDayResults = [];
+ if ($this->byMonthDay) {
+ foreach ($this->byMonthDay as $monthDay) {
+ // Removing values that are out of range for this month
+ if ($monthDay > $startDate->format('t') ||
+ $monthDay < 0 - $startDate->format('t')) {
+ continue;
+ }
+ if ($monthDay > 0) {
+ $byMonthDayResults[] = $monthDay;
+ } else {
+ // Negative values
+ $byMonthDayResults[] = $startDate->format('t') + 1 + $monthDay;
+ }
+ }
+ }
+
+ // If there was just byDay or just byMonthDay, they just specify our
+ // (almost) final list. If both were provided, then byDay limits the
+ // list.
+ if ($this->byMonthDay && $this->byDay) {
+ $result = array_intersect($byMonthDayResults, $byDayResults);
+ } elseif ($this->byMonthDay) {
+ $result = $byMonthDayResults;
+ } else {
+ $result = $byDayResults;
+ }
+ $result = array_unique($result);
+ sort($result, SORT_NUMERIC);
+
+ // The last thing that needs checking is the BYSETPOS. If it's set, it
+ // means only certain items in the set survive the filter.
+ if (!$this->bySetPos) {
+ return $result;
+ }
+
+ $filteredResult = [];
+ foreach ($this->bySetPos as $setPos) {
+ if ($setPos < 0) {
+ $setPos = count($result) + ($setPos + 1);
+ }
+ if (isset($result[$setPos - 1])) {
+ $filteredResult[] = $result[$setPos - 1];
+ }
+ }
+
+ sort($filteredResult, SORT_NUMERIC);
+
+ return $filteredResult;
+ }
+
+ /**
+ * Simple mapping from iCalendar day names to day numbers.
+ *
+ * @var array
+ */
+ protected $dayMap = [
+ 'SU' => 0,
+ 'MO' => 1,
+ 'TU' => 2,
+ 'WE' => 3,
+ 'TH' => 4,
+ 'FR' => 5,
+ 'SA' => 6,
+ ];
+
+ protected function getHours()
+ {
+ $recurrenceHours = [];
+ foreach ($this->byHour as $byHour) {
+ $recurrenceHours[] = $byHour;
+ }
+
+ return $recurrenceHours;
+ }
+
+ protected function getDays()
+ {
+ $recurrenceDays = [];
+ foreach ($this->byDay as $byDay) {
+ // The day may be preceded with a positive (+n) or
+ // negative (-n) integer. However, this does not make
+ // sense in 'weekly' so we ignore it here.
+ $recurrenceDays[] = $this->dayMap[substr($byDay, -2)];
+ }
+
+ return $recurrenceDays;
+ }
+
+ protected function getMonths()
+ {
+ $recurrenceMonths = [];
+ foreach ($this->byMonth as $byMonth) {
+ $recurrenceMonths[] = $byMonth;
+ }
+
+ return $recurrenceMonths;
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Settings.php b/vendor/sabre/vobject/lib/Settings.php
new file mode 100644
index 0000000..b0bb80a
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Settings.php
@@ -0,0 +1,55 @@
+children() as $component) {
+ if (!$component instanceof VObject\Component) {
+ continue;
+ }
+
+ // Get all timezones
+ if ('VTIMEZONE' === $component->name) {
+ $this->vtimezones[(string) $component->TZID] = $component;
+ continue;
+ }
+
+ // Get component UID for recurring Events search
+ if (!$component->UID) {
+ $component->UID = sha1(microtime()).'-vobjectimport';
+ }
+ $uid = (string) $component->UID;
+
+ // Take care of recurring events
+ if (!array_key_exists($uid, $this->objects)) {
+ $this->objects[$uid] = new VCalendar();
+ }
+
+ $this->objects[$uid]->add(clone $component);
+ }
+ }
+
+ /**
+ * Every time getNext() is called, a new object will be parsed, until we
+ * hit the end of the stream.
+ *
+ * When the end is reached, null will be returned.
+ *
+ * @return \Sabre\VObject\Component|null
+ */
+ public function getNext()
+ {
+ if ($object = array_shift($this->objects)) {
+ // create our baseobject
+ $object->version = '2.0';
+ $object->prodid = '-//Sabre//Sabre VObject '.VObject\Version::VERSION.'//EN';
+ $object->calscale = 'GREGORIAN';
+
+ // add vtimezone information to obj (if we have it)
+ foreach ($this->vtimezones as $vtimezone) {
+ $object->add($vtimezone);
+ }
+
+ return $object;
+ } else {
+ return;
+ }
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Splitter/SplitterInterface.php b/vendor/sabre/vobject/lib/Splitter/SplitterInterface.php
new file mode 100644
index 0000000..c845ac5
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Splitter/SplitterInterface.php
@@ -0,0 +1,38 @@
+input = $input;
+ $this->parser = new MimeDir($input, $options);
+ }
+
+ /**
+ * Every time getNext() is called, a new object will be parsed, until we
+ * hit the end of the stream.
+ *
+ * When the end is reached, null will be returned.
+ *
+ * @return \Sabre\VObject\Component|null
+ */
+ public function getNext()
+ {
+ try {
+ $object = $this->parser->parse();
+
+ if (!$object instanceof VObject\Component\VCard) {
+ throw new VObject\ParseException('The supplied input contained non-VCARD data.');
+ }
+ } catch (VObject\EofException $e) {
+ return;
+ }
+
+ return $object;
+ }
+}
diff --git a/vendor/sabre/vobject/lib/StringUtil.php b/vendor/sabre/vobject/lib/StringUtil.php
new file mode 100644
index 0000000..b04539e
--- /dev/null
+++ b/vendor/sabre/vobject/lib/StringUtil.php
@@ -0,0 +1,50 @@
+addGuesser('lic', new GuessFromLicEntry());
+ $this->addGuesser('msTzId', new GuessFromMsTzId());
+ $this->addFinder('tzid', new FindFromTimezoneIdentifier());
+ $this->addFinder('tzmap', new FindFromTimezoneMap());
+ $this->addFinder('offset', new FindFromOffset());
+ }
+
+ private static function getInstance(): self
+ {
+ if (null === self::$instance) {
+ self::$instance = new self();
+ }
+
+ return self::$instance;
+ }
+
+ private function addGuesser(string $key, TimezoneGuesser $guesser): void
+ {
+ $this->timezoneGuessers[$key] = $guesser;
+ }
+
+ private function addFinder(string $key, TimezoneFinder $finder): void
+ {
+ $this->timezoneFinders[$key] = $finder;
+ }
+
+ /**
+ * This method will try to find out the correct timezone for an iCalendar
+ * date-time value.
+ *
+ * You must pass the contents of the TZID parameter, as well as the full
+ * calendar.
+ *
+ * If the lookup fails, this method will return the default PHP timezone
+ * (as configured using date_default_timezone_set, or the date.timezone ini
+ * setting).
+ *
+ * Alternatively, if $failIfUncertain is set to true, it will throw an
+ * exception if we cannot accurately determine the timezone.
+ */
+ private function findTimeZone(string $tzid, ?Component $vcalendar = null, bool $failIfUncertain = false): DateTimeZone
+ {
+ foreach ($this->timezoneFinders as $timezoneFinder) {
+ $timezone = $timezoneFinder->find($tzid, $failIfUncertain);
+ if (!$timezone instanceof DateTimeZone) {
+ continue;
+ }
+
+ return $timezone;
+ }
+
+ if ($vcalendar) {
+ // If that didn't work, we will scan VTIMEZONE objects
+ foreach ($vcalendar->select('VTIMEZONE') as $vtimezone) {
+ if ((string) $vtimezone->TZID === $tzid) {
+ foreach ($this->timezoneGuessers as $timezoneGuesser) {
+ $timezone = $timezoneGuesser->guess($vtimezone, $failIfUncertain);
+ if (!$timezone instanceof DateTimeZone) {
+ continue;
+ }
+
+ return $timezone;
+ }
+ }
+ }
+ }
+
+ if ($failIfUncertain) {
+ throw new InvalidArgumentException('We were unable to determine the correct PHP timezone for tzid: '.$tzid);
+ }
+
+ // If we got all the way here, we default to whatever has been set as the PHP default timezone.
+ return new DateTimeZone(date_default_timezone_get());
+ }
+
+ public static function addTimezoneGuesser(string $key, TimezoneGuesser $guesser): void
+ {
+ self::getInstance()->addGuesser($key, $guesser);
+ }
+
+ public static function addTimezoneFinder(string $key, TimezoneFinder $finder): void
+ {
+ self::getInstance()->addFinder($key, $finder);
+ }
+
+ /**
+ * @param string $tzid
+ * @param false $failIfUncertain
+ *
+ * @return DateTimeZone
+ */
+ public static function getTimeZone($tzid, ?Component $vcalendar = null, $failIfUncertain = false)
+ {
+ return self::getInstance()->findTimeZone($tzid, $vcalendar, $failIfUncertain);
+ }
+
+ public static function clean(): void
+ {
+ self::$instance = null;
+ }
+
+ // Keeping things for backwards compatibility
+ /**
+ * @var array|null
+ *
+ * @deprecated
+ */
+ public static $map = null;
+
+ /**
+ * List of microsoft exchange timezone ids.
+ *
+ * Source: http://msdn.microsoft.com/en-us/library/aa563018(loband).aspx
+ *
+ * @deprecated
+ */
+ public static $microsoftExchangeMap = [
+ 0 => 'UTC',
+ 31 => 'Africa/Casablanca',
+ // Insanely, id #2 is used for both Europe/Lisbon, and Europe/Sarajevo.
+ // I'm not even kidding.. We handle this special case in the
+ // getTimeZone method.
+ 2 => 'Europe/Lisbon',
+ 1 => 'Europe/London',
+ 4 => 'Europe/Berlin',
+ 6 => 'Europe/Prague',
+ 3 => 'Europe/Paris',
+ 69 => 'Africa/Luanda', // This was a best guess
+ 7 => 'Europe/Athens',
+ 5 => 'Europe/Bucharest',
+ 49 => 'Africa/Cairo',
+ 50 => 'Africa/Harare',
+ 59 => 'Europe/Helsinki',
+ 27 => 'Asia/Jerusalem',
+ 26 => 'Asia/Baghdad',
+ 74 => 'Asia/Kuwait',
+ 51 => 'Europe/Moscow',
+ 56 => 'Africa/Nairobi',
+ 25 => 'Asia/Tehran',
+ 24 => 'Asia/Muscat', // Best guess
+ 54 => 'Asia/Baku',
+ 48 => 'Asia/Kabul',
+ 58 => 'Asia/Yekaterinburg',
+ 47 => 'Asia/Karachi',
+ 23 => 'Asia/Calcutta',
+ 62 => 'Asia/Kathmandu',
+ 46 => 'Asia/Almaty',
+ 71 => 'Asia/Dhaka',
+ 66 => 'Asia/Colombo',
+ 61 => 'Asia/Rangoon',
+ 22 => 'Asia/Bangkok',
+ 64 => 'Asia/Krasnoyarsk',
+ 45 => 'Asia/Shanghai',
+ 63 => 'Asia/Irkutsk',
+ 21 => 'Asia/Singapore',
+ 73 => 'Australia/Perth',
+ 75 => 'Asia/Taipei',
+ 20 => 'Asia/Tokyo',
+ 72 => 'Asia/Seoul',
+ 70 => 'Asia/Yakutsk',
+ 19 => 'Australia/Adelaide',
+ 44 => 'Australia/Darwin',
+ 18 => 'Australia/Brisbane',
+ 76 => 'Australia/Sydney',
+ 43 => 'Pacific/Guam',
+ 42 => 'Australia/Hobart',
+ 68 => 'Asia/Vladivostok',
+ 41 => 'Asia/Magadan',
+ 17 => 'Pacific/Auckland',
+ 40 => 'Pacific/Fiji',
+ 67 => 'Pacific/Tongatapu',
+ 29 => 'Atlantic/Azores',
+ 53 => 'Atlantic/Cape_Verde',
+ 30 => 'America/Noronha',
+ 8 => 'America/Sao_Paulo', // Best guess
+ 32 => 'America/Argentina/Buenos_Aires',
+ 60 => 'America/Godthab',
+ 28 => 'America/St_Johns',
+ 9 => 'America/Halifax',
+ 33 => 'America/Caracas',
+ 65 => 'America/Santiago',
+ 35 => 'America/Bogota',
+ 10 => 'America/New_York',
+ 34 => 'America/Indiana/Indianapolis',
+ 55 => 'America/Guatemala',
+ 11 => 'America/Chicago',
+ 37 => 'America/Mexico_City',
+ 36 => 'America/Edmonton',
+ 38 => 'America/Phoenix',
+ 12 => 'America/Denver', // Best guess
+ 13 => 'America/Los_Angeles', // Best guess
+ 14 => 'America/Anchorage',
+ 15 => 'Pacific/Honolulu',
+ 16 => 'Pacific/Midway',
+ 39 => 'Pacific/Kwajalein',
+ ];
+
+ /**
+ * This method will load in all the tz mapping information, if it's not yet
+ * done.
+ *
+ * @deprecated
+ */
+ public static function loadTzMaps()
+ {
+ if (!is_null(self::$map)) {
+ return;
+ }
+
+ self::$map = array_merge(
+ include __DIR__.'/timezonedata/windowszones.php',
+ include __DIR__.'/timezonedata/lotuszones.php',
+ include __DIR__.'/timezonedata/exchangezones.php',
+ include __DIR__.'/timezonedata/php-workaround.php'
+ );
+ }
+
+ /**
+ * This method returns an array of timezone identifiers, that are supported
+ * by DateTimeZone(), but not returned by DateTimeZone::listIdentifiers().
+ *
+ * We're not using DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC) because:
+ * - It's not supported by some PHP versions as well as HHVM.
+ * - It also returns identifiers, that are invalid values for new DateTimeZone() on some PHP versions.
+ * (See timezonedata/php-bc.php and timezonedata php-workaround.php)
+ *
+ * @return array
+ *
+ * @deprecated
+ */
+ public static function getIdentifiersBC()
+ {
+ return include __DIR__.'/timezonedata/php-bc.php';
+ }
+}
diff --git a/vendor/sabre/vobject/lib/TimezoneGuesser/FindFromOffset.php b/vendor/sabre/vobject/lib/TimezoneGuesser/FindFromOffset.php
new file mode 100644
index 0000000..990ac96
--- /dev/null
+++ b/vendor/sabre/vobject/lib/TimezoneGuesser/FindFromOffset.php
@@ -0,0 +1,31 @@
+getIdentifiersBC()))
+ ) {
+ return new DateTimeZone($tzid);
+ }
+ } catch (Exception $e) {
+ }
+
+ return null;
+ }
+
+ /**
+ * This method returns an array of timezone identifiers, that are supported
+ * by DateTimeZone(), but not returned by DateTimeZone::listIdentifiers().
+ *
+ * We're not using DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC) because:
+ * - It's not supported by some PHP versions as well as HHVM.
+ * - It also returns identifiers, that are invalid values for new DateTimeZone() on some PHP versions.
+ * (See timezonedata/php-bc.php and timezonedata php-workaround.php)
+ *
+ * @return array
+ */
+ private function getIdentifiersBC()
+ {
+ return include __DIR__.'/../timezonedata/php-bc.php';
+ }
+}
diff --git a/vendor/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneMap.php b/vendor/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneMap.php
new file mode 100644
index 0000000..b52ba6a
--- /dev/null
+++ b/vendor/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneMap.php
@@ -0,0 +1,78 @@
+hasTzInMap($tzid)) {
+ return new DateTimeZone($this->getTzFromMap($tzid));
+ }
+
+ // Some Microsoft products prefix the offset first, so let's strip that off
+ // and see if it is our tzid map. We don't want to check for this first just
+ // in case there are overrides in our tzid map.
+ foreach ($this->patterns as $pattern) {
+ if (!preg_match($pattern, $tzid, $matches)) {
+ continue;
+ }
+ $tzidAlternate = $matches[3];
+ if ($this->hasTzInMap($tzidAlternate)) {
+ return new DateTimeZone($this->getTzFromMap($tzidAlternate));
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * This method returns an array of timezone identifiers, that are supported
+ * by DateTimeZone(), but not returned by DateTimeZone::listIdentifiers().
+ *
+ * We're not using DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC) because:
+ * - It's not supported by some PHP versions as well as HHVM.
+ * - It also returns identifiers, that are invalid values for new DateTimeZone() on some PHP versions.
+ * (See timezonedata/php-bc.php and timezonedata php-workaround.php)
+ *
+ * @return array
+ */
+ private function getTzMaps()
+ {
+ if ([] === $this->map) {
+ $this->map = array_merge(
+ include __DIR__.'/../timezonedata/windowszones.php',
+ include __DIR__.'/../timezonedata/lotuszones.php',
+ include __DIR__.'/../timezonedata/exchangezones.php',
+ include __DIR__.'/../timezonedata/php-workaround.php'
+ );
+ }
+
+ return $this->map;
+ }
+
+ private function getTzFromMap(string $tzid): string
+ {
+ return $this->getTzMaps()[$tzid];
+ }
+
+ private function hasTzInMap(string $tzid): bool
+ {
+ return isset($this->getTzMaps()[$tzid]);
+ }
+}
diff --git a/vendor/sabre/vobject/lib/TimezoneGuesser/GuessFromLicEntry.php b/vendor/sabre/vobject/lib/TimezoneGuesser/GuessFromLicEntry.php
new file mode 100644
index 0000000..f340a39
--- /dev/null
+++ b/vendor/sabre/vobject/lib/TimezoneGuesser/GuessFromLicEntry.php
@@ -0,0 +1,33 @@
+{'X-LIC-LOCATION'})) {
+ return null;
+ }
+
+ $lic = (string) $vtimezone->{'X-LIC-LOCATION'};
+
+ // Libical generators may specify strings like
+ // "SystemV/EST5EDT". For those we must remove the
+ // SystemV part.
+ if ('SystemV/' === substr($lic, 0, 8)) {
+ $lic = substr($lic, 8);
+ }
+
+ return TimeZoneUtil::getTimeZone($lic, null, $failIfUncertain);
+ }
+}
diff --git a/vendor/sabre/vobject/lib/TimezoneGuesser/GuessFromMsTzId.php b/vendor/sabre/vobject/lib/TimezoneGuesser/GuessFromMsTzId.php
new file mode 100644
index 0000000..b11ce18
--- /dev/null
+++ b/vendor/sabre/vobject/lib/TimezoneGuesser/GuessFromMsTzId.php
@@ -0,0 +1,119 @@
+ 'UTC',
+ 31 => 'Africa/Casablanca',
+
+ // Insanely, id #2 is used for both Europe/Lisbon, and Europe/Sarajevo.
+ // I'm not even kidding.. We handle this special case in the
+ // getTimeZone method.
+ 2 => 'Europe/Lisbon',
+ 1 => 'Europe/London',
+ 4 => 'Europe/Berlin',
+ 6 => 'Europe/Prague',
+ 3 => 'Europe/Paris',
+ 69 => 'Africa/Luanda', // This was a best guess
+ 7 => 'Europe/Athens',
+ 5 => 'Europe/Bucharest',
+ 49 => 'Africa/Cairo',
+ 50 => 'Africa/Harare',
+ 59 => 'Europe/Helsinki',
+ 27 => 'Asia/Jerusalem',
+ 26 => 'Asia/Baghdad',
+ 74 => 'Asia/Kuwait',
+ 51 => 'Europe/Moscow',
+ 56 => 'Africa/Nairobi',
+ 25 => 'Asia/Tehran',
+ 24 => 'Asia/Muscat', // Best guess
+ 54 => 'Asia/Baku',
+ 48 => 'Asia/Kabul',
+ 58 => 'Asia/Yekaterinburg',
+ 47 => 'Asia/Karachi',
+ 23 => 'Asia/Calcutta',
+ 62 => 'Asia/Kathmandu',
+ 46 => 'Asia/Almaty',
+ 71 => 'Asia/Dhaka',
+ 66 => 'Asia/Colombo',
+ 61 => 'Asia/Rangoon',
+ 22 => 'Asia/Bangkok',
+ 64 => 'Asia/Krasnoyarsk',
+ 45 => 'Asia/Shanghai',
+ 63 => 'Asia/Irkutsk',
+ 21 => 'Asia/Singapore',
+ 73 => 'Australia/Perth',
+ 75 => 'Asia/Taipei',
+ 20 => 'Asia/Tokyo',
+ 72 => 'Asia/Seoul',
+ 70 => 'Asia/Yakutsk',
+ 19 => 'Australia/Adelaide',
+ 44 => 'Australia/Darwin',
+ 18 => 'Australia/Brisbane',
+ 76 => 'Australia/Sydney',
+ 43 => 'Pacific/Guam',
+ 42 => 'Australia/Hobart',
+ 68 => 'Asia/Vladivostok',
+ 41 => 'Asia/Magadan',
+ 17 => 'Pacific/Auckland',
+ 40 => 'Pacific/Fiji',
+ 67 => 'Pacific/Tongatapu',
+ 29 => 'Atlantic/Azores',
+ 53 => 'Atlantic/Cape_Verde',
+ 30 => 'America/Noronha',
+ 8 => 'America/Sao_Paulo', // Best guess
+ 32 => 'America/Argentina/Buenos_Aires',
+ 60 => 'America/Godthab',
+ 28 => 'America/St_Johns',
+ 9 => 'America/Halifax',
+ 33 => 'America/Caracas',
+ 65 => 'America/Santiago',
+ 35 => 'America/Bogota',
+ 10 => 'America/New_York',
+ 34 => 'America/Indiana/Indianapolis',
+ 55 => 'America/Guatemala',
+ 11 => 'America/Chicago',
+ 37 => 'America/Mexico_City',
+ 36 => 'America/Edmonton',
+ 38 => 'America/Phoenix',
+ 12 => 'America/Denver', // Best guess
+ 13 => 'America/Los_Angeles', // Best guess
+ 14 => 'America/Anchorage',
+ 15 => 'Pacific/Honolulu',
+ 16 => 'Pacific/Midway',
+ 39 => 'Pacific/Kwajalein',
+ ];
+
+ public function guess(VTimeZone $vtimezone, bool $throwIfUnsure = false): ?DateTimeZone
+ {
+ // Microsoft may add a magic number, which we also have an
+ // answer for.
+ if (!isset($vtimezone->{'X-MICROSOFT-CDO-TZID'})) {
+ return null;
+ }
+ $cdoId = (int) $vtimezone->{'X-MICROSOFT-CDO-TZID'}->getValue();
+
+ // 2 can mean both Europe/Lisbon and Europe/Sarajevo.
+ if (2 === $cdoId && false !== strpos((string) $vtimezone->TZID, 'Sarajevo')) {
+ return new DateTimeZone('Europe/Sarajevo');
+ }
+
+ if (isset(self::$microsoftExchangeMap[$cdoId])) {
+ return new DateTimeZone(self::$microsoftExchangeMap[$cdoId]);
+ }
+
+ return null;
+ }
+}
diff --git a/vendor/sabre/vobject/lib/TimezoneGuesser/TimezoneFinder.php b/vendor/sabre/vobject/lib/TimezoneGuesser/TimezoneFinder.php
new file mode 100644
index 0000000..5aa880a
--- /dev/null
+++ b/vendor/sabre/vobject/lib/TimezoneGuesser/TimezoneFinder.php
@@ -0,0 +1,10 @@
+getDocumentType();
+ if ($inputVersion === $targetVersion) {
+ return clone $input;
+ }
+
+ if (!in_array($inputVersion, [Document::VCARD21, Document::VCARD30, Document::VCARD40])) {
+ throw new \InvalidArgumentException('Only vCard 2.1, 3.0 and 4.0 are supported for the input data');
+ }
+ if (!in_array($targetVersion, [Document::VCARD30, Document::VCARD40])) {
+ throw new \InvalidArgumentException('You can only use vCard 3.0 or 4.0 for the target version');
+ }
+
+ $newVersion = Document::VCARD40 === $targetVersion ? '4.0' : '3.0';
+
+ $output = new Component\VCard([
+ 'VERSION' => $newVersion,
+ ]);
+
+ // We might have generated a default UID. Remove it!
+ unset($output->UID);
+
+ foreach ($input->children() as $property) {
+ $this->convertProperty($input, $output, $property, $targetVersion);
+ }
+
+ return $output;
+ }
+
+ /**
+ * Handles conversion of a single property.
+ *
+ * @param int $targetVersion
+ */
+ protected function convertProperty(Component\VCard $input, Component\VCard $output, Property $property, $targetVersion)
+ {
+ // Skipping these, those are automatically added.
+ if (in_array($property->name, ['VERSION', 'PRODID'])) {
+ return;
+ }
+
+ $parameters = $property->parameters();
+ $valueType = null;
+ if (isset($parameters['VALUE'])) {
+ $valueType = $parameters['VALUE']->getValue();
+ unset($parameters['VALUE']);
+ }
+ if (!$valueType) {
+ $valueType = $property->getValueType();
+ }
+ if (Document::VCARD30 !== $targetVersion && 'PHONE-NUMBER' === $valueType) {
+ $valueType = null;
+ }
+ $newProperty = $output->createProperty(
+ $property->name,
+ $property->getParts(),
+ [], // parameters will get added a bit later.
+ $valueType
+ );
+
+ if (Document::VCARD30 === $targetVersion) {
+ if ($property instanceof Property\Uri && in_array($property->name, ['PHOTO', 'LOGO', 'SOUND'])) {
+ $newProperty = $this->convertUriToBinary($output, $newProperty);
+ } elseif ($property instanceof Property\VCard\DateAndOrTime) {
+ // In vCard 4, the birth year may be optional. This is not the
+ // case for vCard 3. Apple has a workaround for this that
+ // allows applications that support Apple's extension still
+ // omit birthyears in vCard 3, but applications that do not
+ // support this, will just use a random birthyear. We're
+ // choosing 1604 for the birthyear, because that's what apple
+ // uses.
+ $parts = DateTimeParser::parseVCardDateTime($property->getValue());
+ if (is_null($parts['year'])) {
+ $newValue = '1604-'.$parts['month'].'-'.$parts['date'];
+ $newProperty->setValue($newValue);
+ $newProperty['X-APPLE-OMIT-YEAR'] = '1604';
+ }
+
+ if ('ANNIVERSARY' == $newProperty->name) {
+ // Microsoft non-standard anniversary
+ $newProperty->name = 'X-ANNIVERSARY';
+
+ // We also need to add a new apple property for the same
+ // purpose. This apple property needs a 'label' in the same
+ // group, so we first need to find a groupname that doesn't
+ // exist yet.
+ $x = 1;
+ while ($output->select('ITEM'.$x.'.')) {
+ ++$x;
+ }
+ $output->add('ITEM'.$x.'.X-ABDATE', $newProperty->getValue(), ['VALUE' => 'DATE-AND-OR-TIME']);
+ $output->add('ITEM'.$x.'.X-ABLABEL', '_$!!$_');
+ }
+ } elseif ('KIND' === $property->name) {
+ switch (strtolower($property->getValue())) {
+ case 'org':
+ // vCard 3.0 does not have an equivalent to KIND:ORG,
+ // but apple has an extension that means the same
+ // thing.
+ $newProperty = $output->createProperty('X-ABSHOWAS', 'COMPANY');
+ break;
+
+ case 'individual':
+ // Individual is implicit, so we skip it.
+ return;
+
+ case 'group':
+ // OS X addressbook property
+ $newProperty = $output->createProperty('X-ADDRESSBOOKSERVER-KIND', 'GROUP');
+ break;
+ }
+ } elseif ('MEMBER' === $property->name) {
+ $newProperty = $output->createProperty('X-ADDRESSBOOKSERVER-MEMBER', $property->getValue());
+ }
+ } elseif (Document::VCARD40 === $targetVersion) {
+ // These properties were removed in vCard 4.0
+ if (in_array($property->name, ['NAME', 'MAILER', 'LABEL', 'CLASS'])) {
+ return;
+ }
+
+ if ($property instanceof Property\Binary) {
+ $newProperty = $this->convertBinaryToUri($output, $newProperty, $parameters);
+ } elseif ($property instanceof Property\VCard\DateAndOrTime && isset($parameters['X-APPLE-OMIT-YEAR'])) {
+ // If a property such as BDAY contained 'X-APPLE-OMIT-YEAR',
+ // then we're stripping the year from the vcard 4 value.
+ $parts = DateTimeParser::parseVCardDateTime($property->getValue());
+ if ($parts['year'] === $property['X-APPLE-OMIT-YEAR']->getValue()) {
+ $newValue = '--'.$parts['month'].'-'.$parts['date'];
+ $newProperty->setValue($newValue);
+ }
+
+ // Regardless if the year matched or not, we do need to strip
+ // X-APPLE-OMIT-YEAR.
+ unset($parameters['X-APPLE-OMIT-YEAR']);
+ }
+ switch ($property->name) {
+ case 'X-ABSHOWAS':
+ if ('COMPANY' === strtoupper($property->getValue())) {
+ $newProperty = $output->createProperty('KIND', 'ORG');
+ }
+ break;
+ case 'X-ADDRESSBOOKSERVER-KIND':
+ if ('GROUP' === strtoupper($property->getValue())) {
+ $newProperty = $output->createProperty('KIND', 'GROUP');
+ }
+ break;
+ case 'X-ADDRESSBOOKSERVER-MEMBER':
+ $newProperty = $output->createProperty('MEMBER', $property->getValue());
+ break;
+ case 'X-ANNIVERSARY':
+ $newProperty->name = 'ANNIVERSARY';
+ // If we already have an anniversary property with the same
+ // value, ignore.
+ foreach ($output->select('ANNIVERSARY') as $anniversary) {
+ if ($anniversary->getValue() === $newProperty->getValue()) {
+ return;
+ }
+ }
+ break;
+ case 'X-ABDATE':
+ // Find out what the label was, if it exists.
+ if (!$property->group) {
+ break;
+ }
+ $label = $input->{$property->group.'.X-ABLABEL'};
+
+ // We only support converting anniversaries.
+ if (!$label || '_$!!$_' !== $label->getValue()) {
+ break;
+ }
+
+ // If we already have an anniversary property with the same
+ // value, ignore.
+ foreach ($output->select('ANNIVERSARY') as $anniversary) {
+ if ($anniversary->getValue() === $newProperty->getValue()) {
+ return;
+ }
+ }
+ $newProperty->name = 'ANNIVERSARY';
+ break;
+ // Apple's per-property label system.
+ case 'X-ABLABEL':
+ if ('_$!!$_' === $newProperty->getValue()) {
+ // We can safely remove these, as they are converted to
+ // ANNIVERSARY properties.
+ return;
+ }
+ break;
+ }
+ }
+
+ // set property group
+ $newProperty->group = $property->group;
+
+ if (Document::VCARD40 === $targetVersion) {
+ $this->convertParameters40($newProperty, $parameters);
+ } else {
+ $this->convertParameters30($newProperty, $parameters);
+ }
+
+ // Lastly, we need to see if there's a need for a VALUE parameter.
+ //
+ // We can do that by instantiating a empty property with that name, and
+ // seeing if the default valueType is identical to the current one.
+ $tempProperty = $output->createProperty($newProperty->name);
+ if ($tempProperty->getValueType() !== $newProperty->getValueType()) {
+ $newProperty['VALUE'] = $newProperty->getValueType();
+ }
+
+ $output->add($newProperty);
+ }
+
+ /**
+ * Converts a BINARY property to a URI property.
+ *
+ * vCard 4.0 no longer supports BINARY properties.
+ *
+ * @param Property\Uri $property the input property
+ * @param $parameters list of parameters that will eventually be added to
+ * the new property
+ *
+ * @return Property\Uri
+ */
+ protected function convertBinaryToUri(Component\VCard $output, Property\Binary $newProperty, array &$parameters)
+ {
+ $value = $newProperty->getValue();
+ $newProperty = $output->createProperty(
+ $newProperty->name,
+ null, // no value
+ [], // no parameters yet
+ 'URI' // Forcing the BINARY type
+ );
+
+ $mimeType = 'application/octet-stream';
+
+ // See if we can find a better mimetype.
+ if (isset($parameters['TYPE'])) {
+ $newTypes = [];
+ foreach ($parameters['TYPE']->getParts() as $typePart) {
+ if (in_array(
+ strtoupper($typePart),
+ ['JPEG', 'PNG', 'GIF']
+ )) {
+ $mimeType = 'image/'.strtolower($typePart);
+ } else {
+ $newTypes[] = $typePart;
+ }
+ }
+
+ // If there were any parameters we're not converting to a
+ // mime-type, we need to keep them.
+ if ($newTypes) {
+ $parameters['TYPE']->setParts($newTypes);
+ } else {
+ unset($parameters['TYPE']);
+ }
+ }
+
+ $newProperty->setValue('data:'.$mimeType.';base64,'.base64_encode($value));
+
+ return $newProperty;
+ }
+
+ /**
+ * Converts a URI property to a BINARY property.
+ *
+ * In vCard 4.0 attachments are encoded as data: uri. Even though these may
+ * be valid in vCard 3.0 as well, we should convert those to BINARY if
+ * possible, to improve compatibility.
+ *
+ * @param Property\Uri $property the input property
+ *
+ * @return Property\Binary|null
+ */
+ protected function convertUriToBinary(Component\VCard $output, Property\Uri $newProperty)
+ {
+ $value = $newProperty->getValue();
+
+ // Only converting data: uris
+ if ('data:' !== substr($value, 0, 5)) {
+ return $newProperty;
+ }
+
+ $newProperty = $output->createProperty(
+ $newProperty->name,
+ null, // no value
+ [], // no parameters yet
+ 'BINARY'
+ );
+
+ $mimeType = substr($value, 5, strpos($value, ',') - 5);
+ if (strpos($mimeType, ';')) {
+ $mimeType = substr($mimeType, 0, strpos($mimeType, ';'));
+ $newProperty->setValue(base64_decode(substr($value, strpos($value, ',') + 1)));
+ } else {
+ $newProperty->setValue(substr($value, strpos($value, ',') + 1));
+ }
+ unset($value);
+
+ $newProperty['ENCODING'] = 'b';
+ switch ($mimeType) {
+ case 'image/jpeg':
+ $newProperty['TYPE'] = 'JPEG';
+ break;
+ case 'image/png':
+ $newProperty['TYPE'] = 'PNG';
+ break;
+ case 'image/gif':
+ $newProperty['TYPE'] = 'GIF';
+ break;
+ }
+
+ return $newProperty;
+ }
+
+ /**
+ * Adds parameters to a new property for vCard 4.0.
+ */
+ protected function convertParameters40(Property $newProperty, array $parameters)
+ {
+ // Adding all parameters.
+ foreach ($parameters as $param) {
+ // vCard 2.1 allowed parameters with no name
+ if ($param->noName) {
+ $param->noName = false;
+ }
+
+ switch ($param->name) {
+ // We need to see if there's any TYPE=PREF, because in vCard 4
+ // that's now PREF=1.
+ case 'TYPE':
+ foreach ($param->getParts() as $paramPart) {
+ if ('PREF' === strtoupper($paramPart)) {
+ $newProperty->add('PREF', '1');
+ } else {
+ $newProperty->add($param->name, $paramPart);
+ }
+ }
+ break;
+ // These no longer exist in vCard 4
+ case 'ENCODING':
+ case 'CHARSET':
+ break;
+
+ default:
+ $newProperty->add($param->name, $param->getParts());
+ break;
+ }
+ }
+ }
+
+ /**
+ * Adds parameters to a new property for vCard 3.0.
+ */
+ protected function convertParameters30(Property $newProperty, array $parameters)
+ {
+ // Adding all parameters.
+ foreach ($parameters as $param) {
+ // vCard 2.1 allowed parameters with no name
+ if ($param->noName) {
+ $param->noName = false;
+ }
+
+ switch ($param->name) {
+ case 'ENCODING':
+ // This value only existed in vCard 2.1, and should be
+ // removed for anything else.
+ if ('QUOTED-PRINTABLE' !== strtoupper($param->getValue())) {
+ $newProperty->add($param->name, $param->getParts());
+ }
+ break;
+
+ /*
+ * Converting PREF=1 to TYPE=PREF.
+ *
+ * Any other PREF numbers we'll drop.
+ */
+ case 'PREF':
+ if ('1' == $param->getValue()) {
+ $newProperty->add('TYPE', 'PREF');
+ }
+ break;
+
+ default:
+ $newProperty->add($param->name, $param->getParts());
+ break;
+ }
+ }
+ }
+}
diff --git a/vendor/sabre/vobject/lib/Version.php b/vendor/sabre/vobject/lib/Version.php
new file mode 100644
index 0000000..decd424
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Version.php
@@ -0,0 +1,18 @@
+serialize();
+ }
+
+ /**
+ * Serializes a jCal or jCard object.
+ *
+ * @param int $options
+ *
+ * @return string
+ */
+ public static function writeJson(Component $component, $options = 0)
+ {
+ return json_encode($component, $options);
+ }
+
+ /**
+ * Serializes a xCal or xCard object.
+ *
+ * @return string
+ */
+ public static function writeXml(Component $component)
+ {
+ $writer = new Xml\Writer();
+ $writer->openMemory();
+ $writer->setIndent(true);
+
+ $writer->startDocument('1.0', 'utf-8');
+
+ if ($component instanceof Component\VCalendar) {
+ $writer->startElement('icalendar');
+ $writer->writeAttribute('xmlns', Parser\XML::XCAL_NAMESPACE);
+ } else {
+ $writer->startElement('vcards');
+ $writer->writeAttribute('xmlns', Parser\XML::XCARD_NAMESPACE);
+ }
+
+ $component->xmlSerialize($writer);
+
+ $writer->endElement();
+
+ return $writer->outputMemory();
+ }
+}
diff --git a/vendor/sabre/vobject/lib/timezonedata/exchangezones.php b/vendor/sabre/vobject/lib/timezonedata/exchangezones.php
new file mode 100644
index 0000000..3e7eace
--- /dev/null
+++ b/vendor/sabre/vobject/lib/timezonedata/exchangezones.php
@@ -0,0 +1,95 @@
+ 'UTC',
+ 'Casablanca, Monrovia' => 'Africa/Casablanca',
+ 'Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London' => 'Europe/Lisbon',
+ 'Greenwich Mean Time; Dublin, Edinburgh, London' => 'Europe/London',
+ 'Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna' => 'Europe/Berlin',
+ 'Amsterdam, Berlin, Bern, Rom, Stockholm, Wien' => 'Europe/Berlin',
+ 'Belgrade, Pozsony, Budapest, Ljubljana, Prague' => 'Europe/Prague',
+ 'Brussels, Copenhagen, Madrid, Paris' => 'Europe/Paris',
+ 'Paris, Madrid, Brussels, Copenhagen' => 'Europe/Paris',
+ 'Prague, Central Europe' => 'Europe/Prague',
+ 'Sarajevo, Skopje, Sofija, Vilnius, Warsaw, Zagreb' => 'Europe/Sarajevo',
+ 'West Central Africa' => 'Africa/Luanda', // This was a best guess
+ 'Athens, Istanbul, Minsk' => 'Europe/Athens',
+ 'Bucharest' => 'Europe/Bucharest',
+ 'Cairo' => 'Africa/Cairo',
+ 'Harare, Pretoria' => 'Africa/Harare',
+ 'Helsinki, Riga, Tallinn' => 'Europe/Helsinki',
+ 'Israel, Jerusalem Standard Time' => 'Asia/Jerusalem',
+ 'Baghdad' => 'Asia/Baghdad',
+ 'Arab, Kuwait, Riyadh' => 'Asia/Kuwait',
+ 'Moscow, St. Petersburg, Volgograd' => 'Europe/Moscow',
+ 'East Africa, Nairobi' => 'Africa/Nairobi',
+ 'Tehran' => 'Asia/Tehran',
+ 'Abu Dhabi, Muscat' => 'Asia/Muscat', // Best guess
+ 'Baku, Tbilisi, Yerevan' => 'Asia/Baku',
+ 'Kabul' => 'Asia/Kabul',
+ 'Ekaterinburg' => 'Asia/Yekaterinburg',
+ 'Islamabad, Karachi, Tashkent' => 'Asia/Karachi',
+ 'Kolkata, Chennai, Mumbai, New Delhi, India Standard Time' => 'Asia/Calcutta',
+ 'Kathmandu, Nepal' => 'Asia/Kathmandu',
+ 'Almaty, Novosibirsk, North Central Asia' => 'Asia/Almaty',
+ 'Astana, Dhaka' => 'Asia/Dhaka',
+ 'Sri Jayawardenepura, Sri Lanka' => 'Asia/Colombo',
+ 'Rangoon' => 'Asia/Rangoon',
+ 'Bangkok, Hanoi, Jakarta' => 'Asia/Bangkok',
+ 'Krasnoyarsk' => 'Asia/Krasnoyarsk',
+ 'Beijing, Chongqing, Hong Kong SAR, Urumqi' => 'Asia/Shanghai',
+ 'Irkutsk, Ulaan Bataar' => 'Asia/Irkutsk',
+ 'Kuala Lumpur, Singapore' => 'Asia/Singapore',
+ 'Perth, Western Australia' => 'Australia/Perth',
+ 'Taipei' => 'Asia/Taipei',
+ 'Osaka, Sapporo, Tokyo' => 'Asia/Tokyo',
+ 'Seoul, Korea Standard time' => 'Asia/Seoul',
+ 'Yakutsk' => 'Asia/Yakutsk',
+ 'Adelaide, Central Australia' => 'Australia/Adelaide',
+ 'Darwin' => 'Australia/Darwin',
+ 'Brisbane, East Australia' => 'Australia/Brisbane',
+ 'Canberra, Melbourne, Sydney, Hobart (year 2000 only)' => 'Australia/Sydney',
+ 'Guam, Port Moresby' => 'Pacific/Guam',
+ 'Hobart, Tasmania' => 'Australia/Hobart',
+ 'Vladivostok' => 'Asia/Vladivostok',
+ 'Magadan, Solomon Is., New Caledonia' => 'Asia/Magadan',
+ 'Auckland, Wellington' => 'Pacific/Auckland',
+ 'Fiji Islands, Kamchatka, Marshall Is.' => 'Pacific/Fiji',
+ 'Nuku\'alofa, Tonga' => 'Pacific/Tongatapu',
+ 'Azores' => 'Atlantic/Azores',
+ 'Cape Verde Is.' => 'Atlantic/Cape_Verde',
+ 'Mid-Atlantic' => 'America/Noronha',
+ 'Brasilia' => 'America/Sao_Paulo', // Best guess
+ 'Buenos Aires' => 'America/Argentina/Buenos_Aires',
+ 'Greenland' => 'America/Godthab',
+ 'Newfoundland' => 'America/St_Johns',
+ 'Atlantic Time (Canada)' => 'America/Halifax',
+ 'Caracas, La Paz' => 'America/Caracas',
+ 'Santiago' => 'America/Santiago',
+ 'Bogota, Lima, Quito' => 'America/Bogota',
+ 'Eastern Time (US & Canada)' => 'America/New_York',
+ 'Indiana (East)' => 'America/Indiana/Indianapolis',
+ 'Central America' => 'America/Guatemala',
+ 'Central Time (US & Canada)' => 'America/Chicago',
+ 'Mexico City, Tegucigalpa' => 'America/Mexico_City',
+ 'Saskatchewan' => 'America/Edmonton',
+ 'Arizona' => 'America/Phoenix',
+ 'Mountain Time (US & Canada)' => 'America/Denver', // Best guess
+ 'Pacific Time (US & Canada)' => 'America/Los_Angeles', // Best guess
+ 'Pacific Time (US & Canada); Tijuana' => 'America/Los_Angeles', // Best guess
+ 'Alaska' => 'America/Anchorage',
+ 'Hawaii' => 'Pacific/Honolulu',
+ 'Midway Island, Samoa' => 'Pacific/Midway',
+ 'Eniwetok, Kwajalein, Dateline Time' => 'Pacific/Kwajalein',
+];
diff --git a/vendor/sabre/vobject/lib/timezonedata/lotuszones.php b/vendor/sabre/vobject/lib/timezonedata/lotuszones.php
new file mode 100644
index 0000000..4b50808
--- /dev/null
+++ b/vendor/sabre/vobject/lib/timezonedata/lotuszones.php
@@ -0,0 +1,101 @@
+ 'Etc/GMT-12',
+ 'Samoa' => 'Pacific/Apia',
+ 'Hawaiian' => 'Pacific/Honolulu',
+ 'Alaskan' => 'America/Anchorage',
+ 'Pacific' => 'America/Los_Angeles',
+ 'Pacific Standard Time' => 'America/Los_Angeles',
+ 'Mexico Standard Time 2' => 'America/Chihuahua',
+ 'Mountain' => 'America/Denver',
+ // 'Mountain Standard Time' => 'America/Chihuahua', // conflict with windows timezones.
+ 'US Mountain' => 'America/Phoenix',
+ 'Canada Central' => 'America/Edmonton',
+ 'Central America' => 'America/Guatemala',
+ 'Central' => 'America/Chicago',
+ // 'Central Standard Time' => 'America/Mexico_City', // conflict with windows timezones.
+ 'Mexico' => 'America/Mexico_City',
+ 'Eastern' => 'America/New_York',
+ 'SA Pacific' => 'America/Bogota',
+ 'US Eastern' => 'America/Indiana/Indianapolis',
+ 'Venezuela' => 'America/Caracas',
+ 'Atlantic' => 'America/Halifax',
+ 'Central Brazilian' => 'America/Manaus',
+ 'Pacific SA' => 'America/Santiago',
+ 'SA Western' => 'America/La_Paz',
+ 'Newfoundland' => 'America/St_Johns',
+ 'Argentina' => 'America/Argentina/Buenos_Aires',
+ 'E. South America' => 'America/Belem',
+ 'Greenland' => 'America/Godthab',
+ 'Montevideo' => 'America/Montevideo',
+ 'SA Eastern' => 'America/Belem',
+ // 'Mid-Atlantic' => 'Etc/GMT-2', // conflict with windows timezones.
+ 'Azores' => 'Atlantic/Azores',
+ 'Cape Verde' => 'Atlantic/Cape_Verde',
+ 'Greenwich' => 'Atlantic/Reykjavik', // No I'm serious.. Greenwich is not GMT.
+ 'Morocco' => 'Africa/Casablanca',
+ 'Central Europe' => 'Europe/Prague',
+ 'Central European' => 'Europe/Sarajevo',
+ 'Romance' => 'Europe/Paris',
+ 'W. Central Africa' => 'Africa/Lagos', // Best guess
+ 'W. Europe' => 'Europe/Amsterdam',
+ 'E. Europe' => 'Europe/Minsk',
+ 'Egypt' => 'Africa/Cairo',
+ 'FLE' => 'Europe/Helsinki',
+ 'GTB' => 'Europe/Athens',
+ 'Israel' => 'Asia/Jerusalem',
+ 'Jordan' => 'Asia/Amman',
+ 'Middle East' => 'Asia/Beirut',
+ 'Namibia' => 'Africa/Windhoek',
+ 'South Africa' => 'Africa/Harare',
+ 'Arab' => 'Asia/Kuwait',
+ 'Arabic' => 'Asia/Baghdad',
+ 'E. Africa' => 'Africa/Nairobi',
+ 'Georgian' => 'Asia/Tbilisi',
+ 'Russian' => 'Europe/Moscow',
+ 'Iran' => 'Asia/Tehran',
+ 'Arabian' => 'Asia/Muscat',
+ 'Armenian' => 'Asia/Yerevan',
+ 'Azerbijan' => 'Asia/Baku',
+ 'Caucasus' => 'Asia/Yerevan',
+ 'Mauritius' => 'Indian/Mauritius',
+ 'Afghanistan' => 'Asia/Kabul',
+ 'Ekaterinburg' => 'Asia/Yekaterinburg',
+ 'Pakistan' => 'Asia/Karachi',
+ 'West Asia' => 'Asia/Tashkent',
+ 'India' => 'Asia/Calcutta',
+ 'Sri Lanka' => 'Asia/Colombo',
+ 'Nepal' => 'Asia/Kathmandu',
+ 'Central Asia' => 'Asia/Dhaka',
+ 'N. Central Asia' => 'Asia/Almaty',
+ 'Myanmar' => 'Asia/Rangoon',
+ 'North Asia' => 'Asia/Krasnoyarsk',
+ 'SE Asia' => 'Asia/Bangkok',
+ 'China' => 'Asia/Shanghai',
+ 'North Asia East' => 'Asia/Irkutsk',
+ 'Singapore' => 'Asia/Singapore',
+ 'Taipei' => 'Asia/Taipei',
+ 'W. Australia' => 'Australia/Perth',
+ 'Korea' => 'Asia/Seoul',
+ 'Tokyo' => 'Asia/Tokyo',
+ 'Yakutsk' => 'Asia/Yakutsk',
+ 'AUS Central' => 'Australia/Darwin',
+ 'Cen. Australia' => 'Australia/Adelaide',
+ 'AUS Eastern' => 'Australia/Sydney',
+ 'E. Australia' => 'Australia/Brisbane',
+ 'Tasmania' => 'Australia/Hobart',
+ 'Vladivostok' => 'Asia/Vladivostok',
+ 'West Pacific' => 'Pacific/Guam',
+ 'Central Pacific' => 'Asia/Magadan',
+ 'Fiji' => 'Pacific/Fiji',
+ 'New Zealand' => 'Pacific/Auckland',
+ 'Tonga' => 'Pacific/Tongatapu',
+];
diff --git a/vendor/sabre/vobject/lib/timezonedata/php-bc.php b/vendor/sabre/vobject/lib/timezonedata/php-bc.php
new file mode 100644
index 0000000..3116c68
--- /dev/null
+++ b/vendor/sabre/vobject/lib/timezonedata/php-bc.php
@@ -0,0 +1,152 @@
+ 'America/Chicago',
+ 'Cuba' => 'America/Havana',
+ 'Egypt' => 'Africa/Cairo',
+ 'Eire' => 'Europe/Dublin',
+ 'EST5EDT' => 'America/New_York',
+ 'Factory' => 'UTC',
+ 'GB-Eire' => 'Europe/London',
+ 'GMT0' => 'UTC',
+ 'Greenwich' => 'UTC',
+ 'Hongkong' => 'Asia/Hong_Kong',
+ 'Iceland' => 'Atlantic/Reykjavik',
+ 'Iran' => 'Asia/Tehran',
+ 'Israel' => 'Asia/Jerusalem',
+ 'Jamaica' => 'America/Jamaica',
+ 'Japan' => 'Asia/Tokyo',
+ 'Kwajalein' => 'Pacific/Kwajalein',
+ 'Libya' => 'Africa/Tripoli',
+ 'MST7MDT' => 'America/Denver',
+ 'Navajo' => 'America/Denver',
+ 'NZ-CHAT' => 'Pacific/Chatham',
+ 'Poland' => 'Europe/Warsaw',
+ 'Portugal' => 'Europe/Lisbon',
+ 'PST8PDT' => 'America/Los_Angeles',
+ 'Singapore' => 'Asia/Singapore',
+ 'Turkey' => 'Europe/Istanbul',
+ 'Universal' => 'UTC',
+ 'W-SU' => 'Europe/Moscow',
+ 'Zulu' => 'UTC',
+];
diff --git a/vendor/sabre/vobject/lib/timezonedata/windowszones.php b/vendor/sabre/vobject/lib/timezonedata/windowszones.php
new file mode 100644
index 0000000..2049a95
--- /dev/null
+++ b/vendor/sabre/vobject/lib/timezonedata/windowszones.php
@@ -0,0 +1,152 @@
+ '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' => 'America/Chicago',
+ 'Central Standard Time (Mexico)' => 'America/Mexico_City',
+ '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' => 'America/New_York',
+ 'Eastern Standard Time (Mexico)' => 'America/Cancun',
+ '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' => 'America/Denver',
+ 'Mountain Standard Time (Mexico)' => 'America/Chihuahua',
+ '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' => 'America/Los_Angeles',
+ 'Pacific Standard Time (Mexico)' => 'America/Tijuana',
+ 'Pakistan Standard Time' => 'Asia/Karachi',
+ 'Paraguay Standard Time' => 'America/Asuncion',
+ 'Qyzylorda Standard Time' => 'Asia/Qyzylorda',
+ '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/Khartoum',
+ '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',
+ 'Volgograd Standard Time' => 'Europe/Volgograd',
+ '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',
+ 'Yukon Standard Time' => 'America/Whitehorse',
+];
diff --git a/vendor/sabre/vobject/resources/schema/xcal.rng b/vendor/sabre/vobject/resources/schema/xcal.rng
new file mode 100644
index 0000000..4a51460
--- /dev/null
+++ b/vendor/sabre/vobject/resources/schema/xcal.rng
@@ -0,0 +1,1192 @@
+# RELAX NG Schema for iCalendar in XML
+# Extract from RFC6321.
+# Erratum 3042 applied.
+# Erratum 3050 applied.
+# Erratum 3314 applied.
+
+default namespace = "urn:ietf:params:xml:ns:icalendar-2.0"
+
+# 3.2 Property Parameters
+
+# 3.2.1 Alternate Text Representation
+
+altrepparam = element altrep {
+ value-uri
+}
+
+# 3.2.2 Common Name
+
+cnparam = element cn {
+ value-text
+}
+
+# 3.2.3 Calendar User Type
+
+cutypeparam = element cutype {
+ element text {
+ "INDIVIDUAL" |
+ "GROUP" |
+ "RESOURCE" |
+ "ROOM" |
+ "UNKNOWN"
+ }
+}
+
+# 3.2.4 Delegators
+
+delfromparam = element delegated-from {
+ value-cal-address+
+}
+
+# 3.2.5 Delegatees
+
+deltoparam = element delegated-to {
+ value-cal-address+
+}
+
+# 3.2.6 Directory Entry Reference
+
+dirparam = element dir {
+ value-uri
+}
+
+# 3.2.7 Inline Encoding
+
+encodingparam = element encoding {
+ element text {
+ "8BIT" |
+ "BASE64"
+ }
+}
+
+# 3.2.8 Format Type
+
+fmttypeparam = element fmttype {
+ value-text
+}
+
+# 3.2.9 Free/Busy Time Type
+
+fbtypeparam = element fbtype {
+ element text {
+ "FREE" |
+ "BUSY" |
+ "BUSY-UNAVAILABLE" |
+ "BUSY-TENTATIVE"
+ }
+}
+
+# 3.2.10 Language
+
+languageparam = element language {
+ value-text
+}
+
+# 3.2.11 Group or List Membership
+
+memberparam = element member {
+ value-cal-address+
+}
+
+# 3.2.12 Participation Status
+
+partstatparam = element partstat {
+ type-partstat-event |
+ type-partstat-todo |
+ type-partstat-jour
+}
+
+type-partstat-event = (
+ element text {
+ "NEEDS-ACTION" |
+ "ACCEPTED" |
+ "DECLINED" |
+ "TENTATIVE" |
+ "DELEGATED"
+ }
+)
+
+type-partstat-todo = (
+ element text {
+ "NEEDS-ACTION" |
+ "ACCEPTED" |
+ "DECLINED" |
+ "TENTATIVE" |
+ "DELEGATED" |
+ "COMPLETED" |
+ "IN-PROCESS"
+ }
+)
+
+type-partstat-jour = (
+ element text {
+ "NEEDS-ACTION" |
+ "ACCEPTED" |
+ "DECLINED"
+ }
+)
+
+# 3.2.13 Recurrence Identifier Range
+
+rangeparam = element range {
+ element text {
+ "THISANDFUTURE"
+ }
+}
+
+# 3.2.14 Alarm Trigger Relationship
+
+trigrelparam = element related {
+ element text {
+ "START" |
+ "END"
+ }
+}
+
+# 3.2.15 Relationship Type
+
+reltypeparam = element reltype {
+ element text {
+ "PARENT" |
+ "CHILD" |
+ "SIBLING"
+ }
+}
+
+# 3.2.16 Participation Role
+
+roleparam = element role {
+ element text {
+ "CHAIR" |
+ "REQ-PARTICIPANT" |
+ "OPT-PARTICIPANT" |
+ "NON-PARTICIPANT"
+ }
+}
+
+# 3.2.17 RSVP Expectation
+
+rsvpparam = element rsvp {
+ value-boolean
+}
+
+# 3.2.18 Sent By
+
+sentbyparam = element sent-by {
+ value-cal-address
+}
+
+# 3.2.19 Time Zone Identifier
+
+tzidparam = element tzid {
+ value-text
+}
+
+# 3.3 Property Value Data Types
+
+# 3.3.1 BINARY
+
+value-binary = element binary {
+ xsd:string
+}
+
+# 3.3.2 BOOLEAN
+
+value-boolean = element boolean {
+ xsd:boolean
+}
+
+# 3.3.3 CAL-ADDRESS
+
+value-cal-address = element cal-address {
+ xsd:anyURI
+}
+
+# 3.3.4 DATE
+
+pattern-date = xsd:string {
+ pattern = "\d\d\d\d-\d\d-\d\d"
+}
+
+value-date = element date {
+ pattern-date
+}
+
+# 3.3.5 DATE-TIME
+
+pattern-date-time = xsd:string {
+ pattern = "\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ?"
+}
+
+value-date-time = element date-time {
+ pattern-date-time
+}
+
+# 3.3.6 DURATION
+
+pattern-duration = xsd:string {
+ pattern = "(+|-)?P(\d+W)|(\d+D)?"
+ ~ "(T(\d+H(\d+M)?(\d+S)?)|"
+ ~ "(\d+M(\d+S)?)|"
+ ~ "(\d+S))?"
+}
+
+value-duration = element duration {
+ pattern-duration
+}
+
+# 3.3.7 FLOAT
+
+value-float = element float {
+ xsd:float
+}
+
+# 3.3.8 INTEGER
+
+value-integer = element integer {
+ xsd:integer
+}
+
+# 3.3.9 PERIOD
+
+value-period = element period {
+ element start {
+ pattern-date-time
+ },
+ (
+ element end {
+ pattern-date-time
+ } |
+ element duration {
+ pattern-duration
+ }
+ )
+}
+
+# 3.3.10 RECUR
+
+value-recur = element recur {
+ type-freq,
+ (type-until | type-count)?,
+ element interval {
+ xsd:positiveInteger
+ }?,
+ type-bysecond*,
+ type-byminute*,
+ type-byhour*,
+ type-byday*,
+ type-bymonthday*,
+ type-byyearday*,
+ type-byweekno*,
+ type-bymonth*,
+ type-bysetpos*,
+ element wkst { type-weekday }?
+}
+
+type-freq = element freq {
+ "SECONDLY" |
+ "MINUTELY" |
+ "HOURLY" |
+ "DAILY" |
+ "WEEKLY" |
+ "MONTHLY" |
+ "YEARLY"
+}
+
+type-until = element until {
+ type-date |
+ type-date-time
+}
+
+type-count = element count {
+ xsd:positiveInteger
+}
+
+type-bysecond = element bysecond {
+ xsd:nonNegativeInteger
+}
+
+type-byminute = element byminute {
+ xsd:nonNegativeInteger
+}
+
+type-byhour = element byhour {
+ xsd:nonNegativeInteger
+}
+
+type-weekday = (
+ "SU" |
+ "MO" |
+ "TU" |
+ "WE" |
+ "TH" |
+ "FR" |
+ "SA"
+)
+
+type-byday = element byday {
+ xsd:integer?,
+ type-weekday
+}
+
+type-bymonthday = element bymonthday {
+ xsd:integer
+}
+
+type-byyearday = element byyearday {
+ xsd:integer
+}
+
+type-byweekno = element byweekno {
+ xsd:integer
+}
+
+type-bymonth = element bymonth {
+ xsd:positiveInteger
+}
+
+type-bysetpos = element bysetpos {
+ xsd:integer
+}
+
+# 3.3.11 TEXT
+
+value-text = element text {
+ xsd:string
+}
+
+# 3.3.12 TIME
+
+pattern-time = xsd:string {
+ pattern = "\d\d:\d\d:\d\dZ?"
+}
+
+value-time = element time {
+ pattern-time
+}
+
+# 3.3.13 URI
+
+value-uri = element uri {
+ xsd:anyURI
+}
+
+# 3.3.14 UTC-OFFSET
+
+value-utc-offset = element utc-offset {
+ xsd:string { pattern = "(+|-)\d\d:\d\d(:\d\d)?" }
+}
+
+# UNKNOWN
+
+value-unknown = element unknown {
+ xsd:string
+}
+
+# 3.4 iCalendar Stream
+
+start = element icalendar {
+ vcalendar+
+}
+
+# 3.6 Calendar Components
+
+vcalendar = element vcalendar {
+ type-calprops,
+ type-component
+}
+
+type-calprops = element properties {
+ property-prodid &
+ property-version &
+ property-calscale? &
+ property-method?
+}
+
+type-component = element components {
+ (
+ component-vevent |
+ component-vtodo |
+ component-vjournal |
+ component-vfreebusy |
+ component-vtimezone
+ )*
+}
+
+# 3.6.1 Event Component
+
+component-vevent = element vevent {
+ type-eventprop,
+ element components {
+ component-valarm+
+ }?
+}
+
+type-eventprop = element properties {
+ property-dtstamp &
+ property-dtstart &
+ property-uid &
+
+ property-class? &
+ property-created? &
+ property-description? &
+ property-geo? &
+ property-last-mod? &
+ property-location? &
+ property-organizer? &
+ property-priority? &
+ property-seq? &
+ property-status-event? &
+ property-summary? &
+ property-transp? &
+ property-url? &
+ property-recurid? &
+
+ property-rrule? &
+
+ (property-dtend | property-duration)? &
+
+ property-attach* &
+ property-attendee* &
+ property-categories* &
+ property-comment* &
+ property-contact* &
+ property-exdate* &
+ property-rstatus* &
+ property-related* &
+ property-resources* &
+ property-rdate*
+}
+
+# 3.6.2 To-do Component
+
+component-vtodo = element vtodo {
+ type-todoprop,
+ element components {
+ component-valarm+
+ }?
+}
+
+type-todoprop = element properties {
+ property-dtstamp &
+ property-uid &
+
+ property-class? &
+ property-completed? &
+ property-created? &
+ property-description? &
+ property-geo? &
+ property-last-mod? &
+ property-location? &
+ property-organizer? &
+ property-percent? &
+ property-priority? &
+ property-recurid? &
+ property-seq? &
+ property-status-todo? &
+ property-summary? &
+ property-url? &
+
+ property-rrule? &
+
+ (
+ (property-dtstart?, property-dtend? ) |
+ (property-dtstart, property-duration)?
+ ) &
+
+ property-attach* &
+ property-attendee* &
+ property-categories* &
+ property-comment* &
+ property-contact* &
+ property-exdate* &
+ property-rstatus* &
+ property-related* &
+ property-resources* &
+ property-rdate*
+}
+
+# 3.6.3 Journal Component
+
+component-vjournal = element vjournal {
+ type-jourprop
+}
+
+type-jourprop = element properties {
+ property-dtstamp &
+ property-uid &
+
+ property-class? &
+ property-created? &
+ property-dtstart? &
+ property-last-mod? &
+ property-organizer? &
+ property-recurid? &
+ property-seq? &
+ property-status-jour? &
+ property-summary? &
+ property-url? &
+
+ property-rrule? &
+
+ property-attach* &
+ property-attendee* &
+ property-categories* &
+ property-comment* &
+ property-contact* &
+ property-description? &
+ property-exdate* &
+ property-related* &
+ property-rdate* &
+ property-rstatus*
+}
+
+# 3.6.4 Free/Busy Component
+
+component-vfreebusy = element vfreebusy {
+ type-fbprop
+}
+
+type-fbprop = element properties {
+ property-dtstamp &
+ property-uid &
+
+ property-contact? &
+ property-dtstart? &
+ property-dtend? &
+ property-duration? &
+ property-organizer? &
+ property-url? &
+
+ property-attendee* &
+ property-comment* &
+ property-freebusy* &
+ property-rstatus*
+}
+
+# 3.6.5 Time Zone Component
+
+component-vtimezone = element vtimezone {
+ element properties {
+ property-tzid &
+
+ property-last-mod? &
+ property-tzurl?
+ },
+ element components {
+ (component-standard | component-daylight) &
+ component-standard* &
+ component-daylight*
+ }
+}
+
+component-standard = element standard {
+ type-tzprop
+}
+
+component-daylight = element daylight {
+ type-tzprop
+}
+
+type-tzprop = element properties {
+ property-dtstart &
+ property-tzoffsetto &
+ property-tzoffsetfrom &
+
+ property-rrule? &
+
+ property-comment* &
+ property-rdate* &
+ property-tzname*
+}
+
+# 3.6.6 Alarm Component
+
+component-valarm = element valarm {
+ type-audioprop | type-dispprop | type-emailprop
+}
+
+type-audioprop = element properties {
+ property-action &
+
+ property-trigger &
+
+ (property-duration, property-repeat)? &
+
+ property-attach?
+}
+
+type-emailprop = element properties {
+ property-action &
+ property-description &
+ property-trigger &
+ property-summary &
+
+ property-attendee+ &
+
+ (property-duration, property-repeat)? &
+
+ property-attach*
+}
+
+type-dispprop = element properties {
+ property-action &
+ property-description &
+ property-trigger &
+
+ (property-duration, property-repeat)?
+}
+
+# 3.7 Calendar Properties
+
+# 3.7.1 Calendar Scale
+
+property-calscale = element calscale {
+
+ element parameters { empty }?,
+
+ element text { "GREGORIAN" }
+}
+
+# 3.7.2 Method
+
+property-method = element method {
+
+ element parameters { empty }?,
+
+ value-text
+}
+
+# 3.7.3 Product Identifier
+
+property-prodid = element prodid {
+
+ element parameters { empty }?,
+
+ value-text
+}
+
+# 3.7.4 Version
+
+property-version = element version {
+
+ element parameters { empty }?,
+
+ element text { "2.0" }
+}
+
+# 3.8 Component Properties
+
+# 3.8.1 Descriptive Component Properties
+
+# 3.8.1.1 Attachment
+
+property-attach = element attach {
+
+ element parameters {
+ fmttypeparam? &
+ encodingparam?
+ }?,
+
+ value-uri | value-binary
+}
+
+# 3.8.1.2 Categories
+
+property-categories = element categories {
+
+ element parameters {
+ languageparam? &
+ }?,
+
+ value-text+
+}
+
+# 3.8.1.3 Classification
+
+property-class = element class {
+
+ element parameters { empty }?,
+
+ element text {
+ "PUBLIC" |
+ "PRIVATE" |
+ "CONFIDENTIAL"
+ }
+}
+
+# 3.8.1.4 Comment
+
+property-comment = element comment {
+
+ element parameters {
+ altrepparam? &
+ languageparam?
+ }?,
+
+ value-text
+}
+
+# 3.8.1.5 Description
+
+property-description = element description {
+
+ element parameters {
+ altrepparam? &
+ languageparam?
+ }?,
+
+ value-text
+}
+
+# 3.8.1.6 Geographic Position
+
+property-geo = element geo {
+
+ element parameters { empty }?,
+
+ element latitude { xsd:float },
+ element longitude { xsd:float }
+}
+
+# 3.8.1.7 Location
+
+property-location = element location {
+
+ element parameters {
+
+ altrepparam? &
+ languageparam?
+ }?,
+
+ value-text
+}
+
+# 3.8.1.8 Percent Complete
+
+property-percent = element percent-complete {
+
+ element parameters { empty }?,
+
+ value-integer
+}
+
+# 3.8.1.9 Priority
+
+property-priority = element priority {
+
+ element parameters { empty }?,
+
+ value-integer
+}
+
+# 3.8.1.10 Resources
+
+property-resources = element resources {
+
+ element parameters {
+ altrepparam? &
+ languageparam?
+ }?,
+
+ value-text+
+}
+
+# 3.8.1.11 Status
+
+property-status-event = element status {
+
+ element parameters { empty }?,
+
+ element text {
+ "TENTATIVE" |
+ "CONFIRMED" |
+ "CANCELLED"
+ }
+}
+
+property-status-todo = element status {
+
+ element parameters { empty }?,
+
+ element text {
+ "NEEDS-ACTION" |
+ "COMPLETED" |
+ "IN-PROCESS" |
+ "CANCELLED"
+ }
+}
+
+property-status-jour = element status {
+
+ element parameters { empty }?,
+
+ element text {
+ "DRAFT" |
+ "FINAL" |
+ "CANCELLED"
+ }
+}
+
+# 3.8.1.12 Summary
+
+property-summary = element summary {
+
+ element parameters {
+ altrepparam? &
+ languageparam?
+ }?,
+
+ value-text
+}
+
+# 3.8.2 Date and Time Component Properties
+
+# 3.8.2.1 Date/Time Completed
+
+property-completed = element completed {
+
+ element parameters { empty }?,
+
+ value-date-time
+}
+
+# 3.8.2.2 Date/Time End
+
+property-dtend = element dtend {
+
+ element parameters {
+ tzidparam?
+ }?,
+
+ value-date-time |
+ value-date
+}
+
+# 3.8.2.3 Date/Time Due
+
+property-due = element due {
+
+ element parameters {
+ tzidparam?
+ }?,
+
+ value-date-time |
+ value-date
+}
+
+# 3.8.2.4 Date/Time Start
+
+property-dtstart = element dtstart {
+
+ element parameters {
+ tzidparam?
+ }?,
+
+ value-date-time |
+ value-date
+}
+
+# 3.8.2.5 Duration
+
+property-duration = element duration {
+
+ element parameters { empty }?,
+
+ value-duration
+}
+
+# 3.8.2.6 Free/Busy Time
+
+property-freebusy = element freebusy {
+
+ element parameters {
+ fbtypeparam?
+ }?,
+
+
+ value-period+
+}
+
+# 3.8.2.7 Time Transparency
+
+property-transp = element transp {
+
+ element parameters { empty }?,
+
+ element text {
+ "OPAQUE" |
+ "TRANSPARENT"
+ }
+}
+
+# 3.8.3 Time Zone Component Properties
+
+# 3.8.3.1 Time Zone Identifier
+
+property-tzid = element tzid {
+
+ element parameters { empty }?,
+
+ value-text
+}
+
+# 3.8.3.2 Time Zone Name
+
+property-tzname = element tzname {
+
+ element parameters {
+ languageparam?
+ }?,
+
+ value-text
+}
+
+# 3.8.3.3 Time Zone Offset From
+
+property-tzoffsetfrom = element tzoffsetfrom {
+
+ element parameters { empty }?,
+
+ value-utc-offset
+}
+
+# 3.8.3.4 Time Zone Offset To
+
+property-tzoffsetto = element tzoffsetto {
+
+ element parameters { empty }?,
+
+ value-utc-offset
+}
+
+# 3.8.3.5 Time Zone URL
+
+property-tzurl = element tzurl {
+
+ element parameters { empty }?,
+
+ value-uri
+}
+
+# 3.8.4 Relationship Component Properties
+
+# 3.8.4.1 Attendee
+
+property-attendee = element attendee {
+
+ element parameters {
+ cutypeparam? &
+ memberparam? &
+ roleparam? &
+ partstatparam? &
+ rsvpparam? &
+ deltoparam? &
+ delfromparam? &
+ sentbyparam? &
+ cnparam? &
+ dirparam? &
+ languageparam?
+ }?,
+
+ value-cal-address
+}
+
+# 3.8.4.2 Contact
+
+property-contact = element contact {
+
+ element parameters {
+ altrepparam? &
+ languageparam?
+ }?,
+
+ value-text
+}
+
+# 3.8.4.3 Organizer
+
+property-organizer = element organizer {
+
+ element parameters {
+ cnparam? &
+ dirparam? &
+ sentbyparam? &
+ languageparam?
+ }?,
+
+ value-cal-address
+}
+
+# 3.8.4.4 Recurrence ID
+
+property-recurid = element recurrence-id {
+
+ element parameters {
+ tzidparam? &
+ rangeparam?
+ }?,
+
+ value-date-time |
+ value-date
+}
+
+# 3.8.4.5 Related-To
+
+property-related = element related-to {
+
+ element parameters {
+ reltypeparam?
+ }?,
+
+ value-text
+}
+
+# 3.8.4.6 Uniform Resource Locator
+
+property-url = element url {
+
+ element parameters { empty }?,
+
+ value-uri
+}
+
+# 3.8.4.7 Unique Identifier
+
+property-uid = element uid {
+
+ element parameters { empty }?,
+
+ value-text
+}
+
+# 3.8.5 Recurrence Component Properties
+
+# 3.8.5.1 Exception Date/Times
+
+property-exdate = element exdate {
+
+ element parameters {
+ tzidparam?
+ }?,
+
+ value-date-time+ |
+ value-date+
+}
+
+# 3.8.5.2 Recurrence Date/Times
+
+property-rdate = element rdate {
+
+ element parameters {
+ tzidparam?
+ }?,
+
+ value-date-time+ |
+ value-date+ |
+ value-period+
+}
+
+# 3.8.5.3 Recurrence Rule
+
+property-rrule = element rrule {
+
+ element parameters { empty }?,
+
+ value-recur
+}
+
+# 3.8.6 Alarm Component Properties
+
+# 3.8.6.1 Action
+
+property-action = element action {
+
+ element parameters { empty }?,
+
+ element text {
+ "AUDIO" |
+ "DISPLAY" |
+ "EMAIL"
+ }
+}
+
+# 3.8.6.2 Repeat Count
+
+property-repeat = element repeat {
+
+ element parameters { empty }?,
+
+ value-integer
+}
+
+# 3.8.6.3 Trigger
+
+property-trigger = element trigger {
+
+ (
+ element parameters {
+ trigrelparam?
+ }?,
+
+ value-duration
+ ) |
+ (
+ element parameters { empty }?,
+
+ value-date-time
+ )
+}
+
+# 3.8.7 Change Management Component Properties
+
+# 3.8.7.1 Date/Time Created
+
+property-created = element created {
+
+ element parameters { empty }?,
+
+ value-date-time
+}
+
+# 3.8.7.2 Date/Time Stamp
+
+property-dtstamp = element dtstamp {
+
+ element parameters { empty }?,
+
+ value-date-time
+}
+
+# 3.8.7.3 Last Modified
+
+property-last-mod = element last-modified {
+
+ element parameters { empty }?,
+
+ value-date-time
+}
+
+# 3.8.7.4 Sequence Number
+
+property-seq = element sequence {
+
+ element parameters { empty }?,
+
+ value-integer
+}
+
+# 3.8.8 Miscellaneous Component Properties
+
+# 3.8.8.3 Request Status
+
+property-rstatus = element request-status {
+
+ element parameters {
+ languageparam?
+ }?,
+
+ element code { xsd:string },
+ element description { xsd:string },
+ element data { xsd:string }?
+}
diff --git a/vendor/sabre/vobject/resources/schema/xcard.rng b/vendor/sabre/vobject/resources/schema/xcard.rng
new file mode 100644
index 0000000..c0b7cfb
--- /dev/null
+++ b/vendor/sabre/vobject/resources/schema/xcard.rng
@@ -0,0 +1,388 @@
+# RELAX NG Schema for vCard in XML
+# Extract from RFC6351.
+# Erratum 2994 applied.
+# Erratum 3047 applied.
+# Erratum 3008 applied.
+# Erratum 4247 applied.
+
+default namespace = "urn:ietf:params:xml:ns:vcard-4.0"
+
+### Section 3.3: vCard Format Specification
+#
+# 3.3
+iana-token = xsd:string { pattern = "[a-zA-Z0-9\-]+" }
+x-name = xsd:string { pattern = "x-[a-zA-Z0-9\-]+" }
+
+### Section 4: Value types
+#
+# 4.1
+value-text = element text { text }
+value-text-list = value-text+
+
+# 4.2
+value-uri = element uri { xsd:anyURI }
+
+# 4.3.1
+value-date = element date {
+ xsd:string { pattern = "\d{8}|\d{4}-\d\d|--\d\d(\d\d)?|---\d\d" }
+ }
+
+# 4.3.2
+value-time = element time {
+ xsd:string { pattern = "(\d\d(\d\d(\d\d)?)?|-\d\d(\d\d)?|--\d\d)"
+ ~ "(Z|[+\-]\d\d(\d\d)?)?" }
+ }
+
+# 4.3.3
+value-date-time = element date-time {
+ xsd:string { pattern = "(\d{8}|--\d{4}|---\d\d)T\d\d(\d\d(\d\d)?)?"
+ ~ "(Z|[+\-]\d\d(\d\d)?)?" }
+ }
+
+# 4.3.4
+value-date-and-or-time = value-date | value-date-time | value-time
+
+# 4.3.5
+value-timestamp = element timestamp {
+ xsd:string { pattern = "\d{8}T\d{6}(Z|[+\-]\d\d(\d\d)?)?" }
+ }
+
+# 4.4
+value-boolean = element boolean { xsd:boolean }
+
+# 4.5
+value-integer = element integer { xsd:integer }
+
+# 4.6
+value-float = element float { xsd:float }
+
+# 4.7
+value-utc-offset = element utc-offset {
+ xsd:string { pattern = "[+\-]\d\d(\d\d)?" }
+ }
+
+# 4.8
+value-language-tag = element language-tag {
+ xsd:string { pattern = "([a-z]{2,3}((-[a-z]{3}){0,3})?|[a-z]{4,8})"
+ ~ "(-[a-z]{4})?(-([a-z]{2}|\d{3}))?"
+ ~ "(-([0-9a-z]{5,8}|\d[0-9a-z]{3}))*"
+ ~ "(-[0-9a-wyz](-[0-9a-z]{2,8})+)*"
+ ~ "(-x(-[0-9a-z]{1,8})+)?|x(-[0-9a-z]{1,8})+|"
+ ~ "[a-z]{1,3}(-[0-9a-z]{2,8}){1,2}" }
+ }
+
+### Section 5: Parameters
+#
+# 5.1
+param-language = element language { value-language-tag }?
+
+# 5.2
+param-pref = element pref {
+ element integer {
+ xsd:integer { minInclusive = "1" maxInclusive = "100" }
+ }
+ }?
+
+# 5.4
+param-altid = element altid { value-text }?
+
+# 5.5
+param-pid = element pid {
+ element text { xsd:string { pattern = "\d+(\.\d+)?" } }+
+ }?
+
+# 5.6
+param-type = element type { element text { "work" | "home" }+ }?
+
+# 5.7
+param-mediatype = element mediatype { value-text }?
+
+# 5.8
+param-calscale = element calscale { element text { "gregorian" } }?
+
+# 5.9
+param-sort-as = element sort-as { value-text+ }?
+
+# 5.10
+param-geo = element geo { value-uri }?
+
+# 5.11
+param-tz = element tz { value-text | value-uri }?
+
+### Section 6: Properties
+#
+# 6.1.3
+property-source = element source {
+ element parameters { param-altid, param-pid, param-pref,
+ param-mediatype }?,
+ value-uri
+ }
+
+# 6.1.4
+property-kind = element kind {
+ element text { "individual" | "group" | "org" | "location" |
+ x-name | iana-token }*
+ }
+
+# 6.2.1
+property-fn = element fn {
+ element parameters { param-language, param-altid, param-pid,
+ param-pref, param-type }?,
+ value-text
+ }
+
+# 6.2.2
+property-n = element n {
+ element parameters { param-language, param-sort-as, param-altid }?,
+ element surname { text }+,
+ element given { text }+,
+ element additional { text }+,
+ element prefix { text }+,
+ element suffix { text }+
+ }
+
+# 6.2.3
+property-nickname = element nickname {
+ element parameters { param-language, param-altid, param-pid,
+ param-pref, param-type }?,
+ value-text-list
+ }
+
+# 6.2.4
+property-photo = element photo {
+ element parameters { param-altid, param-pid, param-pref, param-type,
+ param-mediatype }?,
+ value-uri
+ }
+
+# 6.2.5
+property-bday = element bday {
+ element parameters { param-altid, param-calscale }?,
+ (value-date-and-or-time | value-text)
+ }
+
+# 6.2.6
+property-anniversary = element anniversary {
+ element parameters { param-altid, param-calscale }?,
+ (value-date-and-or-time | value-text)
+ }
+
+# 6.2.7
+property-gender = element gender {
+ element sex { "" | "M" | "F" | "O" | "N" | "U" },
+ element identity { text }?
+ }
+
+# 6.3.1
+param-label = element label { value-text }?
+property-adr = element adr {
+ element parameters { param-language, param-altid, param-pid,
+ param-pref, param-type, param-geo, param-tz,
+ param-label }?,
+ element pobox { text }+,
+ element ext { text }+,
+ element street { text }+,
+ element locality { text }+,
+ element region { text }+,
+ element code { text }+,
+ element country { text }+
+ }
+
+# 6.4.1
+property-tel = element tel {
+ element parameters {
+ param-altid,
+ param-pid,
+ param-pref,
+ element type {
+ element text { "work" | "home" | "text" | "voice"
+ | "fax" | "cell" | "video" | "pager"
+ | "textphone" | x-name | iana-token }+
+ }?,
+ param-mediatype
+ }?,
+ (value-text | value-uri)
+ }
+
+# 6.4.2
+property-email = element email {
+ element parameters { param-altid, param-pid, param-pref,
+ param-type }?,
+ value-text
+ }
+
+# 6.4.3
+property-impp = element impp {
+ element parameters { param-altid, param-pid, param-pref,
+ param-type, param-mediatype }?,
+ value-uri
+ }
+
+# 6.4.4
+property-lang = element lang {
+ element parameters { param-altid, param-pid, param-pref,
+ param-type }?,
+ value-language-tag
+ }
+
+# 6.5.1
+property-tz = element tz {
+ element parameters { param-altid, param-pid, param-pref,
+ param-type, param-mediatype }?,
+ (value-text | value-uri | value-utc-offset)
+ }
+
+# 6.5.2
+property-geo = element geo {
+ element parameters { param-altid, param-pid, param-pref,
+ param-type, param-mediatype }?,
+ value-uri
+ }
+
+# 6.6.1
+property-title = element title {
+ element parameters { param-language, param-altid, param-pid,
+ param-pref, param-type }?,
+ value-text
+ }
+
+# 6.6.2
+property-role = element role {
+ element parameters { param-language, param-altid, param-pid,
+ param-pref, param-type }?,
+ value-text
+ }
+
+# 6.6.3
+property-logo = element logo {
+ element parameters { param-language, param-altid, param-pid,
+ param-pref, param-type, param-mediatype }?,
+ value-uri
+ }
+
+# 6.6.4
+property-org = element org {
+ element parameters { param-language, param-altid, param-pid,
+ param-pref, param-type, param-sort-as }?,
+ value-text-list
+ }
+
+# 6.6.5
+property-member = element member {
+ element parameters { param-altid, param-pid, param-pref,
+ param-mediatype }?,
+ value-uri
+ }
+
+# 6.6.6
+property-related = element related {
+ element parameters {
+ param-altid,
+ param-pid,
+ param-pref,
+ element type {
+ element text {
+ "work" | "home" | "contact" | "acquaintance" |
+ "friend" | "met" | "co-worker" | "colleague" | "co-resident" |
+ "neighbor" | "child" | "parent" | "sibling" | "spouse" |
+ "kin" | "muse" | "crush" | "date" | "sweetheart" | "me" |
+ "agent" | "emergency"
+ }+
+ }?,
+ param-mediatype
+ }?,
+ (value-uri | value-text)
+ }
+
+# 6.7.1
+property-categories = element categories {
+ element parameters { param-altid, param-pid, param-pref,
+ param-type }?,
+ value-text-list
+ }
+
+# 6.7.2
+property-note = element note {
+ element parameters { param-language, param-altid, param-pid,
+ param-pref, param-type }?,
+ value-text
+ }
+
+# 6.7.3
+property-prodid = element prodid { value-text }
+
+# 6.7.4
+property-rev = element rev { value-timestamp }
+
+# 6.7.5
+property-sound = element sound {
+ element parameters { param-language, param-altid, param-pid,
+ param-pref, param-type, param-mediatype }?,
+ value-uri
+ }
+
+# 6.7.6
+property-uid = element uid { value-uri }
+
+# 6.7.7
+property-clientpidmap = element clientpidmap {
+ element sourceid { xsd:positiveInteger },
+ value-uri
+ }
+
+# 6.7.8
+property-url = element url {
+ element parameters { param-altid, param-pid, param-pref,
+ param-type, param-mediatype }?,
+ value-uri
+ }
+
+# 6.8.1
+property-key = element key {
+ element parameters { param-altid, param-pid, param-pref,
+ param-type, param-mediatype }?,
+ (value-uri | value-text)
+ }
+
+# 6.9.1
+property-fburl = element fburl {
+ element parameters { param-altid, param-pid, param-pref,
+ param-type, param-mediatype }?,
+ value-uri
+ }
+
+# 6.9.2
+property-caladruri = element caladruri {
+ element parameters { param-altid, param-pid, param-pref,
+ param-type, param-mediatype }?,
+ value-uri
+ }
+
+# 6.9.3
+property-caluri = element caluri {
+ element parameters { param-altid, param-pid, param-pref,
+ param-type, param-mediatype }?,
+ value-uri
+ }
+
+# Top-level grammar
+property = property-adr | property-anniversary | property-bday
+ | property-caladruri | property-caluri | property-categories
+ | property-clientpidmap | property-email | property-fburl
+ | property-fn | property-geo | property-impp | property-key
+ | property-kind | property-lang | property-logo
+ | property-member | property-n | property-nickname
+ | property-note | property-org | property-photo
+ | property-prodid | property-related | property-rev
+ | property-role | property-gender | property-sound
+ | property-source | property-tel | property-title
+ | property-tz | property-uid | property-url
+start = element vcards {
+ element vcard {
+ (property
+ | element group {
+ attribute name { text },
+ property*
+ })+
+ }+
+ }
diff --git a/vendor/sabre/xml/LICENSE b/vendor/sabre/xml/LICENSE
new file mode 100644
index 0000000..c9faf40
--- /dev/null
+++ b/vendor/sabre/xml/LICENSE
@@ -0,0 +1,27 @@
+Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/)
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name Sabre nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/sabre/xml/README.md b/vendor/sabre/xml/README.md
new file mode 100644
index 0000000..9513a89
--- /dev/null
+++ b/vendor/sabre/xml/README.md
@@ -0,0 +1,26 @@
+sabre/xml
+=========
+
+
+
+The sabre/xml library is a specialized XML reader and writer.
+
+Documentation
+-------------
+
+* [Introduction](http://sabre.io/xml/).
+* [Installation](http://sabre.io/xml/install/).
+* [Reading XML](http://sabre.io/xml/reading/).
+* [Writing XML](http://sabre.io/xml/writing/).
+
+Major version 3 implements type declarations for input parameters, function returns, variables etc. It supports PHP 7.4 and PHP 8. When you upgrade to major version 3, if you extend classes etc., then you will need to make similar type declarations in your code.
+
+Support
+-------
+
+Head over to the [SabreDAV mailing list](http://groups.google.com/group/sabredav-discuss) for any questions.
+
+Made at fruux
+-------------
+
+This library is being developed by [fruux](https://fruux.com/). Drop us a line for commercial services or enterprise support.
diff --git a/vendor/sabre/xml/composer.json b/vendor/sabre/xml/composer.json
new file mode 100644
index 0000000..faf06b3
--- /dev/null
+++ b/vendor/sabre/xml/composer.json
@@ -0,0 +1,70 @@
+{
+ "name": "sabre/xml",
+ "description" : "sabre/xml is an XML library that you may not hate.",
+ "keywords" : [ "XML", "XMLReader", "XMLWriter", "DOM" ],
+ "homepage" : "https://sabre.io/xml/",
+ "license" : "BSD-3-Clause",
+ "require" : {
+ "php" : "^7.4 || ^8.0",
+ "ext-xmlwriter" : "*",
+ "ext-xmlreader" : "*",
+ "ext-dom" : "*",
+ "lib-libxml" : ">=2.6.20",
+ "sabre/uri" : ">=2.0,<4.0.0"
+ },
+ "authors" : [
+ {
+ "name" : "Evert Pot",
+ "email" : "me@evertpot.com",
+ "homepage" : "http://evertpot.com/",
+ "role" : "Developer"
+ },
+ {
+ "name": "Markus Staab",
+ "email": "markus.staab@redaxo.de",
+ "role" : "Developer"
+ }
+ ],
+ "support" : {
+ "forum" : "https://groups.google.com/group/sabredav-discuss",
+ "source" : "https://github.com/fruux/sabre-xml"
+ },
+ "autoload" : {
+ "psr-4" : {
+ "Sabre\\Xml\\" : "lib/"
+ },
+ "files": [
+ "lib/Deserializer/functions.php",
+ "lib/Serializer/functions.php"
+ ]
+ },
+ "autoload-dev" : {
+ "psr-4" : {
+ "Sabre\\Xml\\" : "tests/Sabre/Xml/"
+ }
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.64",
+ "phpstan/phpstan": "^1.12",
+ "phpunit/phpunit" : "^9.6"
+ },
+ "scripts": {
+ "phpstan": [
+ "phpstan analyse"
+ ],
+ "phpstan-baseline": [
+ "phpstan analyse --generate-baseline phpstan-baseline.neon"
+ ],
+ "cs-fixer": [
+ "PHP_CS_FIXER_IGNORE_ENV=true php-cs-fixer fix"
+ ],
+ "phpunit": [
+ "phpunit --configuration tests/phpunit.xml"
+ ],
+ "test": [
+ "composer phpstan",
+ "composer cs-fixer",
+ "composer phpunit"
+ ]
+ }
+}
diff --git a/vendor/sabre/xml/lib/ContextStackTrait.php b/vendor/sabre/xml/lib/ContextStackTrait.php
new file mode 100644
index 0000000..7c082f5
--- /dev/null
+++ b/vendor/sabre/xml/lib/ContextStackTrait.php
@@ -0,0 +1,116 @@
+
+ */
+ public array $elementMap = [];
+
+ /**
+ * A contextUri pointing to the document being parsed / written.
+ * This uri may be used to resolve relative urls that may appear in the
+ * document.
+ *
+ * The reader and writer don't use this property, but as it's an extremely
+ * common use-case for parsing XML documents, it's added here as a
+ * convenience.
+ */
+ public ?string $contextUri = null;
+
+ /**
+ * This is a list of namespaces that you want to give default prefixes.
+ *
+ * You must make sure you create this entire list before starting to write.
+ * They should be registered on the root element.
+ *
+ * @phpstan-var array
+ */
+ public array $namespaceMap = [];
+
+ /**
+ * This is a list of custom serializers for specific classes.
+ *
+ * The writer may use this if you attempt to serialize an object with a
+ * class that does not implement XmlSerializable.
+ *
+ * Instead, it will look at this classmap to see if there is a custom
+ * serializer here. This is useful if you don't want your value objects
+ * to be responsible for serializing themselves.
+ *
+ * The keys in this classmap need to be fully qualified PHP class names,
+ * the values must be callbacks. The callbacks take two arguments. The
+ * writer class, and the value that must be written.
+ *
+ * function (Writer $writer, object $value)
+ *
+ * @phpstan-var array
+ */
+ public array $classMap = [];
+
+ /**
+ * Backups of previous contexts.
+ *
+ * @var list
+ */
+ protected array $contextStack = [];
+
+ /**
+ * Create a new "context".
+ *
+ * This allows you to safely modify the elementMap, contextUri or
+ * namespaceMap. After you're done, you can restore the old data again
+ * with popContext.
+ */
+ public function pushContext(): void
+ {
+ $this->contextStack[] = [
+ $this->elementMap,
+ $this->contextUri,
+ $this->namespaceMap,
+ $this->classMap,
+ ];
+ }
+
+ /**
+ * Restore the previous "context".
+ */
+ public function popContext(): void
+ {
+ list(
+ $this->elementMap,
+ $this->contextUri,
+ $this->namespaceMap,
+ $this->classMap,
+ ) = array_pop($this->contextStack);
+ }
+}
diff --git a/vendor/sabre/xml/lib/Deserializer/functions.php b/vendor/sabre/xml/lib/Deserializer/functions.php
new file mode 100644
index 0000000..90b9439
--- /dev/null
+++ b/vendor/sabre/xml/lib/Deserializer/functions.php
@@ -0,0 +1,388 @@
+value" array.
+ *
+ * For example, keyvalue will parse:
+ *
+ *
+ *
+ * value1
+ * value2
+ *
+ *
+ *
+ * Into:
+ *
+ * [
+ * "{http://sabredav.org/ns}elem1" => "value1",
+ * "{http://sabredav.org/ns}elem2" => "value2",
+ * "{http://sabredav.org/ns}elem3" => null,
+ * ];
+ *
+ * If you specify the 'namespace' argument, the deserializer will remove
+ * the namespaces of the keys that match that namespace.
+ *
+ * For example, if you call keyValue like this:
+ *
+ * keyValue($reader, 'http://sabredav.org/ns')
+ *
+ * it's output will instead be:
+ *
+ * [
+ * "elem1" => "value1",
+ * "elem2" => "value2",
+ * "elem3" => null,
+ * ];
+ *
+ * Attributes will be removed from the top-level elements. If elements with
+ * the same name appear twice in the list, only the last one will be kept.
+ *
+ * @phpstan-return array
+ */
+function keyValue(Reader $reader, ?string $namespace = null): array
+{
+ // If there's no children, we don't do anything.
+ if ($reader->isEmptyElement) {
+ $reader->next();
+
+ return [];
+ }
+
+ if (!$reader->read()) {
+ $reader->next();
+
+ return [];
+ }
+
+ if (Reader::END_ELEMENT === $reader->nodeType) {
+ $reader->next();
+
+ return [];
+ }
+
+ $values = [];
+
+ do {
+ if (Reader::ELEMENT === $reader->nodeType) {
+ if (null !== $namespace && $reader->namespaceURI === $namespace) {
+ $values[$reader->localName] = $reader->parseCurrentElement()['value'];
+ } else {
+ $clark = $reader->getClark();
+ $values[$clark] = $reader->parseCurrentElement()['value'];
+ }
+ } else {
+ if (!$reader->read()) {
+ break;
+ }
+ }
+ } while (Reader::END_ELEMENT !== $reader->nodeType);
+
+ $reader->read();
+
+ return $values;
+}
+
+/**
+ * The 'enum' deserializer parses elements into a simple list
+ * without values or attributes.
+ *
+ * For example, Elements will parse:
+ *
+ *
+ *
+ *
+ *
+ *
+ * content
+ *
+ *
+ *
+ * Into:
+ *
+ * [
+ * "{http://sabredav.org/ns}elem1",
+ * "{http://sabredav.org/ns}elem2",
+ * "{http://sabredav.org/ns}elem3",
+ * "{http://sabredav.org/ns}elem4",
+ * "{http://sabredav.org/ns}elem5",
+ * ];
+ *
+ * This is useful for 'enum'-like structures.
+ *
+ * If the $namespace argument is specified, it will strip the namespace
+ * for all elements that match that.
+ *
+ * For example,
+ *
+ * enum($reader, 'http://sabredav.org/ns')
+ *
+ * would return:
+ *
+ * [
+ * "elem1",
+ * "elem2",
+ * "elem3",
+ * "elem4",
+ * "elem5",
+ * ];
+ *
+ * @return string[]
+ *
+ * @phpstan-return list
+ */
+function enum(Reader $reader, ?string $namespace = null): array
+{
+ // If there's no children, we don't do anything.
+ if ($reader->isEmptyElement) {
+ $reader->next();
+
+ return [];
+ }
+ if (!$reader->read()) {
+ $reader->next();
+
+ return [];
+ }
+
+ if (Reader::END_ELEMENT === $reader->nodeType) {
+ $reader->next();
+
+ return [];
+ }
+ $currentDepth = $reader->depth;
+
+ $values = [];
+ do {
+ if (Reader::ELEMENT !== $reader->nodeType) {
+ continue;
+ }
+ if (!is_null($namespace) && $namespace === $reader->namespaceURI) {
+ $values[] = $reader->localName;
+ } else {
+ $values[] = (string) $reader->getClark();
+ }
+ } while ($reader->depth >= $currentDepth && $reader->next());
+
+ $reader->next();
+
+ return $values;
+}
+
+/**
+ * The valueObject deserializer turns an XML element into a PHP object of
+ * a specific class.
+ *
+ * This is primarily used by the mapValueObject function from the Service
+ * class, but it can also easily be used for more specific situations.
+ *
+ * @template C of object
+ *
+ * @param class-string $className
+ *
+ * @phpstan-return C
+ */
+function valueObject(Reader $reader, string $className, string $namespace): object
+{
+ $valueObject = new $className();
+ if ($reader->isEmptyElement) {
+ $reader->next();
+
+ return $valueObject;
+ }
+
+ $defaultProperties = get_class_vars($className);
+
+ $reader->read();
+ do {
+ if (Reader::ELEMENT === $reader->nodeType && $reader->namespaceURI == $namespace) {
+ if (property_exists($valueObject, $reader->localName)) {
+ if (is_array($defaultProperties[$reader->localName])) {
+ $valueObject->{$reader->localName}[] = $reader->parseCurrentElement()['value'];
+ } else {
+ $valueObject->{$reader->localName} = $reader->parseCurrentElement()['value'];
+ }
+ } else {
+ // Ignore property
+ $reader->next();
+ }
+ } elseif (Reader::ELEMENT === $reader->nodeType) {
+ // Skipping element from different namespace
+ $reader->next();
+ } else {
+ if (Reader::END_ELEMENT !== $reader->nodeType && !$reader->read()) {
+ break;
+ }
+ }
+ } while (Reader::END_ELEMENT !== $reader->nodeType);
+
+ $reader->read();
+
+ return $valueObject;
+}
+
+/**
+ * This deserializer helps you deserialize xml structures that look like
+ * this:.
+ *
+ *
+ * - ...
+ * - ...
+ * - ...
+ *
+ *
+ * Many XML documents use patterns like that, and this deserializer
+ * allow you to get all the 'items' as an array.
+ *
+ * In that previous example, you would register the deserializer as such:
+ *
+ * $reader->elementMap['{}collection'] = function($reader) {
+ * return repeatingElements($reader, '{}item');
+ * }
+ *
+ * The repeatingElements deserializer simply returns everything as an array.
+ *
+ * $childElementName must either be a clark-notation element name, or if no
+ * namespace is used, the bare element name.
+ *
+ * @phpstan-return list
+ */
+function repeatingElements(Reader $reader, string $childElementName): array
+{
+ if ('{' !== $childElementName[0]) {
+ $childElementName = '{}'.$childElementName;
+ }
+ $result = [];
+
+ foreach ($reader->parseGetElements() as $element) {
+ if ($element['name'] === $childElementName) {
+ $result[] = $element['value'];
+ }
+ }
+
+ return $result;
+}
+
+/**
+ * This deserializer helps you to deserialize structures which contain mixed content.
+ *
+ * some text and an inline tag and even more text
+ *
+ * The above example will return
+ *
+ * [
+ * 'some text',
+ * [
+ * 'name' => '{}extref',
+ * 'value' => 'and an inline tag',
+ * 'attributes' => []
+ * ],
+ * 'and even more text'
+ * ]
+ *
+ * In strict XML documents you won't find this kind of markup but in html this is a quite common pattern.
+ *
+ * @return array
+ */
+function mixedContent(Reader $reader): array
+{
+ // If there's no children, we don't do anything.
+ if ($reader->isEmptyElement) {
+ $reader->next();
+
+ return [];
+ }
+
+ $previousDepth = $reader->depth;
+
+ $content = [];
+ $reader->read();
+ while (true) {
+ if (Reader::ELEMENT == $reader->nodeType) {
+ $content[] = $reader->parseCurrentElement();
+ } elseif ($reader->depth >= $previousDepth && in_array($reader->nodeType, [Reader::TEXT, Reader::CDATA, Reader::WHITESPACE])) {
+ $content[] = $reader->value;
+ $reader->read();
+ } elseif (Reader::END_ELEMENT == $reader->nodeType) {
+ // Ensuring we are moving the cursor after the end element.
+ $reader->read();
+ break;
+ } else {
+ $reader->read();
+ }
+ }
+
+ return $content;
+}
+
+/**
+ * The functionCaller deserializer turns an XML element into whatever your callable returns.
+ *
+ * You can use, e.g., a named constructor (factory method) to create an object using
+ * this function.
+ *
+ * @return mixed whatever the 'func' callable returns
+ *
+ * @throws \InvalidArgumentException|\ReflectionException
+ */
+function functionCaller(Reader $reader, callable $func, string $namespace)
+{
+ if ($reader->isEmptyElement) {
+ $reader->next();
+
+ return null;
+ }
+
+ $funcArgs = [];
+ if (is_array($func)) {
+ $ref = new \ReflectionMethod($func[0], $func[1]);
+ } elseif (is_string($func) && false !== strpos($func, '::')) {
+ // We have a string that should refer to a method that exists, like "MyClass::someMethod"
+ // ReflectionMethod knows how to handle that as-is
+ $ref = new \ReflectionMethod($func);
+ } elseif ($func instanceof \Closure || is_string($func)) {
+ // We have an actual Closure (a real function) or a string that is the name of a function
+ // ReflectionFunction can take either of those
+ $ref = new \ReflectionFunction($func);
+ } else {
+ throw new \InvalidArgumentException(__METHOD__.' unable to use func parameter with ReflectionMethod or ReflectionFunction.');
+ }
+
+ foreach ($ref->getParameters() as $parameter) {
+ $funcArgs[$parameter->getName()] = null;
+ }
+
+ $reader->read();
+ do {
+ if (Reader::ELEMENT === $reader->nodeType && $reader->namespaceURI == $namespace) {
+ if (array_key_exists($reader->localName, $funcArgs)) {
+ $funcArgs[$reader->localName] = $reader->parseCurrentElement()['value'];
+ } else {
+ // Ignore property
+ $reader->next();
+ }
+ } else {
+ $reader->read();
+ }
+ } while (Reader::END_ELEMENT !== $reader->nodeType);
+ $reader->read();
+
+ return $func(...array_values($funcArgs));
+}
diff --git a/vendor/sabre/xml/lib/Element.php b/vendor/sabre/xml/lib/Element.php
new file mode 100644
index 0000000..559eb54
--- /dev/null
+++ b/vendor/sabre/xml/lib/Element.php
@@ -0,0 +1,22 @@
+value = $value;
+ }
+
+ /**
+ * The xmlSerialize method is called during xml writing.
+ *
+ * Use the $writer argument to write its own xml serialization.
+ *
+ * An important note: do _not_ create a parent element. Any element
+ * implementing XmlSerializable should only ever write what's considered
+ * its 'inner xml'.
+ *
+ * The parent of the current element is responsible for writing a
+ * containing element.
+ *
+ * This allows serializers to be re-used for different element names.
+ *
+ * If you are opening new elements, you must also close them again.
+ */
+ public function xmlSerialize(Xml\Writer $writer): void
+ {
+ $writer->write($this->value);
+ }
+
+ /**
+ * The deserialize method is called during xml parsing.
+ *
+ * This method is called statically, this is because in theory this method
+ * may be used as a type of constructor, or factory method.
+ *
+ * Often you want to return an instance of the current class, but you are
+ * free to return other data as well.
+ *
+ * Important note 2: You are responsible for advancing the reader to the
+ * next element. Not doing anything will result in a never-ending loop.
+ *
+ * If you just want to skip parsing for this element altogether, you can
+ * just call $reader->next();
+ *
+ * $reader->parseInnerTree() will parse the entire sub-tree, and advance to
+ * the next element.
+ *
+ * @return array>|string|null
+ */
+ public static function xmlDeserialize(Xml\Reader $reader)
+ {
+ $subTree = $reader->parseInnerTree();
+
+ return $subTree;
+ }
+}
diff --git a/vendor/sabre/xml/lib/Element/Cdata.php b/vendor/sabre/xml/lib/Element/Cdata.php
new file mode 100644
index 0000000..8e16afb
--- /dev/null
+++ b/vendor/sabre/xml/lib/Element/Cdata.php
@@ -0,0 +1,57 @@
+value = $value;
+ }
+
+ /**
+ * The xmlSerialize method is called during xml writing.
+ *
+ * Use the $writer argument to write its own xml serialization.
+ *
+ * An important note: do _not_ create a parent element. Any element
+ * implementing XmlSerializable should only ever write what's considered
+ * its 'inner xml'.
+ *
+ * The parent of the current element is responsible for writing a
+ * containing element.
+ *
+ * This allows serializers to be re-used for different element names.
+ *
+ * If you are opening new elements, you must also close them again.
+ */
+ public function xmlSerialize(Xml\Writer $writer): void
+ {
+ $writer->writeCData($this->value);
+ }
+}
diff --git a/vendor/sabre/xml/lib/Element/Elements.php b/vendor/sabre/xml/lib/Element/Elements.php
new file mode 100644
index 0000000..427cf35
--- /dev/null
+++ b/vendor/sabre/xml/lib/Element/Elements.php
@@ -0,0 +1,102 @@
+
+ *
+ *
+ *
+ *
+ * content
+ *
+ *
+ *
+ * Into:
+ *
+ * [
+ * "{http://sabredav.org/ns}elem1",
+ * "{http://sabredav.org/ns}elem2",
+ * "{http://sabredav.org/ns}elem3",
+ * "{http://sabredav.org/ns}elem4",
+ * "{http://sabredav.org/ns}elem5",
+ * ];
+ *
+ * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Elements implements Xml\Element
+{
+ /**
+ * Value to serialize.
+ *
+ * @var array
+ */
+ protected array $value;
+
+ /**
+ * Constructor.
+ *
+ * @param array $value
+ */
+ public function __construct(array $value = [])
+ {
+ $this->value = $value;
+ }
+
+ /**
+ * The xmlSerialize method is called during xml writing.
+ *
+ * Use the $writer argument to write its own xml serialization.
+ *
+ * An important note: do _not_ create a parent element. Any element
+ * implementing XmlSerializable should only ever write what's considered
+ * its 'inner xml'.
+ *
+ * The parent of the current element is responsible for writing a
+ * containing element.
+ *
+ * This allows serializers to be re-used for different element names.
+ *
+ * If you are opening new elements, you must also close them again.
+ */
+ public function xmlSerialize(Xml\Writer $writer): void
+ {
+ Serializer\enum($writer, $this->value);
+ }
+
+ /**
+ * The deserialize method is called during xml parsing.
+ *
+ * This method is called statically, this is because in theory this method
+ * may be used as a type of constructor, or factory method.
+ *
+ * Often you want to return an instance of the current class, but you are
+ * free to return other data as well.
+ *
+ * Important note 2: You are responsible for advancing the reader to the
+ * next element. Not doing anything will result in a never-ending loop.
+ *
+ * If you just want to skip parsing for this element altogether, you can
+ * just call $reader->next();
+ *
+ * $reader->parseSubTree() will parse the entire sub-tree, and advance to
+ * the next element.
+ *
+ * @return string[]
+ */
+ public static function xmlDeserialize(Xml\Reader $reader)
+ {
+ return Deserializer\enum($reader);
+ }
+}
diff --git a/vendor/sabre/xml/lib/Element/KeyValue.php b/vendor/sabre/xml/lib/Element/KeyValue.php
new file mode 100644
index 0000000..8e5ae05
--- /dev/null
+++ b/vendor/sabre/xml/lib/Element/KeyValue.php
@@ -0,0 +1,102 @@
+value struct.
+ *
+ * Attributes will be removed, and duplicate child elements are discarded.
+ * Complex values within the elements will be parsed by the 'standard' parser.
+ *
+ * For example, KeyValue will parse:
+ *
+ *
+ *
+ * value1
+ * value2
+ *
+ *
+ *
+ * Into:
+ *
+ * [
+ * "{http://sabredav.org/ns}elem1" => "value1",
+ * "{http://sabredav.org/ns}elem2" => "value2",
+ * "{http://sabredav.org/ns}elem3" => null,
+ * ];
+ *
+ * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class KeyValue implements Xml\Element
+{
+ /**
+ * Value to serialize.
+ *
+ * @var array
+ */
+ protected array $value;
+
+ /**
+ * Constructor.
+ *
+ * @param array $value
+ */
+ public function __construct(array $value = [])
+ {
+ $this->value = $value;
+ }
+
+ /**
+ * The xmlSerialize method is called during xml writing.
+ *
+ * Use the $writer argument to write its own xml serialization.
+ *
+ * An important note: do _not_ create a parent element. Any element
+ * implementing XmlSerializable should only ever write what's considered
+ * its 'inner xml'.
+ *
+ * The parent of the current element is responsible for writing a
+ * containing element.
+ *
+ * This allows serializers to be re-used for different element names.
+ *
+ * If you are opening new elements, you must also close them again.
+ */
+ public function xmlSerialize(Xml\Writer $writer): void
+ {
+ $writer->write($this->value);
+ }
+
+ /**
+ * The deserialize method is called during xml parsing.
+ *
+ * This method is called statically, this is because in theory this method
+ * may be used as a type of constructor, or factory method.
+ *
+ * Often you want to return an instance of the current class, but you are
+ * free to return other data as well.
+ *
+ * Important note 2: You are responsible for advancing the reader to the
+ * next element. Not doing anything will result in a never-ending loop.
+ *
+ * If you just want to skip parsing for this element altogether, you can
+ * just call $reader->next();
+ *
+ * $reader->parseInnerTree() will parse the entire sub-tree, and advance to
+ * the next element.
+ *
+ * @return array
+ */
+ public static function xmlDeserialize(Xml\Reader $reader)
+ {
+ return Deserializer\keyValue($reader);
+ }
+}
diff --git a/vendor/sabre/xml/lib/Element/Uri.php b/vendor/sabre/xml/lib/Element/Uri.php
new file mode 100644
index 0000000..e54d706
--- /dev/null
+++ b/vendor/sabre/xml/lib/Element/Uri.php
@@ -0,0 +1,95 @@
+/foo/bar
+ * http://example.org/hi
+ *
+ * If the uri is relative, it will be automatically expanded to an absolute
+ * url during writing and reading, if the contextUri property is set on the
+ * reader and/or writer.
+ *
+ * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Uri implements Xml\Element
+{
+ /**
+ * Uri element value.
+ */
+ protected string $value;
+
+ /**
+ * Constructor.
+ */
+ public function __construct(string $value)
+ {
+ $this->value = $value;
+ }
+
+ /**
+ * The xmlSerialize method is called during xml writing.
+ *
+ * Use the $writer argument to write its own xml serialization.
+ *
+ * An important note: do _not_ create a parent element. Any element
+ * implementing XmlSerializable should only ever write what's considered
+ * its 'inner xml'.
+ *
+ * The parent of the current element is responsible for writing a
+ * containing element.
+ *
+ * This allows serializers to be re-used for different element names.
+ *
+ * If you are opening new elements, you must also close them again.
+ */
+ public function xmlSerialize(Xml\Writer $writer): void
+ {
+ $writer->text(
+ resolve(
+ $writer->contextUri ?? '',
+ $this->value
+ )
+ );
+ }
+
+ /**
+ * This method is called during xml parsing.
+ *
+ * This method is called statically, this is because in theory this method
+ * may be used as a type of constructor, or factory method.
+ *
+ * Often you want to return an instance of the current class, but you are
+ * free to return other data as well.
+ *
+ * Important note 2: You are responsible for advancing the reader to the
+ * next element. Not doing anything will result in a never-ending loop.
+ *
+ * If you just want to skip parsing for this element altogether, you can
+ * just call $reader->next();
+ *
+ * $reader->parseSubTree() will parse the entire sub-tree, and advance to
+ * the next element.
+ */
+ public static function xmlDeserialize(Xml\Reader $reader)
+ {
+ return new self(
+ resolve(
+ (string) $reader->contextUri,
+ $reader->readText()
+ )
+ );
+ }
+}
diff --git a/vendor/sabre/xml/lib/Element/XmlFragment.php b/vendor/sabre/xml/lib/Element/XmlFragment.php
new file mode 100644
index 0000000..94f745b
--- /dev/null
+++ b/vendor/sabre/xml/lib/Element/XmlFragment.php
@@ -0,0 +1,144 @@
+xml = $xml;
+ }
+
+ /**
+ * Returns the inner XML document.
+ */
+ public function getXml(): string
+ {
+ return $this->xml;
+ }
+
+ /**
+ * The xmlSerialize method is called during xml writing.
+ *
+ * Use the $writer argument to write its own xml serialization.
+ *
+ * An important note: do _not_ create a parent element. Any element
+ * implementing XmlSerializable should only ever write what's considered
+ * its 'inner xml'.
+ *
+ * The parent of the current element is responsible for writing a
+ * containing element.
+ *
+ * This allows serializers to be re-used for different element names.
+ *
+ * If you are opening new elements, you must also close them again.
+ */
+ public function xmlSerialize(Writer $writer): void
+ {
+ $reader = new Reader();
+
+ // Wrapping the xml in a container, so root-less values can still be
+ // parsed.
+ $xml = <<
+{$this->getXml()}
+XML;
+
+ $reader->xml($xml);
+
+ while ($reader->read()) {
+ if ($reader->depth < 1) {
+ // Skipping the root node.
+ continue;
+ }
+
+ switch ($reader->nodeType) {
+ case Reader::ELEMENT:
+ $writer->startElement(
+ (string) $reader->getClark()
+ );
+ $empty = $reader->isEmptyElement;
+ while ($reader->moveToNextAttribute()) {
+ switch ($reader->namespaceURI) {
+ case '':
+ $writer->writeAttribute($reader->localName, $reader->value);
+ break;
+ case 'http://www.w3.org/2000/xmlns/':
+ // Skip namespace declarations
+ break;
+ default:
+ $writer->writeAttribute((string) $reader->getClark(), $reader->value);
+ break;
+ }
+ }
+ if ($empty) {
+ $writer->endElement();
+ }
+ break;
+ case Reader::CDATA:
+ case Reader::TEXT:
+ $writer->text(
+ $reader->value
+ );
+ break;
+ case Reader::END_ELEMENT:
+ $writer->endElement();
+ break;
+ }
+ }
+ }
+
+ /**
+ * The deserialize method is called during xml parsing.
+ *
+ * This method is called statically, this is because in theory this method
+ * may be used as a type of constructor, or factory method.
+ *
+ * Often you want to return an instance of the current class, but you are
+ * free to return other data as well.
+ *
+ * You are responsible for advancing the reader to the next element. Not
+ * doing anything will result in a never-ending loop.
+ *
+ * If you just want to skip parsing for this element altogether, you can
+ * just call $reader->next();
+ *
+ * $reader->parseInnerTree() will parse the entire sub-tree, and advance to
+ * the next element.
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $result = new self($reader->readInnerXml());
+ $reader->next();
+
+ return $result;
+ }
+}
diff --git a/vendor/sabre/xml/lib/LibXMLException.php b/vendor/sabre/xml/lib/LibXMLException.php
new file mode 100644
index 0000000..5c642a3
--- /dev/null
+++ b/vendor/sabre/xml/lib/LibXMLException.php
@@ -0,0 +1,49 @@
+errors = $errors;
+ parent::__construct($errors[0]->message.' on line '.$errors[0]->line.', column '.$errors[0]->column, $code, $previousException);
+ }
+
+ /**
+ * Returns the LibXML errors.
+ *
+ * @return \LibXMLError[]
+ */
+ public function getErrors(): array
+ {
+ return $this->errors;
+ }
+}
diff --git a/vendor/sabre/xml/lib/ParseException.php b/vendor/sabre/xml/lib/ParseException.php
new file mode 100644
index 0000000..ff6656b
--- /dev/null
+++ b/vendor/sabre/xml/lib/ParseException.php
@@ -0,0 +1,16 @@
+localName) {
+ return null;
+ }
+
+ return '{'.$this->namespaceURI.'}'.$this->localName;
+ }
+
+ /**
+ * Reads the entire document.
+ *
+ * This function returns an array with the following three elements:
+ * * name - The root element name.
+ * * value - The value for the root element.
+ * * attributes - An array of attributes.
+ *
+ * This function will also disable the standard libxml error handler (which
+ * usually just results in PHP errors), and throw exceptions instead.
+ *
+ * @return array
+ */
+ public function parse(): array
+ {
+ $previousEntityState = null;
+ $shouldCallLibxmlDisableEntityLoader = (\LIBXML_VERSION < 20900);
+ if ($shouldCallLibxmlDisableEntityLoader) {
+ $previousEntityState = libxml_disable_entity_loader(true);
+ }
+ $previousSetting = libxml_use_internal_errors(true);
+
+ try {
+ while (self::ELEMENT !== $this->nodeType) {
+ if (!$this->read()) {
+ $errors = libxml_get_errors();
+ libxml_clear_errors();
+ if ($errors) {
+ throw new LibXMLException($errors);
+ }
+ }
+ }
+ $result = $this->parseCurrentElement();
+
+ // last line of defense in case errors did occur above
+ $errors = libxml_get_errors();
+ libxml_clear_errors();
+ if ($errors) {
+ throw new LibXMLException($errors);
+ }
+ } finally {
+ libxml_use_internal_errors($previousSetting);
+ if ($shouldCallLibxmlDisableEntityLoader) {
+ libxml_disable_entity_loader($previousEntityState);
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * parseGetElements parses everything in the current sub-tree,
+ * and returns an array of elements.
+ *
+ * Each element has a 'name', 'value' and 'attributes' key.
+ *
+ * If the element didn't contain sub-elements, an empty array is always
+ * returned. If there was any text inside the element, it will be
+ * discarded.
+ *
+ * If the $elementMap argument is specified, the existing elementMap will
+ * be overridden while parsing the tree, and restored after this process.
+ *
+ * @param array|null $elementMap
+ *
+ * @return array>
+ */
+ public function parseGetElements(?array $elementMap = null): array
+ {
+ $result = $this->parseInnerTree($elementMap);
+ if (!is_array($result)) {
+ return [];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Parses all elements below the current element.
+ *
+ * This method will return a string if this was a text-node, or an array if
+ * there were sub-elements.
+ *
+ * If there's both text and sub-elements, the text will be discarded.
+ *
+ * If the $elementMap argument is specified, the existing elementMap will
+ * be overridden while parsing the tree, and restored after this process.
+ *
+ * @param array|null $elementMap
+ *
+ * @return array>|string|null
+ */
+ public function parseInnerTree(?array $elementMap = null)
+ {
+ $text = null;
+ $elements = [];
+
+ if (self::ELEMENT === $this->nodeType && $this->isEmptyElement) {
+ // Easy!
+ $this->next();
+
+ return null;
+ }
+
+ if (!is_null($elementMap)) {
+ $this->pushContext();
+ $this->elementMap = $elementMap;
+ }
+
+ try {
+ if (!$this->read()) {
+ $errors = libxml_get_errors();
+ libxml_clear_errors();
+ if ($errors) {
+ throw new LibXMLException($errors);
+ }
+ throw new ParseException('This should never happen (famous last words)');
+ }
+
+ $keepOnParsing = true;
+
+ while ($keepOnParsing) {
+ if (!$this->isValid()) {
+ $errors = libxml_get_errors();
+
+ if ($errors) {
+ libxml_clear_errors();
+ throw new LibXMLException($errors);
+ }
+ }
+
+ switch ($this->nodeType) {
+ case self::ELEMENT:
+ $elements[] = $this->parseCurrentElement();
+ break;
+ case self::TEXT:
+ case self::CDATA:
+ $text .= $this->value;
+ $this->read();
+ break;
+ case self::END_ELEMENT:
+ // Ensuring we are moving the cursor after the end element.
+ $this->read();
+ $keepOnParsing = false;
+ break;
+ case self::NONE:
+ throw new ParseException('We hit the end of the document prematurely. This likely means that some parser "eats" too many elements. Do not attempt to continue parsing.');
+ default:
+ // Advance to the next element
+ $this->read();
+ break;
+ }
+ }
+ } finally {
+ if (!is_null($elementMap)) {
+ $this->popContext();
+ }
+ }
+
+ return $elements ?: $text;
+ }
+
+ /**
+ * Reads all text below the current element, and returns this as a string.
+ */
+ public function readText(): string
+ {
+ $result = '';
+ $previousDepth = $this->depth;
+
+ while ($this->read() && $this->depth != $previousDepth) {
+ if (in_array($this->nodeType, [\XMLReader::TEXT, \XMLReader::CDATA, \XMLReader::WHITESPACE])) {
+ $result .= $this->value;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Parses the current XML element.
+ *
+ * This method returns arn array with 3 properties:
+ * * name - A clark-notation XML element name.
+ * * value - The parsed value.
+ * * attributes - A key-value list of attributes.
+ *
+ * @return array
+ */
+ public function parseCurrentElement(): array
+ {
+ $name = $this->getClark();
+
+ $attributes = [];
+
+ if ($this->hasAttributes) {
+ $attributes = $this->parseAttributes();
+ }
+
+ $value = call_user_func(
+ $this->getDeserializerForElementName((string) $name),
+ $this
+ );
+
+ return [
+ 'name' => $name,
+ 'value' => $value,
+ 'attributes' => $attributes,
+ ];
+ }
+
+ /**
+ * Grabs all the attributes from the current element, and returns them as a
+ * key-value array.
+ *
+ * If the attributes are part of the same namespace, they will simply be
+ * short keys. If they are defined on a different namespace, the attribute
+ * name will be returned in clark-notation.
+ *
+ * @return array
+ */
+ public function parseAttributes(): array
+ {
+ $attributes = [];
+
+ while ($this->moveToNextAttribute()) {
+ if ($this->namespaceURI) {
+ // Ignoring 'xmlns', it doesn't make any sense.
+ if ('http://www.w3.org/2000/xmlns/' === $this->namespaceURI) {
+ continue;
+ }
+
+ $name = $this->getClark();
+ $attributes[$name] = $this->value;
+ } else {
+ $attributes[$this->localName] = $this->value;
+ }
+ }
+ $this->moveToElement();
+
+ return $attributes;
+ }
+
+ /**
+ * Returns the function that should be used to parse the element identified
+ * by its clark-notation name.
+ */
+ public function getDeserializerForElementName(string $name): callable
+ {
+ if (!array_key_exists($name, $this->elementMap)) {
+ if ('{}' == substr($name, 0, 2) && array_key_exists(substr($name, 2), $this->elementMap)) {
+ $name = substr($name, 2);
+ } else {
+ return ['Sabre\\Xml\\Element\\Base', 'xmlDeserialize'];
+ }
+ }
+
+ $deserializer = $this->elementMap[$name];
+ if (is_callable($deserializer)) {
+ return $deserializer;
+ }
+
+ if (is_subclass_of($deserializer, 'Sabre\\Xml\\XmlDeserializable')) {
+ return [$deserializer, 'xmlDeserialize'];
+ }
+
+ $type = gettype($deserializer);
+ if (is_string($deserializer)) {
+ $type .= ' ('.$deserializer.')';
+ } elseif (is_object($deserializer)) {
+ $type .= ' ('.get_class($deserializer).')';
+ }
+ throw new \LogicException('Could not use this type as a deserializer: '.$type.' for element: '.$name);
+ }
+}
diff --git a/vendor/sabre/xml/lib/Serializer/functions.php b/vendor/sabre/xml/lib/Serializer/functions.php
new file mode 100644
index 0000000..a6105a9
--- /dev/null
+++ b/vendor/sabre/xml/lib/Serializer/functions.php
@@ -0,0 +1,207 @@
+
+ *
+ *
+ * content
+ *
+ *
+ * @param string[] $values
+ */
+function enum(Writer $writer, array $values): void
+{
+ foreach ($values as $value) {
+ $writer->writeElement($value);
+ }
+}
+
+/**
+ * The valueObject serializer turns a simple PHP object into a classname.
+ *
+ * Every public property will be encoded as an XML element with the same
+ * name, in the XML namespace as specified.
+ *
+ * Values that are set to null or an empty array are not serialized. To
+ * serialize empty properties, you must specify them as an empty string.
+ */
+function valueObject(Writer $writer, object $valueObject, string $namespace): void
+{
+ foreach (get_object_vars($valueObject) as $key => $val) {
+ if (is_array($val)) {
+ // If $val is an array, it has a special meaning. We need to
+ // generate one child element for each item in $val
+ foreach ($val as $child) {
+ $writer->writeElement('{'.$namespace.'}'.$key, $child);
+ }
+ } elseif (null !== $val) {
+ $writer->writeElement('{'.$namespace.'}'.$key, $val);
+ }
+ }
+}
+
+/**
+ * This serializer helps you serialize xml structures that look like
+ * this:.
+ *
+ *
+ * - ...
+ * - ...
+ * - ...
+ *
+ *
+ * In that previous example, this serializer just serializes the item element,
+ * and this could be called like this:
+ *
+ * repeatingElements($writer, $items, '{}item');
+ *
+ * @param array $items
+ */
+function repeatingElements(Writer $writer, array $items, string $childElementName): void
+{
+ foreach ($items as $item) {
+ $writer->writeElement($childElementName, $item);
+ }
+}
+
+/**
+ * This function is the 'default' serializer that is able to serialize most
+ * things, and delegates to other serializers if needed.
+ *
+ * The standardSerializer supports a wide-array of values.
+ *
+ * $value may be a string or integer, it will just write out the string as text.
+ * $value may be an instance of XmlSerializable or Element, in which case it
+ * calls it's xmlSerialize() method.
+ * $value may be a PHP callback/function/closure, in case we call the callback
+ * and give it the Writer as an argument.
+ * $value may be an object, and if it's in the classMap we automatically call
+ * the correct serializer for it.
+ * $value may be null, in which case we do nothing.
+ *
+ * If $value is an array, the array must look like this:
+ *
+ * [
+ * [
+ * 'name' => '{namespaceUri}element-name',
+ * 'value' => '...',
+ * 'attributes' => [ 'attName' => 'attValue' ]
+ * ]
+ * [,
+ * 'name' => '{namespaceUri}element-name2',
+ * 'value' => '...',
+ * ]
+ * ]
+ *
+ * This would result in xml like:
+ *
+ *
+ * ...
+ *
+ *
+ * ...
+ *
+ *
+ * The value property may be any value standardSerializer supports, so you can
+ * nest data-structures this way. Both value and attributes are optional.
+ *
+ * Alternatively, you can also specify the array using this syntax:
+ *
+ * [
+ * [
+ * '{namespaceUri}element-name' => '...',
+ * '{namespaceUri}element-name2' => '...',
+ * ]
+ * ]
+ *
+ * This is excellent for simple key->value structures, and here you can also
+ * specify anything for the value.
+ *
+ * You can even mix the two array syntaxes.
+ *
+ * @param string|int|float|bool|array|object $value
+ */
+function standardSerializer(Writer $writer, $value): void
+{
+ if (is_scalar($value)) {
+ // String, integer, float, boolean
+ $writer->text((string) $value);
+ } elseif ($value instanceof XmlSerializable) {
+ // XmlSerializable classes or Element classes.
+ $value->xmlSerialize($writer);
+ } elseif (is_object($value) && isset($writer->classMap[get_class($value)])) {
+ // It's an object which class appears in the classmap.
+ $writer->classMap[get_class($value)]($writer, $value);
+ } elseif (is_callable($value)) {
+ // A callback
+ $value($writer);
+ } elseif (is_array($value) && array_key_exists('name', $value)) {
+ // if the array had a 'name' element, we assume that this array
+ // describes a 'name' and optionally 'attributes' and 'value'.
+
+ $name = $value['name'];
+ $attributes = isset($value['attributes']) ? $value['attributes'] : [];
+ $value = isset($value['value']) ? $value['value'] : null;
+
+ $writer->startElement($name);
+ $writer->writeAttributes($attributes);
+ $writer->write($value);
+ $writer->endElement();
+ } elseif (is_array($value)) {
+ foreach ($value as $name => $item) {
+ if (is_int($name)) {
+ // This item has a numeric index. We just loop through the
+ // array and throw it back in the writer.
+ standardSerializer($writer, $item);
+ } elseif (is_string($name) && is_array($item) && isset($item['attributes'])) {
+ // The key is used for a name, but $item has 'attributes' and
+ // possibly 'value'
+ $writer->startElement($name);
+ $writer->writeAttributes($item['attributes']);
+ if (isset($item['value'])) {
+ $writer->write($item['value']);
+ }
+ $writer->endElement();
+ } elseif (is_string($name)) {
+ // This was a plain key-value array.
+ $writer->startElement($name);
+ $writer->write($item);
+ $writer->endElement();
+ } else {
+ throw new \InvalidArgumentException('The writer does not know how to serialize arrays with keys of type: '.gettype($name));
+ }
+ }
+ } elseif (is_object($value)) {
+ throw new \InvalidArgumentException('The writer cannot serialize objects of class: '.get_class($value));
+ } elseif (!is_null($value)) {
+ throw new \InvalidArgumentException('The writer cannot serialize values of type: '.gettype($value));
+ }
+}
diff --git a/vendor/sabre/xml/lib/Service.php b/vendor/sabre/xml/lib/Service.php
new file mode 100644
index 0000000..ace923f
--- /dev/null
+++ b/vendor/sabre/xml/lib/Service.php
@@ -0,0 +1,326 @@
+
+ */
+ public array $elementMap = [];
+
+ /**
+ * This is a list of namespaces that you want to give default prefixes.
+ *
+ * You must make sure you create this entire list before starting to write.
+ * They should be registered on the root element.
+ *
+ * @phpstan-var array
+ */
+ public array $namespaceMap = [];
+
+ /**
+ * This is a list of custom serializers for specific classes.
+ *
+ * The writer may use this if you attempt to serialize an object with a
+ * class that does not implement XmlSerializable.
+ *
+ * Instead, it will look at this classmap to see if there is a custom
+ * serializer here. This is useful if you don't want your value objects
+ * to be responsible for serializing themselves.
+ *
+ * The keys in this classmap need to be fully qualified PHP class names,
+ * the values must be callbacks. The callbacks take two arguments. The
+ * writer class, and the value that must be written.
+ *
+ * function (Writer $writer, object $value)
+ *
+ * @phpstan-var array
+ */
+ public array $classMap = [];
+
+ /**
+ * A bitmask of the LIBXML_* constants.
+ */
+ public int $options = 0;
+
+ /**
+ * Returns a fresh XML Reader.
+ */
+ public function getReader(): Reader
+ {
+ $r = new Reader();
+ $r->elementMap = $this->elementMap;
+
+ return $r;
+ }
+
+ /**
+ * Returns a fresh xml writer.
+ */
+ public function getWriter(): Writer
+ {
+ $w = new Writer();
+ $w->namespaceMap = $this->namespaceMap;
+ $w->classMap = $this->classMap;
+
+ return $w;
+ }
+
+ /**
+ * Parses a document in full.
+ *
+ * Input may be specified as a string or readable stream resource.
+ * The returned value is the value of the root document.
+ *
+ * Specifying the $contextUri allows the parser to figure out what the URI
+ * of the document was. This allows relative URIs within the document to be
+ * expanded easily.
+ *
+ * The $rootElementName is specified by reference and will be populated
+ * with the root element name of the document.
+ *
+ * @param string|resource $input
+ *
+ * @return array|object|string
+ *
+ * @throws ParseException
+ */
+ public function parse($input, ?string $contextUri = null, ?string &$rootElementName = null)
+ {
+ if (!is_string($input)) {
+ // Unfortunately the XMLReader doesn't support streams. When it
+ // does, we can optimize this.
+ if (is_resource($input)) {
+ $input = (string) stream_get_contents($input);
+ } else {
+ // Input is not a string and not a resource.
+ // Therefore, it has to be a closed resource.
+ // Effectively empty input has been passed in.
+ $input = '';
+ }
+ }
+
+ // If input is empty, then it's safe to throw an exception
+ if (empty($input)) {
+ throw new ParseException('The input element to parse is empty. Do not attempt to parse');
+ }
+
+ $r = $this->getReader();
+ $r->contextUri = $contextUri;
+ $r->XML($input, null, $this->options);
+
+ $result = $r->parse();
+ $rootElementName = $result['name'];
+
+ return $result['value'];
+ }
+
+ /**
+ * Parses a document in full, and specify what the expected root element
+ * name is.
+ *
+ * This function works similar to parse, but the difference is that the
+ * user can specify what the expected name of the root element should be,
+ * in clark notation.
+ *
+ * This is useful in cases where you expected a specific document to be
+ * passed, and reduces the amount of if statements.
+ *
+ * It's also possible to pass an array of expected rootElements if your
+ * code may expect more than one document type.
+ *
+ * @param string|string[] $rootElementName
+ * @param string|resource $input
+ *
+ * @return array|object|string
+ *
+ * @throws ParseException
+ */
+ public function expect($rootElementName, $input, ?string $contextUri = null)
+ {
+ if (!is_string($input)) {
+ // Unfortunately the XMLReader doesn't support streams. When it
+ // does, we can optimize this.
+ if (is_resource($input)) {
+ $input = (string) stream_get_contents($input);
+ } else {
+ // Input is not a string and not a resource.
+ // Therefore, it has to be a closed resource.
+ // Effectively empty input has been passed in.
+ $input = '';
+ }
+ }
+
+ // If input is empty, then it's safe to throw an exception
+ if (empty($input)) {
+ throw new ParseException('The input element to parse is empty. Do not attempt to parse');
+ }
+
+ $r = $this->getReader();
+ $r->contextUri = $contextUri;
+ $r->XML($input, null, $this->options);
+
+ $rootElementName = (array) $rootElementName;
+
+ foreach ($rootElementName as &$rEl) {
+ if ('{' !== $rEl[0]) {
+ $rEl = '{}'.$rEl;
+ }
+ }
+
+ $result = $r->parse();
+ if (!in_array($result['name'], $rootElementName, true)) {
+ throw new ParseException('Expected '.implode(' or ', $rootElementName).' but received '.$result['name'].' as the root element');
+ }
+
+ return $result['value'];
+ }
+
+ /**
+ * Generates an XML document in one go.
+ *
+ * The $rootElement must be specified in clark notation.
+ * The value must be a string, an array or an object implementing
+ * XmlSerializable. Basically, anything that's supported by the Writer
+ * object.
+ *
+ * $contextUri can be used to specify a sort of 'root' of the PHP application,
+ * in case the xml document is used as a http response.
+ *
+ * This allows an implementor to easily create URI's relative to the root
+ * of the domain.
+ *
+ * @param string|array|object|XmlSerializable $value
+ */
+ public function write(string $rootElementName, $value, ?string $contextUri = null): string
+ {
+ $w = $this->getWriter();
+ $w->openMemory();
+ $w->contextUri = $contextUri;
+ $w->setIndent(true);
+ $w->startDocument();
+ $w->writeElement($rootElementName, $value);
+
+ return $w->outputMemory();
+ }
+
+ /**
+ * Map an XML element to a PHP class.
+ *
+ * Calling this function will automatically set up the Reader and Writer
+ * classes to turn a specific XML element to a PHP class.
+ *
+ * For example, given a class such as :
+ *
+ * class Author {
+ * public $firstName;
+ * public $lastName;
+ * }
+ *
+ * and an XML element such as:
+ *
+ *
+ * ...
+ * ...
+ *
+ *
+ * These can easily be mapped by calling:
+ *
+ * $service->mapValueObject('{http://example.org}author', 'Author');
+ *
+ * @param class-string $className
+ */
+ public function mapValueObject(string $elementName, string $className): void
+ {
+ list($namespace) = self::parseClarkNotation($elementName);
+
+ $this->elementMap[$elementName] = function (Reader $reader) use ($className, $namespace) {
+ return \Sabre\Xml\Deserializer\valueObject($reader, $className, $namespace);
+ };
+ $this->classMap[$className] = function (Writer $writer, $valueObject) use ($namespace) {
+ \Sabre\Xml\Serializer\valueObject($writer, $valueObject, $namespace);
+ };
+ $this->valueObjectMap[$className] = $elementName;
+ }
+
+ /**
+ * Writes a value object.
+ *
+ * This function largely behaves similar to write(), except that it's
+ * intended specifically to serialize a Value Object into an XML document.
+ *
+ * The ValueObject must have been previously registered using
+ * mapValueObject().
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function writeValueObject(object $object, ?string $contextUri = null): string
+ {
+ if (!isset($this->valueObjectMap[get_class($object)])) {
+ throw new \InvalidArgumentException('"'.get_class($object).'" is not a registered value object class. Register your class with mapValueObject.');
+ }
+
+ return $this->write(
+ $this->valueObjectMap[get_class($object)],
+ $object,
+ $contextUri
+ );
+ }
+
+ /**
+ * Parses a clark-notation string, and returns the namespace and element
+ * name components.
+ *
+ * If the string was invalid, it will throw an InvalidArgumentException.
+ *
+ * @return array{string, string}
+ *
+ * @throws \InvalidArgumentException
+ */
+ public static function parseClarkNotation(string $str): array
+ {
+ static $cache = [];
+
+ if (!isset($cache[$str])) {
+ if (!preg_match('/^{([^}]*)}(.*)$/', $str, $matches)) {
+ throw new \InvalidArgumentException('\''.$str.'\' is not a valid clark-notation formatted string');
+ }
+
+ $cache[$str] = [
+ $matches[1],
+ $matches[2],
+ ];
+ }
+
+ return $cache[$str];
+ }
+
+ /**
+ * A list of classes and which XML elements they map to.
+ *
+ * @var array
+ */
+ protected array $valueObjectMap = [];
+}
diff --git a/vendor/sabre/xml/lib/Version.php b/vendor/sabre/xml/lib/Version.php
new file mode 100644
index 0000000..3403931
--- /dev/null
+++ b/vendor/sabre/xml/lib/Version.php
@@ -0,0 +1,20 @@
+
+ */
+ protected array $adhocNamespaces = [];
+
+ /**
+ * When the first element is written, this flag is set to true.
+ *
+ * This ensures that the namespaces in the namespaces map are only written
+ * once.
+ */
+ protected bool $namespacesWritten = false;
+
+ /**
+ * Writes a value to the output stream.
+ *
+ * The following values are supported:
+ * 1. Scalar values will be written as-is, as text.
+ * 2. Null values will be skipped (resulting in a short xml tag).
+ * 3. If a value is an instance of an Element class, writing will be
+ * delegated to the object.
+ * 4. If a value is an array, two formats are supported.
+ *
+ * Array format 1:
+ * [
+ * "{namespace}name1" => "..",
+ * "{namespace}name2" => "..",
+ * ]
+ *
+ * One element will be created for each key in this array. The values of
+ * this array support any format this method supports (this method is
+ * called recursively).
+ *
+ * Array format 2:
+ *
+ * [
+ * [
+ * "name" => "{namespace}name1"
+ * "value" => "..",
+ * "attributes" => [
+ * "attr" => "attribute value",
+ * ]
+ * ],
+ * [
+ * "name" => "{namespace}name1"
+ * "value" => "..",
+ * "attributes" => [
+ * "attr" => "attribute value",
+ * ]
+ * ]
+ * ]
+ *
+ * @param mixed $value PHP value to be written
+ */
+ public function write($value): void
+ {
+ Serializer\standardSerializer($this, $value);
+ }
+
+ /**
+ * Opens a new element.
+ *
+ * You can either just use a local element name, or you can use clark-
+ * notation to start a new element.
+ *
+ * Example:
+ *
+ * $writer->startElement('{http://www.w3.org/2005/Atom}entry');
+ *
+ * Would result in something like:
+ *
+ *
+ *
+ * Note: this function doesn't have the string typehint, because PHP's
+ * XMLWriter::startElement doesn't either.
+ * From PHP 8.0 the typehint exists, so it can be added here after PHP 7.4 is dropped.
+ *
+ * @param string $name
+ */
+ public function startElement($name): bool
+ {
+ if ('{' === $name[0]) {
+ list($namespace, $localName) =
+ Service::parseClarkNotation($name);
+
+ if (array_key_exists($namespace, $this->namespaceMap)) {
+ $result = $this->startElementNS(
+ '' === $this->namespaceMap[$namespace] ? null : $this->namespaceMap[$namespace],
+ $localName,
+ null
+ );
+ } else {
+ // An empty namespace means it's the global namespace. This is
+ // allowed, but it mustn't get a prefix.
+ if ('' === $namespace) {
+ $result = $this->startElement($localName);
+ $this->writeAttribute('xmlns', '');
+ } else {
+ if (!isset($this->adhocNamespaces[$namespace])) {
+ $this->adhocNamespaces[$namespace] = 'x'.(count($this->adhocNamespaces) + 1);
+ }
+ $result = $this->startElementNS($this->adhocNamespaces[$namespace], $localName, $namespace);
+ }
+ }
+ } else {
+ $result = parent::startElement($name);
+ }
+
+ if (!$this->namespacesWritten) {
+ foreach ($this->namespaceMap as $namespace => $prefix) {
+ $this->writeAttribute($prefix ? 'xmlns:'.$prefix : 'xmlns', $namespace);
+ }
+ $this->namespacesWritten = true;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Write a full element tag and it's contents.
+ *
+ * This method automatically closes the element as well.
+ *
+ * The element name may be specified in clark-notation.
+ *
+ * Examples:
+ *
+ * $writer->writeElement('{http://www.w3.org/2005/Atom}author',null);
+ * becomes:
+ *
+ *
+ * $writer->writeElement('{http://www.w3.org/2005/Atom}author', [
+ * '{http://www.w3.org/2005/Atom}name' => 'Evert Pot',
+ * ]);
+ * becomes:
+ * Evert Pot
+ *
+ * Note: this function doesn't have the string typehint, because PHP's
+ * XMLWriter::startElement doesn't either.
+ * From PHP 8.0 the typehint exists, so it can be added here after PHP 7.4 is dropped.
+ *
+ * @param string $name
+ * @param array|string|object|null $content
+ */
+ public function writeElement($name, $content = null): bool
+ {
+ $this->startElement($name);
+ if (!is_null($content)) {
+ $this->write($content);
+ }
+ $this->endElement();
+
+ return true;
+ }
+
+ /**
+ * Writes a list of attributes.
+ *
+ * Attributes are specified as a key->value array.
+ *
+ * The key is an attribute name. If the key is a 'localName', the current
+ * xml namespace is assumed. If it's a 'clark notation key', this namespace
+ * will be used instead.
+ *
+ * @param array $attributes
+ */
+ public function writeAttributes(array $attributes): void
+ {
+ foreach ($attributes as $name => $value) {
+ $this->writeAttribute($name, $value);
+ }
+ }
+
+ /**
+ * Writes a new attribute.
+ *
+ * The name may be specified in clark-notation.
+ *
+ * Returns true when successful.
+ *
+ * Note: this function doesn't have typehints, because for some reason
+ * PHP's XMLWriter::writeAttribute doesn't either.
+ * From PHP 8.0 the typehint exists, so it can be added here after PHP 7.4 is dropped.
+ *
+ * @param string $name
+ * @param string $value
+ */
+ public function writeAttribute($name, $value): bool
+ {
+ if ('{' !== $name[0]) {
+ return parent::writeAttribute($name, $value);
+ }
+
+ list(
+ $namespace,
+ $localName,
+ ) = Service::parseClarkNotation($name);
+
+ if (array_key_exists($namespace, $this->namespaceMap)) {
+ // It's an attribute with a namespace we know
+ return $this->writeAttribute(
+ $this->namespaceMap[$namespace].':'.$localName,
+ $value
+ );
+ }
+
+ // We don't know the namespace, we must add it in-line
+ if (!isset($this->adhocNamespaces[$namespace])) {
+ $this->adhocNamespaces[$namespace] = 'x'.(count($this->adhocNamespaces) + 1);
+ }
+
+ return $this->writeAttributeNS(
+ $this->adhocNamespaces[$namespace],
+ $localName,
+ $namespace,
+ $value
+ );
+ }
+}
diff --git a/vendor/sabre/xml/lib/XmlDeserializable.php b/vendor/sabre/xml/lib/XmlDeserializable.php
new file mode 100644
index 0000000..56448d1
--- /dev/null
+++ b/vendor/sabre/xml/lib/XmlDeserializable.php
@@ -0,0 +1,38 @@
+next();
+ *
+ * $reader->parseInnerTree() will parse the entire sub-tree, and advance to
+ * the next element.
+ *
+ * @return mixed see comments above
+ */
+ public static function xmlDeserialize(Reader $reader);
+}
diff --git a/vendor/sabre/xml/lib/XmlSerializable.php b/vendor/sabre/xml/lib/XmlSerializable.php
new file mode 100644
index 0000000..4cd4e7f
--- /dev/null
+++ b/vendor/sabre/xml/lib/XmlSerializable.php
@@ -0,0 +1,34 @@
+