';
+
+ /**
+ * https://www.kanzaki.com/docs/ical/summary.html
+ *
+ * @var string
+ */
+ public $summary;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/dtstart.html
+ *
+ * @var string
+ */
+ public $dtstart;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/dtend.html
+ *
+ * @var string
+ */
+ public $dtend;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/duration.html
+ *
+ * @var string|null
+ */
+ public $duration;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/dtstamp.html
+ *
+ * @var string
+ */
+ public $dtstamp;
+
+ /**
+ * When the event starts, represented as a timezone-adjusted string
+ *
+ * @var string
+ */
+ public $dtstart_tz;
+
+ /**
+ * When the event ends, represented as a timezone-adjusted string
+ *
+ * @var string
+ */
+ public $dtend_tz;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/uid.html
+ *
+ * @var string
+ */
+ public $uid;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/created.html
+ *
+ * @var string
+ */
+ public $created;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/lastModified.html
+ *
+ * @var string
+ */
+ public $last_modified;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/description.html
+ *
+ * @var string|null
+ */
+ public $description;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/location.html
+ *
+ * @var string|null
+ */
+ public $location;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/sequence.html
+ *
+ * @var string
+ */
+ public $sequence;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/status.html
+ *
+ * @var string
+ */
+ public $status;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/transp.html
+ *
+ * @var string
+ */
+ public $transp;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/organizer.html
+ *
+ * @var string
+ */
+ public $organizer;
+
+ /**
+ * https://www.kanzaki.com/docs/ical/attendee.html
+ *
+ * @var string
+ */
+ public $attendee;
+
+ /**
+ * Manage additional properties
+ *
+ * @var array
+ */
+ public $additionalProperties = array();
+
+ /**
+ * Creates the Event object
+ *
+ * @param array $data
+ * @return void
+ */
+ public function __construct(array $data = array())
+ {
+ foreach ($data as $key => $value) {
+ $variable = self::snakeCase($key);
+ if (property_exists($this, $variable)) {
+ $this->{$variable} = $this->prepareData($value);
+ } else {
+ $this->additionalProperties[$variable] = $this->prepareData($value);
+ }
+ }
+ }
+
+ /**
+ * Magic getter method
+ *
+ * @param string $additionalPropertyName
+ * @return mixed
+ */
+ public function __get($additionalPropertyName)
+ {
+ if (array_key_exists($additionalPropertyName, $this->additionalProperties)) {
+ return $this->additionalProperties[$additionalPropertyName];
+ }
+
+ return null;
+ }
+
+ /**
+ * Magic isset method
+ *
+ * @param string $name
+ * @return boolean
+ */
+ public function __isset($name)
+ {
+ return is_null($this->$name) === false;
+ }
+
+ /**
+ * Prepares the data for output
+ *
+ * @param mixed $value
+ * @return mixed
+ */
+ protected function prepareData($value)
+ {
+ if (is_string($value)) {
+ return stripslashes(trim(str_replace('\n', "\n", $value)));
+ }
+
+ if (is_array($value)) {
+ return array_map(function ($value) {
+ return $this->prepareData($value);
+ }, $value);
+ }
+
+ return $value;
+ }
+
+ /**
+ * Returns Event data excluding anything blank
+ * within an HTML template
+ *
+ * @param string $html HTML template to use
+ * @return string
+ */
+ public function printData($html = self::HTML_TEMPLATE)
+ {
+ $data = array(
+ 'SUMMARY' => $this->summary,
+ 'DTSTART' => $this->dtstart,
+ 'DTEND' => $this->dtend,
+ 'DTSTART_TZ' => $this->dtstart_tz,
+ 'DTEND_TZ' => $this->dtend_tz,
+ 'DURATION' => $this->duration,
+ 'DTSTAMP' => $this->dtstamp,
+ 'UID' => $this->uid,
+ 'CREATED' => $this->created,
+ 'LAST-MODIFIED' => $this->last_modified,
+ 'DESCRIPTION' => $this->description,
+ 'LOCATION' => $this->location,
+ 'SEQUENCE' => $this->sequence,
+ 'STATUS' => $this->status,
+ 'TRANSP' => $this->transp,
+ 'ORGANISER' => $this->organizer,
+ 'ATTENDEE(S)' => $this->attendee,
+ );
+
+ // Remove any blank values
+ $data = array_filter($data);
+
+ $output = '';
+
+ foreach ($data as $key => $value) {
+ $output .= sprintf($html, $key, $value);
+ }
+
+ return $output;
+ }
+
+ /**
+ * Converts the given input to snake_case
+ *
+ * @param string $input
+ * @param string $glue
+ * @param string $separator
+ * @return string
+ */
+ protected static function snakeCase($input, $glue = '_', $separator = '-')
+ {
+ $inputSplit = preg_split('/(?<=[a-z])(?=[A-Z])/x', $input);
+
+ if ($inputSplit === false) {
+ return $input;
+ }
+
+ $inputSplit = implode($glue, $inputSplit);
+ $inputSplit = str_replace($separator, $glue, $inputSplit);
+
+ return strtolower($inputSplit);
+ }
+}
diff --git a/lib/composer/vendor/johngrogg/ics-parser/src/ICal/ICal.php b/lib/composer/vendor/johngrogg/ics-parser/src/ICal/ICal.php
new file mode 100644
index 0000000..f4aab16
--- /dev/null
+++ b/lib/composer/vendor/johngrogg/ics-parser/src/ICal/ICal.php
@@ -0,0 +1,2731 @@
+
+ * @license https://opensource.org/licenses/mit-license.php MIT License
+ * @version 3.2.0
+ */
+
+namespace ICal;
+
+class ICal
+{
+ // phpcs:disable Generic.Arrays.DisallowLongArraySyntax
+
+ const DATE_TIME_FORMAT = 'Ymd\THis';
+ const DATE_TIME_FORMAT_PRETTY = 'F Y H:i:s';
+ const ICAL_DATE_TIME_TEMPLATE = 'TZID=%s:';
+ const ISO_8601_WEEK_START = 'MO';
+ const RECURRENCE_EVENT = 'Generated recurrence event';
+ const SECONDS_IN_A_WEEK = 604800;
+ const TIME_FORMAT = 'His';
+ const TIME_ZONE_UTC = 'UTC';
+ const UNIX_FORMAT = 'U';
+ const UNIX_MIN_YEAR = 1970;
+
+ /**
+ * Tracks the number of alarms in the current iCal feed
+ *
+ * @var integer
+ */
+ public $alarmCount = 0;
+
+ /**
+ * Tracks the number of events in the current iCal feed
+ *
+ * @var integer
+ */
+ public $eventCount = 0;
+
+ /**
+ * Tracks the free/busy count in the current iCal feed
+ *
+ * @var integer
+ */
+ public $freeBusyCount = 0;
+
+ /**
+ * Tracks the number of todos in the current iCal feed
+ *
+ * @var integer
+ */
+ public $todoCount = 0;
+
+ /**
+ * The value in years to use for indefinite, recurring events
+ *
+ * @var integer
+ */
+ public $defaultSpan = 2;
+
+ /**
+ * Enables customisation of the default time zone
+ *
+ * @var string|null
+ */
+ public $defaultTimeZone;
+
+ /**
+ * The two letter representation of the first day of the week
+ *
+ * @var string
+ */
+ public $defaultWeekStart = self::ISO_8601_WEEK_START;
+
+ /**
+ * Toggles whether to skip the parsing of recurrence rules
+ *
+ * @var boolean
+ */
+ public $skipRecurrence = false;
+
+ /**
+ * Toggles whether to disable all character replacement.
+ *
+ * @var boolean
+ */
+ public $disableCharacterReplacement = false;
+
+ /**
+ * If this value is an integer, the parser will ignore all events more than roughly this many days before now.
+ * If this value is a date, the parser will ignore all events occurring before this date.
+ *
+ * @var \DateTimeInterface|integer|null
+ */
+ public $filterDaysBefore;
+
+ /**
+ * If this value is an integer, the parser will ignore all events more than roughly this many days after now.
+ * If this value is a date, the parser will ignore all events occurring after this date.
+ *
+ * @var \DateTimeInterface|integer|null
+ */
+ public $filterDaysAfter;
+
+ /**
+ * The parsed calendar
+ *
+ * @var array
+ */
+ public $cal = array();
+
+ /**
+ * Tracks the VFREEBUSY component
+ *
+ * @var integer
+ */
+ protected $freeBusyIndex = 0;
+
+ /**
+ * Variable to track the previous keyword
+ *
+ * @var string
+ */
+ protected $lastKeyword;
+
+ /**
+ * Cache valid IANA time zone IDs to avoid unnecessary lookups
+ *
+ * @var array
+ */
+ protected $validIanaTimeZones = array();
+
+ /**
+ * Event recurrence instances that have been altered
+ *
+ * @var array
+ */
+ protected $alteredRecurrenceInstances = array();
+
+ /**
+ * An associative array containing weekday conversion data
+ *
+ * The order of the days in the array follow the ISO-8601 specification of a week.
+ *
+ * @var array
+ */
+ protected $weekdays = array(
+ 'MO' => 'monday',
+ 'TU' => 'tuesday',
+ 'WE' => 'wednesday',
+ 'TH' => 'thursday',
+ 'FR' => 'friday',
+ 'SA' => 'saturday',
+ 'SU' => 'sunday',
+ );
+
+ /**
+ * An associative array containing frequency conversion terms
+ *
+ * @var array
+ */
+ protected $frequencyConversion = array(
+ 'DAILY' => 'day',
+ 'WEEKLY' => 'week',
+ 'MONTHLY' => 'month',
+ 'YEARLY' => 'year',
+ );
+
+ /**
+ * Holds the username and password for HTTP basic authentication
+ *
+ * @var array
+ */
+ protected $httpBasicAuth = array();
+
+ /**
+ * Holds the custom User Agent string header
+ *
+ * @var string
+ */
+ protected $httpUserAgent;
+
+ /**
+ * Holds the custom Accept Language string header
+ *
+ * @var string
+ */
+ protected $httpAcceptLanguage;
+
+ /**
+ * Holds the custom HTTP Protocol version
+ *
+ * @var string
+ */
+ protected $httpProtocolVersion;
+
+ /**
+ * Define which variables can be configured
+ *
+ * @var array
+ */
+ private static $configurableOptions = array(
+ 'defaultSpan',
+ 'defaultTimeZone',
+ 'defaultWeekStart',
+ 'disableCharacterReplacement',
+ 'filterDaysAfter',
+ 'filterDaysBefore',
+ 'httpUserAgent',
+ 'skipRecurrence',
+ );
+
+ /**
+ * CLDR time zones mapped to IANA time zones.
+ *
+ * @var array
+ */
+ private static $cldrTimeZonesMap = array(
+ '(UTC-12:00) International Date Line West' => 'Etc/GMT+12',
+ '(UTC-11:00) Coordinated Universal Time-11' => 'Etc/GMT+11',
+ '(UTC-10:00) Hawaii' => 'Pacific/Honolulu',
+ '(UTC-09:00) Alaska' => 'America/Anchorage',
+ '(UTC-08:00) Pacific Time (US & Canada)' => 'America/Los_Angeles',
+ '(UTC-07:00) Arizona' => 'America/Phoenix',
+ '(UTC-07:00) Chihuahua, La Paz, Mazatlan' => 'America/Chihuahua',
+ '(UTC-07:00) Mountain Time (US & Canada)' => 'America/Denver',
+ '(UTC-06:00) Central America' => 'America/Guatemala',
+ '(UTC-06:00) Central Time (US & Canada)' => 'America/Chicago',
+ '(UTC-06:00) Guadalajara, Mexico City, Monterrey' => 'America/Mexico_City',
+ '(UTC-06:00) Saskatchewan' => 'America/Regina',
+ '(UTC-05:00) Bogota, Lima, Quito, Rio Branco' => 'America/Bogota',
+ '(UTC-05:00) Chetumal' => 'America/Cancun',
+ '(UTC-05:00) Eastern Time (US & Canada)' => 'America/New_York',
+ '(UTC-05:00) Indiana (East)' => 'America/Indianapolis',
+ '(UTC-04:00) Asuncion' => 'America/Asuncion',
+ '(UTC-04:00) Atlantic Time (Canada)' => 'America/Halifax',
+ '(UTC-04:00) Caracas' => 'America/Caracas',
+ '(UTC-04:00) Cuiaba' => 'America/Cuiaba',
+ '(UTC-04:00) Georgetown, La Paz, Manaus, San Juan' => 'America/La_Paz',
+ '(UTC-04:00) Santiago' => 'America/Santiago',
+ '(UTC-03:30) Newfoundland' => 'America/St_Johns',
+ '(UTC-03:00) Brasilia' => 'America/Sao_Paulo',
+ '(UTC-03:00) Cayenne, Fortaleza' => 'America/Cayenne',
+ '(UTC-03:00) City of Buenos Aires' => 'America/Buenos_Aires',
+ '(UTC-03:00) Greenland' => 'America/Godthab',
+ '(UTC-03:00) Montevideo' => 'America/Montevideo',
+ '(UTC-03:00) Salvador' => 'America/Bahia',
+ '(UTC-02:00) Coordinated Universal Time-02' => 'Etc/GMT+2',
+ '(UTC-01:00) Azores' => 'Atlantic/Azores',
+ '(UTC-01:00) Cabo Verde Is.' => 'Atlantic/Cape_Verde',
+ '(UTC) Coordinated Universal Time' => 'Etc/GMT',
+ '(UTC+00:00) Casablanca' => 'Africa/Casablanca',
+ '(UTC+00:00) Dublin, Edinburgh, Lisbon, London' => 'Europe/London',
+ '(UTC+00:00) Monrovia, Reykjavik' => 'Atlantic/Reykjavik',
+ '(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna' => 'Europe/Berlin',
+ '(UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague' => 'Europe/Budapest',
+ '(UTC+01:00) Brussels, Copenhagen, Madrid, Paris' => 'Europe/Paris',
+ '(UTC+01:00) Sarajevo, Skopje, Warsaw, Zagreb' => 'Europe/Warsaw',
+ '(UTC+01:00) West Central Africa' => 'Africa/Lagos',
+ '(UTC+02:00) Amman' => 'Asia/Amman',
+ '(UTC+02:00) Athens, Bucharest' => 'Europe/Bucharest',
+ '(UTC+02:00) Beirut' => 'Asia/Beirut',
+ '(UTC+02:00) Cairo' => 'Africa/Cairo',
+ '(UTC+02:00) Chisinau' => 'Europe/Chisinau',
+ '(UTC+02:00) Damascus' => 'Asia/Damascus',
+ '(UTC+02:00) Harare, Pretoria' => 'Africa/Johannesburg',
+ '(UTC+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius' => 'Europe/Kiev',
+ '(UTC+02:00) Jerusalem' => 'Asia/Jerusalem',
+ '(UTC+02:00) Kaliningrad' => 'Europe/Kaliningrad',
+ '(UTC+02:00) Tripoli' => 'Africa/Tripoli',
+ '(UTC+02:00) Windhoek' => 'Africa/Windhoek',
+ '(UTC+03:00) Baghdad' => 'Asia/Baghdad',
+ '(UTC+03:00) Istanbul' => 'Europe/Istanbul',
+ '(UTC+03:00) Kuwait, Riyadh' => 'Asia/Riyadh',
+ '(UTC+03:00) Minsk' => 'Europe/Minsk',
+ '(UTC+03:00) Moscow, St. Petersburg, Volgograd' => 'Europe/Moscow',
+ '(UTC+03:00) Nairobi' => 'Africa/Nairobi',
+ '(UTC+03:30) Tehran' => 'Asia/Tehran',
+ '(UTC+04:00) Abu Dhabi, Muscat' => 'Asia/Dubai',
+ '(UTC+04:00) Baku' => 'Asia/Baku',
+ '(UTC+04:00) Izhevsk, Samara' => 'Europe/Samara',
+ '(UTC+04:00) Port Louis' => 'Indian/Mauritius',
+ '(UTC+04:00) Tbilisi' => 'Asia/Tbilisi',
+ '(UTC+04:00) Yerevan' => 'Asia/Yerevan',
+ '(UTC+04:30) Kabul' => 'Asia/Kabul',
+ '(UTC+05:00) Ashgabat, Tashkent' => 'Asia/Tashkent',
+ '(UTC+05:00) Ekaterinburg' => 'Asia/Yekaterinburg',
+ '(UTC+05:00) Islamabad, Karachi' => 'Asia/Karachi',
+ '(UTC+05:30) Chennai, Kolkata, Mumbai, New Delhi' => 'Asia/Calcutta',
+ '(UTC+05:30) Sri Jayawardenepura' => 'Asia/Colombo',
+ '(UTC+05:45) Kathmandu' => 'Asia/Katmandu',
+ '(UTC+06:00) Astana' => 'Asia/Almaty',
+ '(UTC+06:00) Dhaka' => 'Asia/Dhaka',
+ '(UTC+06:30) Yangon (Rangoon)' => 'Asia/Rangoon',
+ '(UTC+07:00) Bangkok, Hanoi, Jakarta' => 'Asia/Bangkok',
+ '(UTC+07:00) Krasnoyarsk' => 'Asia/Krasnoyarsk',
+ '(UTC+07:00) Novosibirsk' => 'Asia/Novosibirsk',
+ '(UTC+08:00) Beijing, Chongqing, Hong Kong, Urumqi' => 'Asia/Shanghai',
+ '(UTC+08:00) Irkutsk' => 'Asia/Irkutsk',
+ '(UTC+08:00) Kuala Lumpur, Singapore' => 'Asia/Singapore',
+ '(UTC+08:00) Perth' => 'Australia/Perth',
+ '(UTC+08:00) Taipei' => 'Asia/Taipei',
+ '(UTC+08:00) Ulaanbaatar' => 'Asia/Ulaanbaatar',
+ '(UTC+09:00) Osaka, Sapporo, Tokyo' => 'Asia/Tokyo',
+ '(UTC+09:00) Pyongyang' => 'Asia/Pyongyang',
+ '(UTC+09:00) Seoul' => 'Asia/Seoul',
+ '(UTC+09:00) Yakutsk' => 'Asia/Yakutsk',
+ '(UTC+09:30) Adelaide' => 'Australia/Adelaide',
+ '(UTC+09:30) Darwin' => 'Australia/Darwin',
+ '(UTC+10:00) Brisbane' => 'Australia/Brisbane',
+ '(UTC+10:00) Canberra, Melbourne, Sydney' => 'Australia/Sydney',
+ '(UTC+10:00) Guam, Port Moresby' => 'Pacific/Port_Moresby',
+ '(UTC+10:00) Hobart' => 'Australia/Hobart',
+ '(UTC+10:00) Vladivostok' => 'Asia/Vladivostok',
+ '(UTC+11:00) Chokurdakh' => 'Asia/Srednekolymsk',
+ '(UTC+11:00) Magadan' => 'Asia/Magadan',
+ '(UTC+11:00) Solomon Is., New Caledonia' => 'Pacific/Guadalcanal',
+ '(UTC+12:00) Anadyr, Petropavlovsk-Kamchatsky' => 'Asia/Kamchatka',
+ '(UTC+12:00) Auckland, Wellington' => 'Pacific/Auckland',
+ '(UTC+12:00) Coordinated Universal Time+12' => 'Etc/GMT-12',
+ '(UTC+12:00) Fiji' => 'Pacific/Fiji',
+ "(UTC+13:00) Nuku'alofa" => 'Pacific/Tongatapu',
+ '(UTC+13:00) Samoa' => 'Pacific/Apia',
+ '(UTC+14:00) Kiritimati Island' => 'Pacific/Kiritimati',
+ );
+
+ /**
+ * Maps Windows (non-CLDR) time zone ID to IANA ID. This is pragmatic but not 100% precise as one Windows zone ID
+ * maps to multiple IANA IDs (one for each territory). For all practical purposes this should be good enough, though.
+ *
+ * Source: http://unicode.org/repos/cldr/trunk/common/supplemental/windowsZones.xml
+ *
+ * @var array
+ */
+ private static $windowsTimeZonesMap = array(
+ 'AUS Central Standard Time' => 'Australia/Darwin',
+ 'AUS Eastern Standard Time' => 'Australia/Sydney',
+ 'Afghanistan Standard Time' => 'Asia/Kabul',
+ 'Alaskan Standard Time' => 'America/Anchorage',
+ 'Aleutian Standard Time' => 'America/Adak',
+ 'Altai Standard Time' => 'Asia/Barnaul',
+ 'Arab Standard Time' => 'Asia/Riyadh',
+ 'Arabian Standard Time' => 'Asia/Dubai',
+ 'Arabic Standard Time' => 'Asia/Baghdad',
+ 'Argentina Standard Time' => 'America/Buenos_Aires',
+ 'Astrakhan Standard Time' => 'Europe/Astrakhan',
+ 'Atlantic Standard Time' => 'America/Halifax',
+ 'Aus Central W. Standard Time' => 'Australia/Eucla',
+ 'Azerbaijan Standard Time' => 'Asia/Baku',
+ 'Azores Standard Time' => 'Atlantic/Azores',
+ 'Bahia Standard Time' => 'America/Bahia',
+ 'Bangladesh Standard Time' => 'Asia/Dhaka',
+ 'Belarus Standard Time' => 'Europe/Minsk',
+ 'Bougainville Standard Time' => 'Pacific/Bougainville',
+ 'Canada Central Standard Time' => 'America/Regina',
+ 'Cape Verde Standard Time' => 'Atlantic/Cape_Verde',
+ 'Caucasus Standard Time' => 'Asia/Yerevan',
+ 'Cen. Australia Standard Time' => 'Australia/Adelaide',
+ 'Central America Standard Time' => 'America/Guatemala',
+ 'Central Asia Standard Time' => 'Asia/Almaty',
+ 'Central Brazilian Standard Time' => 'America/Cuiaba',
+ 'Central Europe Standard Time' => 'Europe/Budapest',
+ 'Central European Standard Time' => 'Europe/Warsaw',
+ 'Central Pacific Standard Time' => 'Pacific/Guadalcanal',
+ 'Central Standard Time (Mexico)' => 'America/Mexico_City',
+ 'Central Standard Time' => 'America/Chicago',
+ 'Chatham Islands Standard Time' => 'Pacific/Chatham',
+ 'China Standard Time' => 'Asia/Shanghai',
+ 'Cuba Standard Time' => 'America/Havana',
+ 'Dateline Standard Time' => 'Etc/GMT+12',
+ 'E. Africa Standard Time' => 'Africa/Nairobi',
+ 'E. Australia Standard Time' => 'Australia/Brisbane',
+ 'E. Europe Standard Time' => 'Europe/Chisinau',
+ 'E. South America Standard Time' => 'America/Sao_Paulo',
+ 'Easter Island Standard Time' => 'Pacific/Easter',
+ 'Eastern Standard Time (Mexico)' => 'America/Cancun',
+ 'Eastern Standard Time' => 'America/New_York',
+ 'Egypt Standard Time' => 'Africa/Cairo',
+ 'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg',
+ 'FLE Standard Time' => 'Europe/Kiev',
+ 'Fiji Standard Time' => 'Pacific/Fiji',
+ 'GMT Standard Time' => 'Europe/London',
+ 'GTB Standard Time' => 'Europe/Bucharest',
+ 'Georgian Standard Time' => 'Asia/Tbilisi',
+ 'Greenland Standard Time' => 'America/Godthab',
+ 'Greenwich Standard Time' => 'Atlantic/Reykjavik',
+ 'Haiti Standard Time' => 'America/Port-au-Prince',
+ 'Hawaiian Standard Time' => 'Pacific/Honolulu',
+ 'India Standard Time' => 'Asia/Calcutta',
+ 'Iran Standard Time' => 'Asia/Tehran',
+ 'Israel Standard Time' => 'Asia/Jerusalem',
+ 'Jordan Standard Time' => 'Asia/Amman',
+ 'Kaliningrad Standard Time' => 'Europe/Kaliningrad',
+ 'Korea Standard Time' => 'Asia/Seoul',
+ 'Libya Standard Time' => 'Africa/Tripoli',
+ 'Line Islands Standard Time' => 'Pacific/Kiritimati',
+ 'Lord Howe Standard Time' => 'Australia/Lord_Howe',
+ 'Magadan Standard Time' => 'Asia/Magadan',
+ 'Magallanes Standard Time' => 'America/Punta_Arenas',
+ 'Marquesas Standard Time' => 'Pacific/Marquesas',
+ 'Mauritius Standard Time' => 'Indian/Mauritius',
+ 'Middle East Standard Time' => 'Asia/Beirut',
+ 'Montevideo Standard Time' => 'America/Montevideo',
+ 'Morocco Standard Time' => 'Africa/Casablanca',
+ 'Mountain Standard Time (Mexico)' => 'America/Chihuahua',
+ 'Mountain Standard Time' => 'America/Denver',
+ 'Myanmar Standard Time' => 'Asia/Rangoon',
+ 'N. Central Asia Standard Time' => 'Asia/Novosibirsk',
+ 'Namibia Standard Time' => 'Africa/Windhoek',
+ 'Nepal Standard Time' => 'Asia/Katmandu',
+ 'New Zealand Standard Time' => 'Pacific/Auckland',
+ 'Newfoundland Standard Time' => 'America/St_Johns',
+ 'Norfolk Standard Time' => 'Pacific/Norfolk',
+ 'North Asia East Standard Time' => 'Asia/Irkutsk',
+ 'North Asia Standard Time' => 'Asia/Krasnoyarsk',
+ 'North Korea Standard Time' => 'Asia/Pyongyang',
+ 'Omsk Standard Time' => 'Asia/Omsk',
+ 'Pacific SA Standard Time' => 'America/Santiago',
+ 'Pacific Standard Time (Mexico)' => 'America/Tijuana',
+ 'Pacific Standard Time' => 'America/Los_Angeles',
+ 'Pakistan Standard Time' => 'Asia/Karachi',
+ 'Paraguay Standard Time' => 'America/Asuncion',
+ 'Romance Standard Time' => 'Europe/Paris',
+ 'Russia Time Zone 10' => 'Asia/Srednekolymsk',
+ 'Russia Time Zone 11' => 'Asia/Kamchatka',
+ 'Russia Time Zone 3' => 'Europe/Samara',
+ 'Russian Standard Time' => 'Europe/Moscow',
+ 'SA Eastern Standard Time' => 'America/Cayenne',
+ 'SA Pacific Standard Time' => 'America/Bogota',
+ 'SA Western Standard Time' => 'America/La_Paz',
+ 'SE Asia Standard Time' => 'Asia/Bangkok',
+ 'Saint Pierre Standard Time' => 'America/Miquelon',
+ 'Sakhalin Standard Time' => 'Asia/Sakhalin',
+ 'Samoa Standard Time' => 'Pacific/Apia',
+ 'Sao Tome Standard Time' => 'Africa/Sao_Tome',
+ 'Saratov Standard Time' => 'Europe/Saratov',
+ 'Singapore Standard Time' => 'Asia/Singapore',
+ 'South Africa Standard Time' => 'Africa/Johannesburg',
+ 'Sri Lanka Standard Time' => 'Asia/Colombo',
+ 'Sudan Standard Time' => 'Africa/Tripoli',
+ 'Syria Standard Time' => 'Asia/Damascus',
+ 'Taipei Standard Time' => 'Asia/Taipei',
+ 'Tasmania Standard Time' => 'Australia/Hobart',
+ 'Tocantins Standard Time' => 'America/Araguaina',
+ 'Tokyo Standard Time' => 'Asia/Tokyo',
+ 'Tomsk Standard Time' => 'Asia/Tomsk',
+ 'Tonga Standard Time' => 'Pacific/Tongatapu',
+ 'Transbaikal Standard Time' => 'Asia/Chita',
+ 'Turkey Standard Time' => 'Europe/Istanbul',
+ 'Turks And Caicos Standard Time' => 'America/Grand_Turk',
+ 'US Eastern Standard Time' => 'America/Indianapolis',
+ 'US Mountain Standard Time' => 'America/Phoenix',
+ 'UTC' => 'Etc/GMT',
+ 'UTC+12' => 'Etc/GMT-12',
+ 'UTC+13' => 'Etc/GMT-13',
+ 'UTC-02' => 'Etc/GMT+2',
+ 'UTC-08' => 'Etc/GMT+8',
+ 'UTC-09' => 'Etc/GMT+9',
+ 'UTC-11' => 'Etc/GMT+11',
+ 'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar',
+ 'Venezuela Standard Time' => 'America/Caracas',
+ 'Vladivostok Standard Time' => 'Asia/Vladivostok',
+ 'W. Australia Standard Time' => 'Australia/Perth',
+ 'W. Central Africa Standard Time' => 'Africa/Lagos',
+ 'W. Europe Standard Time' => 'Europe/Berlin',
+ 'W. Mongolia Standard Time' => 'Asia/Hovd',
+ 'West Asia Standard Time' => 'Asia/Tashkent',
+ 'West Bank Standard Time' => 'Asia/Hebron',
+ 'West Pacific Standard Time' => 'Pacific/Port_Moresby',
+ 'Yakutsk Standard Time' => 'Asia/Yakutsk',
+ );
+
+ /**
+ * If `$filterDaysBefore` or `$filterDaysAfter` are set then the events are filtered according to the window defined
+ * by this field and `$windowMaxTimestamp`.
+ *
+ * @var integer
+ */
+ private $windowMinTimestamp;
+
+ /**
+ * If `$filterDaysBefore` or `$filterDaysAfter` are set then the events are filtered according to the window defined
+ * by this field and `$windowMinTimestamp`.
+ *
+ * @var integer
+ */
+ private $windowMaxTimestamp;
+
+ /**
+ * `true` if either `$filterDaysBefore` or `$filterDaysAfter` are set.
+ *
+ * @var boolean
+ */
+ private $shouldFilterByWindow = false;
+
+ /**
+ * Creates the ICal object
+ *
+ * @param mixed $files
+ * @param array $options
+ * @return void
+ */
+ public function __construct($files = false, array $options = array())
+ {
+ if (\PHP_VERSION_ID < 80100) {
+ ini_set('auto_detect_line_endings', '1');
+ }
+
+ foreach ($options as $option => $value) {
+ if (in_array($option, self::$configurableOptions)) {
+ $this->{$option} = $value;
+ }
+ }
+
+ // Fallback to use the system default time zone
+ if (!isset($this->defaultTimeZone) || !$this->isValidTimeZoneId($this->defaultTimeZone)) {
+ $this->defaultTimeZone = $this->getDefaultTimeZone(true);
+ }
+
+ // Ideally you would use `PHP_INT_MIN` from PHP 7
+ $php_int_min = -2147483648;
+
+ $this->windowMinTimestamp = $php_int_min;
+
+ if (!is_null($this->filterDaysBefore)) {
+ if (is_int($this->filterDaysBefore)) {
+ $this->windowMinTimestamp = (new \DateTime('now'))
+ ->sub(new \DateInterval('P' . $this->filterDaysBefore . 'D'))
+ ->getTimestamp();
+ }
+
+ if ($this->filterDaysBefore instanceof \DateTimeInterface) {
+ $this->windowMinTimestamp = $this->filterDaysBefore->getTimestamp();
+ }
+ }
+
+ $this->windowMaxTimestamp = PHP_INT_MAX;
+
+ if (!is_null($this->filterDaysAfter)) {
+ if (is_int($this->filterDaysAfter)) {
+ $this->windowMaxTimestamp = (new \DateTime('now'))
+ ->add(new \DateInterval('P' . $this->filterDaysAfter . 'D'))
+ ->getTimestamp();
+ }
+
+ if ($this->filterDaysAfter instanceof \DateTimeInterface) {
+ $this->windowMaxTimestamp = $this->filterDaysAfter->getTimestamp();
+ }
+ }
+
+ $this->shouldFilterByWindow = !is_null($this->filterDaysBefore) || !is_null($this->filterDaysAfter);
+
+ if ($files !== false) {
+ $files = is_array($files) ? $files : array($files);
+
+ foreach ($files as $file) {
+ if (!is_array($file) && $this->isFileOrUrl($file)) {
+ $lines = $this->fileOrUrl($file);
+ } else {
+ $lines = is_array($file) ? $file : array($file);
+ }
+
+ $this->initLines($lines);
+ }
+ }
+ }
+
+ /**
+ * Initialises lines from a string
+ *
+ * @param string $string
+ * @return ICal
+ */
+ public function initString($string)
+ {
+ $string = str_replace(array("\r\n", "\n\r", "\r"), "\n", $string);
+
+ if ($this->cal === array()) {
+ $lines = explode("\n", $string);
+
+ $this->initLines($lines);
+ } else {
+ trigger_error('ICal::initString: Calendar already initialised in constructor', E_USER_NOTICE);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Initialises lines from a file
+ *
+ * @param string $file
+ * @return ICal
+ */
+ public function initFile($file)
+ {
+ if ($this->cal === array()) {
+ $lines = $this->fileOrUrl($file);
+
+ $this->initLines($lines);
+ } else {
+ trigger_error('ICal::initFile: Calendar already initialised in constructor', E_USER_NOTICE);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Initialises lines from a URL
+ *
+ * @param string $url
+ * @param string $username
+ * @param string $password
+ * @param string $userAgent
+ * @param string $acceptLanguage
+ * @param string $httpProtocolVersion
+ * @return ICal
+ */
+ public function initUrl($url, $username = null, $password = null, $userAgent = null, $acceptLanguage = null, $httpProtocolVersion = null)
+ {
+ if (!is_null($username) && !is_null($password)) {
+ $this->httpBasicAuth['username'] = $username;
+ $this->httpBasicAuth['password'] = $password;
+ }
+
+ if (!is_null($userAgent)) {
+ $this->httpUserAgent = $userAgent;
+ }
+
+ if (!is_null($acceptLanguage)) {
+ $this->httpAcceptLanguage = $acceptLanguage;
+ }
+
+ if (!is_null($httpProtocolVersion)) {
+ $this->httpProtocolVersion = $httpProtocolVersion;
+ }
+
+ $this->initFile($url);
+
+ return $this;
+ }
+
+ /**
+ * Initialises the parser using an array
+ * containing each line of iCal content
+ *
+ * @param array $lines
+ * @return void
+ */
+ protected function initLines(array $lines)
+ {
+ $lines = $this->unfold($lines);
+
+ if (stristr($lines[0], 'BEGIN:VCALENDAR') !== false) {
+ $component = '';
+ foreach ($lines as $line) {
+ $line = rtrim($line); // Trim trailing whitespace
+ $line = $this->removeUnprintableChars($line);
+
+ if (empty($line)) {
+ continue;
+ }
+
+ if (!$this->disableCharacterReplacement) {
+ $line = str_replace(array(
+ ' ',
+ "\t",
+ "\xc2\xa0", // Non-breaking space
+ ), ' ', $line);
+
+ $line = $this->cleanCharacters($line);
+ }
+
+ $add = $this->keyValueFromString($line);
+ $keyword = $add[0];
+ $values = $add[1]; // May be an array containing multiple values
+
+ if (!is_array($values)) {
+ if (!empty($values)) {
+ $values = array($values); // Make an array as not one already
+ $blankArray = array(); // Empty placeholder array
+ $values[] = $blankArray;
+ } else {
+ $values = array(); // Use blank array to ignore this line
+ }
+ } elseif (empty($values[0])) {
+ $values = array(); // Use blank array to ignore this line
+ }
+
+ // Reverse so that our array of properties is processed first
+ $values = array_reverse($values);
+
+ foreach ($values as $value) {
+ switch ($line) {
+ // https://www.kanzaki.com/docs/ical/vtodo.html
+ case 'BEGIN:VTODO':
+ if (!is_array($value)) {
+ $this->todoCount++;
+ }
+
+ $component = 'VTODO';
+
+ break;
+
+ case 'BEGIN:VEVENT':
+ // https://www.kanzaki.com/docs/ical/vevent.html
+ if (!is_array($value)) {
+ $this->eventCount++;
+ }
+
+ $component = 'VEVENT';
+
+ break;
+
+ case 'BEGIN:VFREEBUSY':
+ // https://www.kanzaki.com/docs/ical/vfreebusy.html
+ if (!is_array($value)) {
+ $this->freeBusyIndex++;
+ }
+
+ $component = 'VFREEBUSY';
+
+ break;
+
+ case 'BEGIN:VALARM':
+ if (!is_array($value)) {
+ $this->alarmCount++;
+ }
+
+ $component = 'VALARM';
+
+ break;
+
+ case 'END:VALARM':
+ $component = 'VEVENT';
+
+ break;
+
+ case 'BEGIN:DAYLIGHT':
+ case 'BEGIN:STANDARD':
+ case 'BEGIN:VCALENDAR':
+ case 'BEGIN:VTIMEZONE':
+ $component = $value;
+
+ break;
+
+ case 'END:DAYLIGHT':
+ case 'END:STANDARD':
+ case 'END:VCALENDAR':
+ case 'END:VFREEBUSY':
+ case 'END:VTIMEZONE':
+ case 'END:VTODO':
+ $component = 'VCALENDAR';
+
+ break;
+
+ case 'END:VEVENT':
+ if ($this->shouldFilterByWindow) {
+ $this->removeLastEventIfOutsideWindowAndNonRecurring();
+ }
+
+ $component = 'VCALENDAR';
+
+ break;
+
+ default:
+ $this->addCalendarComponentWithKeyAndValue($component, $keyword, $value);
+
+ break;
+ }
+ }
+ }
+
+ $this->processEvents();
+
+ if (!$this->skipRecurrence) {
+ $this->processRecurrences();
+
+ // Apply changes to altered recurrence instances
+ if ($this->alteredRecurrenceInstances !== array()) {
+ $events = $this->cal['VEVENT'];
+
+ foreach ($this->alteredRecurrenceInstances as $alteredRecurrenceInstance) {
+ if (isset($alteredRecurrenceInstance['altered-event'])) {
+ $alteredEvent = $alteredRecurrenceInstance['altered-event'];
+ $key = key($alteredEvent);
+ $events[$key] = $alteredEvent[$key];
+ }
+ }
+
+ $this->cal['VEVENT'] = $events;
+ }
+ }
+
+ if ($this->shouldFilterByWindow) {
+ $this->reduceEventsToMinMaxRange();
+ }
+
+ $this->processDateConversions();
+ }
+ }
+
+ /**
+ * Removes the last event (i.e. most recently parsed) if its start date is outside the window spanned by
+ * `$windowMinTimestamp` / `$windowMaxTimestamp`.
+ *
+ * @return void
+ */
+ protected function removeLastEventIfOutsideWindowAndNonRecurring()
+ {
+ $events = $this->cal['VEVENT'];
+
+ if ($events !== array()) {
+ $lastIndex = count($events) - 1;
+ $lastEvent = $events[$lastIndex];
+
+ if ((!isset($lastEvent['RRULE']) || $lastEvent['RRULE'] === '') && $this->doesEventStartOutsideWindow($lastEvent)) {
+ $this->eventCount--;
+
+ unset($events[$lastIndex]);
+ }
+
+ $this->cal['VEVENT'] = $events;
+ }
+ }
+
+ /**
+ * Reduces the number of events to the defined minimum and maximum range
+ *
+ * @return void
+ */
+ protected function reduceEventsToMinMaxRange()
+ {
+ $events = (isset($this->cal['VEVENT'])) ? $this->cal['VEVENT'] : array();
+
+ if ($events !== array()) {
+ foreach ($events as $key => $anEvent) {
+ if ($anEvent === null) {
+ unset($events[$key]);
+
+ continue;
+ }
+
+ if ($this->doesEventStartOutsideWindow($anEvent)) {
+ $this->eventCount--;
+
+ unset($events[$key]);
+
+ continue;
+ }
+ }
+
+ $this->cal['VEVENT'] = $events;
+ }
+ }
+
+ /**
+ * Determines whether the event start date is outside `$windowMinTimestamp` / `$windowMaxTimestamp`.
+ * Returns `true` for invalid dates.
+ *
+ * @param array $event
+ * @return boolean
+ */
+ protected function doesEventStartOutsideWindow(array $event)
+ {
+ return !$this->isValidDate($event['DTSTART']) || $this->isOutOfRange($event['DTSTART'], $this->windowMinTimestamp, $this->windowMaxTimestamp);
+ }
+
+ /**
+ * Determines whether a valid iCalendar date is within a given range
+ *
+ * @param string $calendarDate
+ * @param integer $minTimestamp
+ * @param integer $maxTimestamp
+ * @return boolean
+ */
+ protected function isOutOfRange($calendarDate, $minTimestamp, $maxTimestamp)
+ {
+ $timestamp = strtotime(explode('T', $calendarDate)[0]);
+
+ return $timestamp < $minTimestamp || $timestamp > $maxTimestamp;
+ }
+
+ /**
+ * Unfolds an iCal file in preparation for parsing
+ * (https://icalendar.org/iCalendar-RFC-5545/3-1-content-lines.html)
+ *
+ * @param array $lines
+ * @return array
+ */
+ protected function unfold(array $lines)
+ {
+ $string = implode(PHP_EOL, $lines);
+ $string = str_ireplace(' ', ' ', $string);
+
+ $cleanedString = preg_replace('/' . PHP_EOL . '[ \t]/', '', $string);
+
+ $lines = explode(PHP_EOL, $cleanedString ?: $string);
+
+ return $lines;
+ }
+
+ /**
+ * Add one key and value pair to the `$this->cal` array
+ *
+ * @param string $component
+ * @param string|boolean $keyword
+ * @param string|array $value
+ * @return void
+ */
+ protected function addCalendarComponentWithKeyAndValue($component, $keyword, $value)
+ {
+ if ($keyword === false) {
+ $keyword = $this->lastKeyword;
+ }
+
+ switch ($component) {
+ case 'VALARM':
+ $key1 = 'VEVENT';
+ $key2 = ($this->eventCount - 1);
+ $key3 = $component;
+
+ if (!isset($this->cal[$key1][$key2][$key3]["{$keyword}_array"])) {
+ $this->cal[$key1][$key2][$key3]["{$keyword}_array"] = array();
+ }
+
+ if (is_array($value)) {
+ // Add array of properties to the end
+ $this->cal[$key1][$key2][$key3]["{$keyword}_array"][] = $value;
+ } else {
+ if (!isset($this->cal[$key1][$key2][$key3][$keyword])) {
+ $this->cal[$key1][$key2][$key3][$keyword] = $value;
+ }
+
+ if ($this->cal[$key1][$key2][$key3][$keyword] !== $value) {
+ $this->cal[$key1][$key2][$key3][$keyword] .= ',' . $value;
+ }
+ }
+ break;
+
+ case 'VEVENT':
+ $key1 = $component;
+ $key2 = ($this->eventCount - 1);
+
+ if (!isset($this->cal[$key1][$key2]["{$keyword}_array"])) {
+ $this->cal[$key1][$key2]["{$keyword}_array"] = array();
+ }
+
+ if (is_array($value)) {
+ // Add array of properties to the end
+ $this->cal[$key1][$key2]["{$keyword}_array"][] = $value;
+ } else {
+ if (!isset($this->cal[$key1][$key2][$keyword])) {
+ $this->cal[$key1][$key2][$keyword] = $value;
+ }
+
+ if ($keyword === 'EXDATE') {
+ if (trim($value) === $value) {
+ $array = array_filter(explode(',', $value));
+ $this->cal[$key1][$key2]["{$keyword}_array"][] = $array;
+ } else {
+ $value = explode(',', implode(',', $this->cal[$key1][$key2]["{$keyword}_array"][1]) . trim($value));
+ $this->cal[$key1][$key2]["{$keyword}_array"][1] = $value;
+ }
+ } else {
+ $this->cal[$key1][$key2]["{$keyword}_array"][] = $value;
+
+ if ($keyword === 'DURATION') {
+ $duration = new \DateInterval($value);
+ $this->cal[$key1][$key2]["{$keyword}_array"][] = $duration;
+ }
+ }
+
+ if (!is_array($value) && $this->cal[$key1][$key2][$keyword] !== $value) {
+ $this->cal[$key1][$key2][$keyword] .= ',' . $value;
+ }
+ }
+ break;
+
+ case 'VFREEBUSY':
+ $key1 = $component;
+ $key2 = ($this->freeBusyIndex - 1);
+ $key3 = $keyword;
+
+ if ($keyword === 'FREEBUSY') {
+ if (is_array($value)) {
+ $this->cal[$key1][$key2][$key3][][] = $value;
+ } else {
+ $this->freeBusyCount++;
+
+ end($this->cal[$key1][$key2][$key3]);
+ $key = key($this->cal[$key1][$key2][$key3]);
+
+ $value = explode('/', $value);
+ $this->cal[$key1][$key2][$key3][$key][] = $value;
+ }
+ } else {
+ $this->cal[$key1][$key2][$key3][] = $value;
+ }
+ break;
+
+ case 'VTODO':
+ $this->cal[$component][$this->todoCount - 1][$keyword] = $value;
+
+ break;
+
+ default:
+ $this->cal[$component][$keyword] = $value;
+
+ break;
+ }
+
+ if (is_string($keyword)) {
+ $this->lastKeyword = $keyword;
+ }
+ }
+
+ /**
+ * Gets the key value pair from an iCal string
+ *
+ * @param string $text
+ * @return array
+ */
+ public function keyValueFromString($text)
+ {
+ $splitLine = $this->parseLine($text);
+ $object = array();
+ $paramObj = array();
+ $valueObj = '';
+ $i = 0;
+
+ while ($i < count($splitLine)) {
+ // The first token corresponds to the property name
+ if ($i === 0) {
+ $object[0] = $splitLine[$i];
+ $i++;
+
+ continue;
+ }
+
+ // After each semicolon define the property parameters
+ if ($splitLine[$i] == ';') {
+ $i++;
+ $paramName = $splitLine[$i];
+ $i += 2;
+ $paramValue = array();
+ $multiValue = false;
+ // A parameter can have multiple values separated by a comma
+ while ($i + 1 < count($splitLine) && $splitLine[$i + 1] === ',') {
+ $paramValue[] = $splitLine[$i];
+ $i += 2;
+ $multiValue = true;
+ }
+
+ if ($multiValue) {
+ $paramValue[] = $splitLine[$i];
+ } else {
+ $paramValue = $splitLine[$i];
+ }
+
+ // Create object with paramName => paramValue
+ $paramObj[$paramName] = $paramValue;
+ }
+
+ // After a colon all tokens are concatenated (non-standard behaviour because the property can have multiple values
+ // according to RFC5545)
+ if ($splitLine[$i] === ':') {
+ $i++;
+ while ($i < count($splitLine)) {
+ $valueObj .= $splitLine[$i];
+ $i++;
+ }
+ }
+
+ $i++;
+ }
+
+ // Object construction
+ if ($paramObj !== array()) {
+ $object[1][0] = $valueObj;
+ $object[1][1] = $paramObj;
+ } else {
+ $object[1] = $valueObj;
+ }
+
+ return $object;
+ }
+
+ /**
+ * Parses a line from an iCal file into an array of tokens
+ *
+ * @param string $line
+ * @return array
+ */
+ protected function parseLine($line)
+ {
+ $words = array();
+ $word = '';
+ // The use of str_split is not a problem here even if the character set is in utf8
+ // Indeed we only compare the characters , ; : = " which are on a single byte
+ $arrayOfChar = str_split($line);
+ $inDoubleQuotes = false;
+
+ foreach ($arrayOfChar as $char) {
+ // Don't stop the word on ; , : = if it is enclosed in double quotes
+ if ($char === '"') {
+ if ($word !== '') {
+ $words[] = $word;
+ }
+
+ $word = '';
+ $inDoubleQuotes = !$inDoubleQuotes;
+ } elseif (!in_array($char, array(';', ':', ',', '=')) || $inDoubleQuotes) {
+ $word .= $char;
+ } else {
+ if ($word !== '') {
+ $words[] = $word;
+ }
+
+ $words[] = $char;
+ $word = '';
+ }
+ }
+
+ $words[] = $word;
+
+ return $words;
+ }
+
+ /**
+ * Returns the default time zone if set.
+ * Falls back to the system default if not set.
+ *
+ * @param boolean $forceReturnSystemDefault
+ * @return string
+ */
+ private function getDefaultTimeZone($forceReturnSystemDefault = false)
+ {
+ $systemDefault = date_default_timezone_get();
+
+ if ($forceReturnSystemDefault) {
+ return $systemDefault;
+ }
+
+ return $this->defaultTimeZone ?: $systemDefault;
+ }
+
+ /**
+ * Returns a `DateTime` object from an iCal date time format
+ *
+ * @param string $icalDate
+ * @return \DateTime|false
+ * @throws \Exception
+ */
+ public function iCalDateToDateTime($icalDate)
+ {
+ /**
+ * iCal times may be in 3 formats, (https://www.kanzaki.com/docs/ical/dateTime.html)
+ *
+ * UTC: Has a trailing 'Z'
+ * Floating: No time zone reference specified, no trailing 'Z', use local time
+ * TZID: Set time zone as specified
+ *
+ * Use DateTime class objects to get around limitations with `mktime` and `gmmktime`.
+ * Must have a local time zone set to process floating times.
+ */
+ $pattern = '/^(?:TZID=)?([^:]*|".*")'; // [1]: Time zone
+ $pattern .= ':?'; // Time zone delimiter
+ $pattern .= '(\d{8})'; // [2]: YYYYMMDD
+ $pattern .= 'T?'; // Time delimiter
+ $pattern .= '(?(?<=T)(\d{6}))'; // [3]: HHMMSS (filled if delimiter present)
+ $pattern .= '(Z?)/'; // [4]: UTC flag
+
+ preg_match($pattern, $icalDate, $date);
+
+ if ($date === array()) {
+ throw new \Exception('Invalid iCal date format.');
+ }
+
+ // A Unix timestamp usually cannot represent a date prior to 1 Jan 1970.
+ // PHP, on the other hand, uses negative numbers for that. Thus we don't
+ // need to special case them.
+
+ if ($date[4] === 'Z') {
+ $dateTimeZone = new \DateTimeZone(self::TIME_ZONE_UTC);
+ } elseif (isset($date[1]) && $date[1] !== '') {
+ $dateTimeZone = $this->timeZoneStringToDateTimeZone($date[1]);
+ } else {
+ $dateTimeZone = new \DateTimeZone($this->getDefaultTimeZone());
+ }
+
+ // The exclamation mark at the start of the format string indicates that if a
+ // time portion is not included, the time in the returned DateTime should be
+ // set to 00:00:00. Without it, the time would be set to the current system time.
+ $dateFormat = '!Ymd';
+ $dateBasic = $date[2];
+ if (isset($date[3]) && $date[3] !== '') {
+ $dateBasic .= "T{$date[3]}";
+ $dateFormat .= '\THis';
+ }
+
+ return \DateTime::createFromFormat($dateFormat, $dateBasic, $dateTimeZone);
+ }
+
+ /**
+ * Returns a Unix timestamp from an iCal date time format
+ *
+ * @param string $icalDate
+ * @return integer
+ */
+ public function iCalDateToUnixTimestamp($icalDate)
+ {
+ $iCalDateToDateTime = $this->iCalDateToDateTime($icalDate);
+
+ if ($iCalDateToDateTime === false) {
+ trigger_error("ICal::iCalDateToUnixTimestamp: Invalid date passed ({$icalDate})", E_USER_NOTICE);
+
+ return 0;
+ }
+
+ return $iCalDateToDateTime->getTimestamp();
+ }
+
+ /**
+ * Returns a date adapted to the calendar time zone depending on the event `TZID`
+ *
+ * @param array $event
+ * @param string $key
+ * @param string|null $format
+ * @return string|integer|boolean|\DateTime
+ */
+ public function iCalDateWithTimeZone(array $event, $key, $format = self::DATE_TIME_FORMAT)
+ {
+ if (!isset($event["{$key}_array"]) || !isset($event[$key])) {
+ return false;
+ }
+
+ $dateArray = $event["{$key}_array"];
+
+ if ($key === 'DURATION') {
+ $dateTime = $this->parseDuration($event['DTSTART'], $dateArray[2]);
+
+ if ($dateTime instanceof \DateTime === false) {
+ trigger_error("ICal::iCalDateWithTimeZone: Invalid date passed ({$event['DTSTART']})", E_USER_NOTICE);
+
+ return false;
+ }
+ } else {
+ // When constructing from a Unix Timestamp, no time zone needs passing.
+ $dateTime = new \DateTime("@{$dateArray[2]}");
+ }
+
+ $calendarTimeZone = $this->calendarTimeZone();
+
+ if (!is_null($calendarTimeZone)) {
+ // Set the time zone we wish to use when running `$dateTime->format`.
+ $dateTime->setTimezone(new \DateTimeZone($calendarTimeZone));
+ }
+
+ if (is_null($format)) {
+ return $dateTime;
+ }
+
+ return $dateTime->format($format);
+ }
+
+ /**
+ * Performs admin tasks on all events as read from the iCal file.
+ * Adds a Unix timestamp to all `{DTSTART|DTEND|RECURRENCE-ID}_array` arrays
+ * Tracks modified recurrence instances
+ *
+ * @return void
+ */
+ protected function processEvents()
+ {
+ $checks = null;
+ $events = (isset($this->cal['VEVENT'])) ? $this->cal['VEVENT'] : array();
+
+ if ($events !== array()) {
+ foreach ($events as $key => $anEvent) {
+ foreach (array('DTSTART', 'DTEND', 'RECURRENCE-ID') as $type) {
+ if (isset($anEvent[$type])) {
+ $date = $anEvent["{$type}_array"][1];
+
+ if (isset($anEvent["{$type}_array"][0]['TZID'])) {
+ $timeZone = $this->escapeParamText($anEvent["{$type}_array"][0]['TZID']);
+ $date = sprintf(self::ICAL_DATE_TIME_TEMPLATE, $timeZone) . $date;
+ }
+
+ $anEvent["{$type}_array"][2] = $this->iCalDateToUnixTimestamp($date);
+ $anEvent["{$type}_array"][3] = $date;
+ }
+ }
+
+ if (isset($anEvent['RECURRENCE-ID'])) {
+ $uid = $anEvent['UID'];
+
+ if (!isset($this->alteredRecurrenceInstances[$uid])) {
+ $this->alteredRecurrenceInstances[$uid] = array();
+ }
+
+ $recurrenceDateUtc = $this->iCalDateToUnixTimestamp($anEvent['RECURRENCE-ID_array'][3]);
+ $this->alteredRecurrenceInstances[$uid][$key] = $recurrenceDateUtc;
+ }
+
+ $events[$key] = $anEvent;
+ }
+
+ $eventKeysToRemove = array();
+
+ foreach ($events as $key => $event) {
+ $checks[] = !isset($event['RECURRENCE-ID']);
+ $checks[] = isset($event['UID']);
+ $checks[] = isset($event['UID']) && isset($this->alteredRecurrenceInstances[$event['UID']]);
+
+ if ((bool) array_product($checks)) {
+ $eventDtstartUnix = $this->iCalDateToUnixTimestamp($event['DTSTART_array'][3]);
+
+ // phpcs:ignore CustomPHPCS.ControlStructures.AssignmentInCondition
+ if (($alteredEventKey = array_search($eventDtstartUnix, $this->alteredRecurrenceInstances[$event['UID']], true)) !== false) {
+ $eventKeysToRemove[] = $alteredEventKey;
+
+ $alteredEvent = array_replace_recursive($events[$key], $events[$alteredEventKey]);
+ $this->alteredRecurrenceInstances[$event['UID']]['altered-event'] = array($key => $alteredEvent);
+ }
+ }
+
+ unset($checks);
+ }
+
+ foreach ($eventKeysToRemove as $eventKeyToRemove) {
+ $events[$eventKeyToRemove] = null;
+ }
+
+ $this->cal['VEVENT'] = $events;
+ }
+ }
+
+ /**
+ * Processes recurrence rules
+ *
+ * @return void
+ */
+ protected function processRecurrences()
+ {
+ $events = (isset($this->cal['VEVENT'])) ? $this->cal['VEVENT'] : array();
+
+ // If there are no events, then we have nothing to process.
+ if ($events === array()) {
+ return;
+ }
+
+ $allEventRecurrences = array();
+ $eventKeysToRemove = array();
+
+ foreach ($events as $key => $anEvent) {
+ if (!isset($anEvent['RRULE']) || $anEvent['RRULE'] === '') {
+ continue;
+ }
+
+ // Tag as generated by a recurrence rule
+ $anEvent['RRULE_array'][2] = self::RECURRENCE_EVENT;
+
+ // Create new initial starting point.
+ $initialEventDate = $this->icalDateToDateTime($anEvent['DTSTART_array'][3]);
+
+ if ($initialEventDate === false) {
+ trigger_error("ICal::processRecurrences: Invalid date passed ({$anEvent['DTSTART_array'][3]})", E_USER_NOTICE);
+
+ continue;
+ }
+
+ // Separate the RRULE stanzas, and explode the values that are lists.
+ $rrules = array();
+ foreach (array_filter(explode(';', $anEvent['RRULE'])) as $s) {
+ list($k, $v) = explode('=', $s);
+ if (in_array($k, array('BYSETPOS', 'BYDAY', 'BYMONTHDAY', 'BYMONTH', 'BYYEARDAY', 'BYWEEKNO'))) {
+ $rrules[$k] = explode(',', $v);
+ } else {
+ $rrules[$k] = $v;
+ }
+ }
+
+ $frequency = $rrules['FREQ'];
+
+ if (!is_string($frequency)) {
+ trigger_error('ICal::processRecurrences: Invalid frequency passed', E_USER_NOTICE);
+
+ continue;
+ }
+
+ // Reject RRULE if BYDAY stanza is invalid:
+ // > The BYDAY rule part MUST NOT be specified with a numeric value
+ // > when the FREQ rule part is not set to MONTHLY or YEARLY.
+ // > Furthermore, the BYDAY rule part MUST NOT be specified with a
+ // > numeric value with the FREQ rule part set to YEARLY when the
+ // > BYWEEKNO rule part is specified.
+ if (isset($rrules['BYDAY'])) {
+ $checkByDays = function ($carry, $weekday) {
+ return $carry && substr($weekday, -2) === $weekday;
+ };
+ if (!in_array($frequency, array('MONTHLY', 'YEARLY'))) {
+ if (is_array($rrules['BYDAY']) && !array_reduce($rrules['BYDAY'], $checkByDays, true)) {
+ trigger_error("ICal::processRecurrences: A {$frequency} RRULE may not contain BYDAY values with numeric prefixes", E_USER_NOTICE);
+
+ continue;
+ }
+ } elseif ($frequency === 'YEARLY' && (isset($rrules['BYWEEKNO']) && ($rrules['BYWEEKNO'] !== '' && $rrules['BYWEEKNO'] !== array()))) {
+ if (is_array($rrules['BYDAY']) && !array_reduce($rrules['BYDAY'], $checkByDays, true)) {
+ trigger_error('ICal::processRecurrences: A YEARLY RRULE with a BYWEEKNO part may not contain BYDAY values with numeric prefixes', E_USER_NOTICE);
+
+ continue;
+ }
+ }
+ }
+
+ $interval = (empty($rrules['INTERVAL'])) ? 1 : (int) $rrules['INTERVAL'];
+
+ // Throw an error if this isn't an integer.
+ if (!is_int($this->defaultSpan)) {
+ trigger_error('ICal::defaultSpan: User defined value is not an integer', E_USER_NOTICE);
+ }
+
+ // Compute EXDATEs
+ $exdates = $this->parseExdates($anEvent);
+
+ // Determine if the initial date is also an EXDATE
+ $initialDateIsExdate = array_reduce($exdates, function ($carry, $exdate) use ($initialEventDate) {
+ return $carry || $exdate->getTimestamp() === $initialEventDate->getTimestamp();
+ }, false);
+
+ if ($initialDateIsExdate) {
+ $eventKeysToRemove[] = $key;
+ }
+
+ /**
+ * Determine at what point we should stop calculating recurrences
+ * by looking at the UNTIL or COUNT rrule stanza, or, if neither
+ * if set, using a fallback.
+ *
+ * If the initial date is also an EXDATE, it shouldn't be included
+ * in the count.
+ *
+ * Syntax:
+ * UNTIL={enddate}
+ * COUNT=
+ *
+ * Where:
+ * enddate = ||
+ */
+ $count = 1;
+ $countLimit = (isset($rrules['COUNT'])) ? intval($rrules['COUNT']) : PHP_INT_MAX;
+ $now = date_create();
+
+ $until = $now === false
+ ? 0
+ : $now->modify("{$this->defaultSpan} years")->setTime(23, 59, 59)->getTimestamp();
+
+ $untilWhile = $until;
+
+ if (isset($rrules['UNTIL']) && is_string($rrules['UNTIL'])) {
+ $untilDT = $this->iCalDateToDateTime($rrules['UNTIL']);
+ $until = min($until, ($untilDT === false) ? $until : $untilDT->getTimestamp());
+
+ // There are certain edge cases where we need to go a little beyond the UNTIL to
+ // ensure we get all events. Consider:
+ //
+ // DTSTART:20200103
+ // RRULE:FREQ=MONTHLY;BYDAY=-5FR;UNTIL=20200502
+ //
+ // In this case the last occurrence should be 1st May, however when we transition
+ // from April to May:
+ //
+ // $until ~= 2nd May
+ // $frequencyRecurringDateTime ~= 3rd May
+ //
+ // And as the latter comes after the former, the while loop ends before any dates
+ // in May have the chance to be considered.
+ $untilWhile = min($untilWhile, ($untilDT === false) ? $untilWhile : $untilDT->modify("+1 {$this->frequencyConversion[$frequency]}")->getTimestamp());
+ }
+
+ $eventRecurrences = array();
+
+ $frequencyRecurringDateTime = clone $initialEventDate;
+ while ($frequencyRecurringDateTime->getTimestamp() <= $untilWhile && $count < $countLimit) {
+ $candidateDateTimes = array();
+
+ // phpcs:ignore Squiz.ControlStructures.SwitchDeclaration.MissingDefault
+ switch ($frequency) {
+ case 'DAILY':
+ if (isset($rrules['BYMONTHDAY']) && (is_array($rrules['BYMONTHDAY']) && $rrules['BYMONTHDAY'] !== array())) {
+ if (!isset($monthDays)) {
+ // This variable is unset when we change months (see below)
+ $monthDays = $this->getDaysOfMonthMatchingByMonthDayRRule($rrules['BYMONTHDAY'], $frequencyRecurringDateTime);
+ }
+
+ if (!in_array($frequencyRecurringDateTime->format('j'), $monthDays)) {
+ break;
+ }
+ }
+
+ $candidateDateTimes[] = clone $frequencyRecurringDateTime;
+
+ break;
+
+ case 'WEEKLY':
+ $initialDayOfWeek = $frequencyRecurringDateTime->format('N');
+ $matchingDays = array($initialDayOfWeek);
+
+ if (isset($rrules['BYDAY']) && (is_array($rrules['BYDAY']) && $rrules['BYDAY'] !== array())) {
+ // setISODate() below uses the ISO-8601 specification of weeks: start on
+ // a Monday, end on a Sunday. However, RRULEs (or the caller of the
+ // parser) may state an alternate WeeKSTart.
+ $wkstTransition = 7;
+
+ if (empty($rrules['WKST'])) {
+ if ($this->defaultWeekStart !== self::ISO_8601_WEEK_START) {
+ $wkstTransition = array_search($this->defaultWeekStart, array_keys($this->weekdays), true);
+ }
+ } elseif ($rrules['WKST'] !== self::ISO_8601_WEEK_START) {
+ $wkstTransition = array_search($rrules['WKST'], array_keys($this->weekdays), true);
+ }
+
+ $matchingDays = array_map(
+ function ($weekday) use ($initialDayOfWeek, $wkstTransition, $interval) {
+ $day = array_search($weekday, array_keys($this->weekdays), true);
+
+ if ($day < $initialDayOfWeek) {
+ $day += 7;
+ }
+
+ if ($day >= $wkstTransition) {
+ $day += 7 * ($interval - 1);
+ }
+
+ // Ignoring alternate week starts, $day at this point will have a
+ // value between 0 and 6. But setISODate() expects a value of 1 to 7.
+ // Even with alternate week starts, we still need to +1 to set the
+ // correct weekday.
+ $day++;
+
+ return $day;
+ },
+ $rrules['BYDAY']
+ );
+ }
+
+ sort($matchingDays);
+
+ foreach ($matchingDays as $day) {
+ $clonedDateTime = clone $frequencyRecurringDateTime;
+ $candidateDateTimes[] = $clonedDateTime->setISODate(
+ (int) $frequencyRecurringDateTime->format('o'),
+ (int) $frequencyRecurringDateTime->format('W'),
+ (int) $day
+ );
+ }
+ break;
+
+ case 'MONTHLY':
+ $matchingDays = array();
+
+ if (isset($rrules['BYMONTHDAY']) && (is_array($rrules['BYMONTHDAY']) && $rrules['BYMONTHDAY'] !== array())) {
+ $matchingDays = $this->getDaysOfMonthMatchingByMonthDayRRule($rrules['BYMONTHDAY'], $frequencyRecurringDateTime);
+ if (isset($rrules['BYDAY']) && (is_array($rrules['BYDAY']) && $rrules['BYDAY'] !== array())) {
+ $matchingDays = array_filter(
+ $this->getDaysOfMonthMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime),
+ function ($monthDay) use ($matchingDays) {
+ return in_array($monthDay, $matchingDays);
+ }
+ );
+ }
+ } elseif (isset($rrules['BYDAY']) && (is_array($rrules['BYDAY']) && $rrules['BYDAY'] !== array())) {
+ $matchingDays = $this->getDaysOfMonthMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime);
+ } else {
+ $matchingDays[] = $frequencyRecurringDateTime->format('d');
+ }
+
+ if (isset($rrules['BYSETPOS']) && (is_array($rrules['BYSETPOS']) && $rrules['BYSETPOS'] !== array())) {
+ $matchingDays = $this->filterValuesUsingBySetPosRRule($rrules['BYSETPOS'], $matchingDays);
+ }
+
+ foreach ($matchingDays as $day) {
+ // Skip invalid dates (e.g. 30th February)
+ if ($day > $frequencyRecurringDateTime->format('t')) {
+ continue;
+ }
+
+ $clonedDateTime = clone $frequencyRecurringDateTime;
+ $candidateDateTimes[] = $clonedDateTime->setDate(
+ (int) $frequencyRecurringDateTime->format('Y'),
+ (int) $frequencyRecurringDateTime->format('m'),
+ $day
+ );
+ }
+ break;
+
+ case 'YEARLY':
+ $matchingDays = array();
+
+ if (isset($rrules['BYMONTH']) && (is_array($rrules['BYMONTH']) && $rrules['BYMONTH'] !== array())) {
+ $bymonthRecurringDatetime = clone $frequencyRecurringDateTime;
+ foreach ($rrules['BYMONTH'] as $byMonth) {
+ $bymonthRecurringDatetime->setDate(
+ (int) $frequencyRecurringDateTime->format('Y'),
+ (int) $byMonth,
+ (int) $frequencyRecurringDateTime->format('d')
+ );
+
+ // Determine the days of the month affected
+ // (The interaction between BYMONTHDAY and BYDAY is resolved later.)
+ $monthDays = array();
+ if (isset($rrules['BYMONTHDAY']) && (is_array($rrules['BYMONTHDAY']) && $rrules['BYMONTHDAY'] !== array())) {
+ $monthDays = $this->getDaysOfMonthMatchingByMonthDayRRule($rrules['BYMONTHDAY'], $bymonthRecurringDatetime);
+ } elseif (isset($rrules['BYDAY']) && (is_array($rrules['BYDAY']) && $rrules['BYDAY'] !== array())) {
+ $monthDays = $this->getDaysOfMonthMatchingByDayRRule($rrules['BYDAY'], $bymonthRecurringDatetime);
+ } else {
+ $monthDays[] = $bymonthRecurringDatetime->format('d');
+ }
+
+ // And add each of them to the list of recurrences
+ foreach ($monthDays as $day) {
+ $matchingDays[] = $bymonthRecurringDatetime->setDate(
+ (int) $frequencyRecurringDateTime->format('Y'),
+ (int) $bymonthRecurringDatetime->format('m'),
+ $day
+ )->format('z') + 1;
+ }
+ }
+ } elseif (isset($rrules['BYWEEKNO']) && (is_array($rrules['BYWEEKNO']) && $rrules['BYWEEKNO'] !== array())) {
+ $matchingDays = $this->getDaysOfYearMatchingByWeekNoRRule($rrules['BYWEEKNO'], $frequencyRecurringDateTime);
+ } elseif (isset($rrules['BYYEARDAY']) && (is_array($rrules['BYYEARDAY']) && $rrules['BYYEARDAY'] !== array())) {
+ $matchingDays = $this->getDaysOfYearMatchingByYearDayRRule($rrules['BYYEARDAY'], $frequencyRecurringDateTime);
+ } elseif (isset($rrules['BYMONTHDAY']) && (is_array($rrules['BYMONTHDAY']) && $rrules['BYMONTHDAY'] !== array())) {
+ $matchingDays = $this->getDaysOfYearMatchingByMonthDayRRule($rrules['BYMONTHDAY'], $frequencyRecurringDateTime);
+ }
+
+ if (isset($rrules['BYDAY']) && (is_array($rrules['BYDAY']) && $rrules['BYDAY'] !== array())) {
+ if (isset($rrules['BYYEARDAY']) && ($rrules['BYYEARDAY'] !== '' && $rrules['BYYEARDAY'] !== array()) || isset($rrules['BYMONTHDAY']) && ($rrules['BYMONTHDAY'] !== '' && $rrules['BYMONTHDAY'] !== array()) || isset($rrules['BYWEEKNO']) && ($rrules['BYWEEKNO'] !== '' && $rrules['BYWEEKNO'] !== array())) {
+ $matchingDays = array_filter(
+ $this->getDaysOfYearMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime),
+ function ($yearDay) use ($matchingDays) {
+ return in_array($yearDay, $matchingDays);
+ }
+ );
+ } elseif ($matchingDays === array()) {
+ $matchingDays = $this->getDaysOfYearMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime);
+ }
+ }
+
+ if ($matchingDays === array()) {
+ $matchingDays = array($frequencyRecurringDateTime->format('z') + 1);
+ } else {
+ sort($matchingDays);
+ }
+
+ if (isset($rrules['BYSETPOS']) && (is_array($rrules['BYSETPOS']) && $rrules['BYSETPOS'] !== array())) {
+ $matchingDays = $this->filterValuesUsingBySetPosRRule($rrules['BYSETPOS'], $matchingDays);
+ }
+
+ foreach ($matchingDays as $day) {
+ $clonedDateTime = clone $frequencyRecurringDateTime;
+ $candidateDateTimes[] = $clonedDateTime->setDate(
+ (int) $frequencyRecurringDateTime->format('Y'),
+ 1,
+ $day
+ );
+ }
+ break;
+ }
+
+ foreach ($candidateDateTimes as $candidate) {
+ $timestamp = $candidate->getTimestamp();
+ if ($timestamp <= $initialEventDate->getTimestamp()) {
+ continue;
+ }
+
+ if ($timestamp > $until) {
+ break;
+ }
+
+ // Exclusions
+ $isExcluded = array_filter($exdates, function ($exdate) use ($timestamp) {
+ return $exdate->getTimestamp() === $timestamp;
+ });
+
+ if (isset($this->alteredRecurrenceInstances[$anEvent['UID']])) {
+ if (in_array($timestamp, $this->alteredRecurrenceInstances[$anEvent['UID']])) {
+ $isExcluded = true;
+ }
+ }
+
+ if (!$isExcluded) {
+ $eventRecurrences[] = $candidate;
+ $this->eventCount++;
+ }
+
+ // Count all evaluated candidates including excluded ones,
+ // and if RRULE[COUNT] (if set) is reached then break.
+ $count++;
+ if ($count >= $countLimit) {
+ break 2;
+ }
+ }
+
+ // Move forwards $interval $frequency.
+ $monthPreMove = $frequencyRecurringDateTime->format('m');
+ $frequencyRecurringDateTime->modify("{$interval} {$this->frequencyConversion[$frequency]}");
+
+ // As noted in Example #2 on https://www.php.net/manual/en/datetime.modify.php,
+ // there are some occasions where adding months doesn't give the month you might
+ // expect. For instance: January 31st + 1 month == March 3rd (March 2nd on a leap
+ // year.) The following code crudely rectifies this.
+ if ($frequency === 'MONTHLY') {
+ $monthDiff = $frequencyRecurringDateTime->format('m') - $monthPreMove;
+
+ if (($monthDiff > 0 && $monthDiff > $interval) || ($monthDiff < 0 && $monthDiff > $interval - 12)) {
+ $frequencyRecurringDateTime->modify('-1 month');
+ }
+ }
+
+ // $monthDays is set in the DAILY frequency if the BYMONTHDAY stanza is present in
+ // the RRULE. The variable only needs to be updated when we change months, so we
+ // unset it here, prompting a recreation next iteration.
+ if (isset($monthDays) && $frequencyRecurringDateTime->format('m') !== $monthPreMove) {
+ unset($monthDays);
+ }
+ }
+
+ unset($monthDays); // Unset it here as well, so it doesn't bleed into the calculation of the next recurring event.
+
+ // Determine event length
+ $eventLength = 0;
+ if (isset($anEvent['DURATION'])) {
+ $clonedDateTime = clone $initialEventDate;
+ $endDate = $clonedDateTime->add($anEvent['DURATION_array'][2]);
+ $eventLength = $endDate->getTimestamp() - $anEvent['DTSTART_array'][2];
+ } elseif (isset($anEvent['DTEND_array'])) {
+ $eventLength = $anEvent['DTEND_array'][2] - $anEvent['DTSTART_array'][2];
+ }
+
+ // Whether or not the initial date was UTC
+ $initialDateWasUTC = substr($anEvent['DTSTART'], -1) === 'Z';
+
+ // Build the param array
+ $dateParamArray = array();
+ if (
+ !$initialDateWasUTC
+ && isset($anEvent['DTSTART_array'][0]['TZID'])
+ && $this->isValidTimeZoneId($anEvent['DTSTART_array'][0]['TZID'])
+ ) {
+ $dateParamArray['TZID'] = $anEvent['DTSTART_array'][0]['TZID'];
+ }
+
+ // Populate the `DT{START|END}[_array]`s
+ $eventRecurrences = array_map(
+ function ($recurringDatetime) use ($anEvent, $eventLength, $initialDateWasUTC, $dateParamArray) {
+ $tzidPrefix = (isset($dateParamArray['TZID'])) ? 'TZID=' . $this->escapeParamText($dateParamArray['TZID']) . ':' : '';
+
+ foreach (array('DTSTART', 'DTEND') as $dtkey) {
+ $anEvent[$dtkey] = $recurringDatetime->format(self::DATE_TIME_FORMAT) . (($initialDateWasUTC) ? 'Z' : '');
+
+ $anEvent["{$dtkey}_array"] = array(
+ $dateParamArray, // [0] Array of params (incl. TZID)
+ $anEvent[$dtkey], // [1] ICalDateTime string w/o TZID
+ $recurringDatetime->getTimestamp(), // [2] Unix Timestamp
+ "{$tzidPrefix}{$anEvent[$dtkey]}", // [3] Full ICalDateTime string
+ );
+
+ if ($dtkey !== 'DTEND') {
+ $recurringDatetime->modify("{$eventLength} seconds");
+ }
+ }
+
+ return $anEvent;
+ },
+ $eventRecurrences
+ );
+
+ $allEventRecurrences = array_merge($allEventRecurrences, $eventRecurrences);
+ }
+
+ // Nullify the initial events that are also EXDATEs
+ foreach ($eventKeysToRemove as $eventKeyToRemove) {
+ $events[$eventKeyToRemove] = null;
+ }
+
+ $events = array_merge($events, $allEventRecurrences);
+
+ $this->cal['VEVENT'] = $events;
+ }
+
+ /**
+ * Resolves values from indices of the range 1 -> $limit.
+ *
+ * For instance, if passed [1, 4, -16] and 28, this will return [1, 4, 13].
+ *
+ * @param array $indexes
+ * @param integer $limit
+ * @return array
+ */
+ protected function resolveIndicesOfRange(array $indexes, $limit)
+ {
+ $matching = array();
+ foreach ($indexes as $index) {
+ if ($index > 0 && $index <= $limit) {
+ $matching[] = $index;
+ } elseif ($index < 0 && -$index <= $limit) {
+ $matching[] = $index + $limit + 1;
+ }
+ }
+
+ sort($matching);
+
+ return $matching;
+ }
+
+ /**
+ * Find all days of a month that match the BYDAY stanza of an RRULE.
+ *
+ * With no {ordwk}, then return the day number of every {weekday}
+ * within the month.
+ *
+ * With a +ve {ordwk}, then return the {ordwk} {weekday} within the
+ * month.
+ *
+ * With a -ve {ordwk}, then return the {ordwk}-to-last {weekday}
+ * within the month.
+ *
+ * RRule Syntax:
+ * BYDAY={bywdaylist}
+ *
+ * Where:
+ * bywdaylist = {weekdaynum}[,{weekdaynum}...]
+ * weekdaynum = [[+]{ordwk} || -{ordwk}]{weekday}
+ * ordwk = 1 to 53
+ * weekday = SU || MO || TU || WE || TH || FR || SA
+ *
+ * @param array $byDays
+ * @param \DateTime $initialDateTime
+ * @return array
+ */
+ protected function getDaysOfMonthMatchingByDayRRule(array $byDays, $initialDateTime)
+ {
+ $matchingDays = array();
+ $currentMonth = $initialDateTime->format('n');
+
+ foreach ($byDays as $weekday) {
+ $bydayDateTime = clone $initialDateTime;
+
+ $ordwk = intval(substr($weekday, 0, -2));
+
+ // Quantise the date to the first instance of the requested day in a month
+ // (Or last if we have a -ve {ordwk})
+ $bydayDateTime->modify(
+ (($ordwk < 0) ? 'Last' : 'First') .
+ ' ' .
+ $this->weekdays[substr($weekday, -2)] . // e.g. "Monday"
+ ' of ' .
+ $initialDateTime->format('F') // e.g. "June"
+ );
+
+ if ($ordwk < 0) { // -ve {ordwk}
+ $bydayDateTime->modify((++$ordwk) . ' week');
+ if ($bydayDateTime->format('n') === $currentMonth) {
+ $matchingDays[] = $bydayDateTime->format('j');
+ }
+ } elseif ($ordwk > 0) { // +ve {ordwk}
+ $bydayDateTime->modify((--$ordwk) . ' week');
+ if ($bydayDateTime->format('n') === $currentMonth) {
+ $matchingDays[] = $bydayDateTime->format('j');
+ }
+ } else { // No {ordwk}
+ while ($bydayDateTime->format('n') === $initialDateTime->format('n')) {
+ $matchingDays[] = $bydayDateTime->format('j');
+ $bydayDateTime->modify('+1 week');
+ }
+ }
+ }
+
+ // Sort into ascending order
+ sort($matchingDays);
+
+ return $matchingDays;
+ }
+
+ /**
+ * Find all days of a month that match the BYMONTHDAY stanza of an RRULE.
+ *
+ * RRUle Syntax:
+ * BYMONTHDAY={bymodaylist}
+ *
+ * Where:
+ * bymodaylist = {monthdaynum}[,{monthdaynum}...]
+ * monthdaynum = ([+] || -) {ordmoday}
+ * ordmoday = 1 to 31
+ *
+ * @param array $byMonthDays
+ * @param \DateTime $initialDateTime
+ * @return array
+ */
+ protected function getDaysOfMonthMatchingByMonthDayRRule(array $byMonthDays, $initialDateTime)
+ {
+ return $this->resolveIndicesOfRange($byMonthDays, (int) $initialDateTime->format('t'));
+ }
+
+ /**
+ * Find all days of a year that match the BYDAY stanza of an RRULE.
+ *
+ * With no {ordwk}, then return the day number of every {weekday}
+ * within the year.
+ *
+ * With a +ve {ordwk}, then return the {ordwk} {weekday} within the
+ * year.
+ *
+ * With a -ve {ordwk}, then return the {ordwk}-to-last {weekday}
+ * within the year.
+ *
+ * RRule Syntax:
+ * BYDAY={bywdaylist}
+ *
+ * Where:
+ * bywdaylist = {weekdaynum}[,{weekdaynum}...]
+ * weekdaynum = [[+]{ordwk} || -{ordwk}]{weekday}
+ * ordwk = 1 to 53
+ * weekday = SU || MO || TU || WE || TH || FR || SA
+ *
+ * @param array $byDays
+ * @param \DateTime $initialDateTime
+ * @return array
+ */
+ protected function getDaysOfYearMatchingByDayRRule(array $byDays, $initialDateTime)
+ {
+ $matchingDays = array();
+
+ foreach ($byDays as $weekday) {
+ $bydayDateTime = clone $initialDateTime;
+
+ $ordwk = intval(substr($weekday, 0, -2));
+
+ // Quantise the date to the first instance of the requested day in a year
+ // (Or last if we have a -ve {ordwk})
+ $bydayDateTime->modify(
+ (($ordwk < 0) ? 'Last' : 'First') .
+ ' ' .
+ $this->weekdays[substr($weekday, -2)] . // e.g. "Monday"
+ ' of ' . (($ordwk < 0) ? 'December' : 'January') .
+ ' ' . $initialDateTime->format('Y') // e.g. "2018"
+ );
+
+ if ($ordwk < 0) { // -ve {ordwk}
+ $bydayDateTime->modify((++$ordwk) . ' week');
+ $matchingDays[] = $bydayDateTime->format('z') + 1;
+ } elseif ($ordwk > 0) { // +ve {ordwk}
+ $bydayDateTime->modify((--$ordwk) . ' week');
+ $matchingDays[] = $bydayDateTime->format('z') + 1;
+ } else { // No {ordwk}
+ while ($bydayDateTime->format('Y') === $initialDateTime->format('Y')) {
+ $matchingDays[] = $bydayDateTime->format('z') + 1;
+ $bydayDateTime->modify('+1 week');
+ }
+ }
+ }
+
+ // Sort into ascending order
+ sort($matchingDays);
+
+ return $matchingDays;
+ }
+
+ /**
+ * Find all days of a year that match the BYYEARDAY stanza of an RRULE.
+ *
+ * RRUle Syntax:
+ * BYYEARDAY={byyrdaylist}
+ *
+ * Where:
+ * byyrdaylist = {yeardaynum}[,{yeardaynum}...]
+ * yeardaynum = ([+] || -) {ordyrday}
+ * ordyrday = 1 to 366
+ *
+ * @param array $byYearDays
+ * @param \DateTime $initialDateTime
+ * @return array
+ */
+ protected function getDaysOfYearMatchingByYearDayRRule(array $byYearDays, $initialDateTime)
+ {
+ // `\DateTime::format('L')` returns 1 if leap year, 0 if not.
+ $daysInThisYear = $initialDateTime->format('L') ? 366 : 365;
+
+ return $this->resolveIndicesOfRange($byYearDays, $daysInThisYear);
+ }
+
+ /**
+ * Find all days of a year that match the BYWEEKNO stanza of an RRULE.
+ *
+ * Unfortunately, the RFC5545 specification does not specify exactly
+ * how BYWEEKNO should expand on the initial DTSTART when provided
+ * without any other stanzas.
+ *
+ * A comparison of expansions used by other ics parsers may be found
+ * at https://github.com/s0600204/ics-parser-1/wiki/byweekno
+ *
+ * This method uses the same expansion as the python-dateutil module.
+ *
+ * RRUle Syntax:
+ * BYWEEKNO={bywknolist}
+ *
+ * Where:
+ * bywknolist = {weeknum}[,{weeknum}...]
+ * weeknum = ([+] || -) {ordwk}
+ * ordwk = 1 to 53
+ *
+ * @param array $byWeekNums
+ * @param \DateTime $initialDateTime
+ * @return array
+ */
+ protected function getDaysOfYearMatchingByWeekNoRRule(array $byWeekNums, $initialDateTime)
+ {
+ // `\DateTime::format('L')` returns 1 if leap year, 0 if not.
+ $isLeapYear = $initialDateTime->format('L');
+ $initialYear = date_create("first day of January {$initialDateTime->format('Y')}");
+ $firstDayOfTheYear = ($initialYear === false) ? null : $initialYear->format('D');
+ $weeksInThisYear = ($firstDayOfTheYear === 'Thu' || $isLeapYear && $firstDayOfTheYear === 'Wed') ? 53 : 52;
+
+ $matchingWeeks = $this->resolveIndicesOfRange($byWeekNums, $weeksInThisYear);
+ $matchingDays = array();
+ $byweekDateTime = clone $initialDateTime;
+ foreach ($matchingWeeks as $weekNum) {
+ $dayNum = $byweekDateTime->setISODate(
+ (int) $initialDateTime->format('Y'),
+ $weekNum,
+ 1
+ )->format('z') + 1;
+ for ($x = 0; $x < 7; ++$x) {
+ $matchingDays[] = $x + $dayNum;
+ }
+ }
+
+ sort($matchingDays);
+
+ return $matchingDays;
+ }
+
+ /**
+ * Find all days of a year that match the BYMONTHDAY stanza of an RRULE.
+ *
+ * RRule Syntax:
+ * BYMONTHDAY={bymodaylist}
+ *
+ * Where:
+ * bymodaylist = {monthdaynum}[,{monthdaynum}...]
+ * monthdaynum = ([+] || -) {ordmoday}
+ * ordmoday = 1 to 31
+ *
+ * @param array $byMonthDays
+ * @param \DateTime $initialDateTime
+ * @return array
+ */
+ protected function getDaysOfYearMatchingByMonthDayRRule(array $byMonthDays, $initialDateTime)
+ {
+ $matchingDays = array();
+ $monthDateTime = clone $initialDateTime;
+ for ($month = 1; $month < 13; $month++) {
+ $monthDateTime->setDate(
+ (int) $initialDateTime->format('Y'),
+ $month,
+ 1
+ );
+
+ $monthDays = $this->getDaysOfMonthMatchingByMonthDayRRule($byMonthDays, $monthDateTime);
+ foreach ($monthDays as $day) {
+ $matchingDays[] = $monthDateTime->setDate(
+ (int) $initialDateTime->format('Y'),
+ (int) $monthDateTime->format('m'),
+ $day
+ )->format('z') + 1;
+ }
+ }
+
+ return $matchingDays;
+ }
+
+ /**
+ * Filters a provided values-list by applying a BYSETPOS RRule.
+ *
+ * Where a +ve {daynum} is provided, the {ordday} position'd value as
+ * measured from the start of the list of values should be retained.
+ *
+ * Where a -ve {daynum} is provided, the {ordday} position'd value as
+ * measured from the end of the list of values should be retained.
+ *
+ * RRule Syntax:
+ * BYSETPOS={bysplist}
+ *
+ * Where:
+ * bysplist = {setposday}[,{setposday}...]
+ * setposday = {daynum}
+ * daynum = [+ || -] {ordday}
+ * ordday = 1 to 366
+ *
+ * @param array $bySetPos
+ * @param array $valuesList
+ * @return array
+ */
+ protected function filterValuesUsingBySetPosRRule(array $bySetPos, array $valuesList)
+ {
+ $filteredMatches = array();
+
+ foreach ($bySetPos as $setPosition) {
+ if ($setPosition < 0) {
+ $setPosition = count($valuesList) + ++$setPosition;
+ }
+
+ // Positioning starts at 1, array indexes start at 0
+ if (isset($valuesList[$setPosition - 1])) {
+ $filteredMatches[] = $valuesList[$setPosition - 1];
+ }
+ }
+
+ return $filteredMatches;
+ }
+
+ /**
+ * Processes date conversions using the time zone
+ *
+ * Add keys `DTSTART_tz` and `DTEND_tz` to each Event
+ * These keys contain dates adapted to the calendar
+ * time zone depending on the event `TZID`.
+ *
+ * @return void
+ * @throws \Exception
+ */
+ protected function processDateConversions()
+ {
+ $events = (isset($this->cal['VEVENT'])) ? $this->cal['VEVENT'] : array();
+
+ if ($events !== array()) {
+ foreach ($events as $key => $anEvent) {
+ if (is_null($anEvent) || !$this->isValidDate($anEvent['DTSTART'])) {
+ unset($events[$key]);
+ $this->eventCount--;
+
+ continue;
+ }
+
+ $events[$key]['DTSTART_tz'] = $this->iCalDateWithTimeZone($anEvent, 'DTSTART');
+
+ if ($this->iCalDateWithTimeZone($anEvent, 'DTEND')) {
+ $events[$key]['DTEND_tz'] = $this->iCalDateWithTimeZone($anEvent, 'DTEND');
+ } elseif ($this->iCalDateWithTimeZone($anEvent, 'DURATION')) {
+ $events[$key]['DTEND_tz'] = $this->iCalDateWithTimeZone($anEvent, 'DURATION');
+ } else {
+ $events[$key]['DTEND_tz'] = $events[$key]['DTSTART_tz'];
+ }
+ }
+
+ $this->cal['VEVENT'] = $events;
+ }
+ }
+
+ /**
+ * Returns an array of Events.
+ * Every event is a class with the event
+ * details being properties within it.
+ *
+ * @return array
+ */
+ public function events()
+ {
+ $array = $this->cal;
+ $array = isset($array['VEVENT']) ? $array['VEVENT'] : array();
+
+ $events = array();
+
+ foreach ($array as $event) {
+ $events[] = new Event($event);
+ }
+
+ return $events;
+ }
+
+ /**
+ * Returns the calendar name
+ *
+ * @return string
+ */
+ public function calendarName()
+ {
+ return isset($this->cal['VCALENDAR']['X-WR-CALNAME']) ? $this->cal['VCALENDAR']['X-WR-CALNAME'] : '';
+ }
+
+ /**
+ * Returns the calendar description
+ *
+ * @return string
+ */
+ public function calendarDescription()
+ {
+ return isset($this->cal['VCALENDAR']['X-WR-CALDESC']) ? $this->cal['VCALENDAR']['X-WR-CALDESC'] : '';
+ }
+
+ /**
+ * Returns the calendar time zone
+ *
+ * @param boolean $ignoreUtc
+ * @return string|null
+ */
+ public function calendarTimeZone($ignoreUtc = false)
+ {
+ if (isset($this->cal['VCALENDAR']['X-WR-TIMEZONE'])) {
+ $timeZone = $this->cal['VCALENDAR']['X-WR-TIMEZONE'];
+ } elseif (isset($this->cal['VTIMEZONE']['TZID'])) {
+ $timeZone = $this->cal['VTIMEZONE']['TZID'];
+ } else {
+ $timeZone = $this->defaultTimeZone;
+ }
+
+ // Validate the time zone, falling back to the time zone set in the PHP environment.
+ $timeZone = $this->timeZoneStringToDateTimeZone($timeZone)->getName();
+
+ if ($ignoreUtc && strtoupper($timeZone) === self::TIME_ZONE_UTC) {
+ return null;
+ }
+
+ return $timeZone;
+ }
+
+ /**
+ * Returns an array of arrays with all free/busy events.
+ * Every event is an associative array and each property
+ * is an element it.
+ *
+ * @return array
+ */
+ public function freeBusyEvents()
+ {
+ $array = $this->cal;
+
+ return isset($array['VFREEBUSY']) ? $array['VFREEBUSY'] : array();
+ }
+
+ /**
+ * Returns a boolean value whether the
+ * current calendar has events or not
+ *
+ * @return boolean
+ */
+ public function hasEvents()
+ {
+ return ($this->events() !== array()) ?: false;
+ }
+
+ /**
+ * Returns a sorted array of the events in a given range,
+ * or an empty array if no events exist in the range.
+ *
+ * Events will be returned if the start or end date is contained within the
+ * range (inclusive), or if the event starts before and end after the range.
+ *
+ * If a start date is not specified or of a valid format, then the start
+ * of the range will default to the current time and date of the server.
+ *
+ * If an end date is not specified or of a valid format, then the end of
+ * the range will default to the current time and date of the server,
+ * plus 20 years.
+ *
+ * Note that this function makes use of Unix timestamps. This might be a
+ * problem for events on, during, or after 29 Jan 2038.
+ * See https://en.wikipedia.org/wiki/Unix_time#Representing_the_number
+ *
+ * @param string|null $rangeStart
+ * @param string|null $rangeEnd
+ * @return array
+ * @throws \Exception
+ */
+ public function eventsFromRange($rangeStart = null, $rangeEnd = null)
+ {
+ // Sort events before processing range
+ $events = $this->sortEventsWithOrder($this->events());
+
+ if ($events === array()) {
+ return array();
+ }
+
+ $extendedEvents = array();
+
+ if (!is_null($rangeStart)) {
+ try {
+ $rangeStart = new \DateTime($rangeStart, new \DateTimeZone($this->getDefaultTimeZone()));
+ } catch (\Exception $exception) {
+ error_log("ICal::eventsFromRange: Invalid date passed ({$rangeStart})");
+ $rangeStart = false;
+ }
+ } else {
+ $rangeStart = new \DateTime('now', new \DateTimeZone($this->getDefaultTimeZone()));
+ }
+
+ if (!is_null($rangeEnd)) {
+ try {
+ $rangeEnd = new \DateTime($rangeEnd, new \DateTimeZone($this->getDefaultTimeZone()));
+ } catch (\Exception $exception) {
+ error_log("ICal::eventsFromRange: Invalid date passed ({$rangeEnd})");
+ $rangeEnd = false;
+ }
+ } else {
+ $rangeEnd = new \DateTime('now', new \DateTimeZone($this->getDefaultTimeZone()));
+ $rangeEnd->modify('+20 years');
+ }
+
+ if ($rangeEnd !== false && $rangeStart !== false) {
+ // If start and end are identical and are dates with no times...
+ if ($rangeEnd->format('His') == 0 && $rangeStart->getTimestamp() === $rangeEnd->getTimestamp()) {
+ $rangeEnd->modify('+1 day');
+ }
+
+ $rangeStart = $rangeStart->getTimestamp();
+ $rangeEnd = $rangeEnd->getTimestamp();
+ }
+
+ foreach ($events as $anEvent) {
+ $eventStart = $anEvent->dtstart_array[2];
+ $eventEnd = (isset($anEvent->dtend_array[2])) ? $anEvent->dtend_array[2] : null;
+
+ if (
+ ($eventStart >= $rangeStart && $eventStart < $rangeEnd) // Event start date contained in the range
+ || (
+ $eventEnd !== null
+ && (
+ ($eventEnd > $rangeStart && $eventEnd <= $rangeEnd) // Event end date contained in the range
+ || ($eventStart < $rangeStart && $eventEnd > $rangeEnd) // Event starts before and finishes after range
+ )
+ )
+ ) {
+ $extendedEvents[] = $anEvent;
+ }
+ }
+
+ return $extendedEvents;
+ }
+
+ /**
+ * Returns a sorted array of the events following a given string
+ *
+ * @param string $interval
+ * @return array
+ */
+ public function eventsFromInterval($interval)
+ {
+ $timeZone = $this->getDefaultTimeZone();
+ $rangeStart = new \DateTime('now', new \DateTimeZone($timeZone));
+ $rangeEnd = new \DateTime('now', new \DateTimeZone($timeZone));
+
+ $dateInterval = \DateInterval::createFromDateString($interval);
+
+ if ($dateInterval instanceof \DateInterval) {
+ $rangeEnd->add($dateInterval);
+ }
+
+ return $this->eventsFromRange($rangeStart->format('Y-m-d'), $rangeEnd->format('Y-m-d'));
+ }
+
+ /**
+ * Sorts events based on a given sort order
+ *
+ * @param array $events
+ * @param integer $sortOrder Either SORT_ASC, SORT_DESC, SORT_REGULAR, SORT_NUMERIC, SORT_STRING
+ * @return array
+ */
+ public function sortEventsWithOrder(array $events, $sortOrder = SORT_ASC)
+ {
+ $extendedEvents = array();
+ $timestamp = array();
+
+ foreach ($events as $key => $anEvent) {
+ $extendedEvents[] = $anEvent;
+ $timestamp[$key] = $anEvent->dtstart_array[2];
+ }
+
+ array_multisort($timestamp, $sortOrder, $extendedEvents);
+
+ return $extendedEvents;
+ }
+
+ /**
+ * Checks if a time zone is valid (IANA, CLDR, or Windows)
+ *
+ * @param string $timeZone
+ * @return boolean
+ */
+ protected function isValidTimeZoneId($timeZone)
+ {
+ return $this->isValidIanaTimeZoneId($timeZone) !== false
+ || $this->isValidCldrTimeZoneId($timeZone) !== false
+ || $this->isValidWindowsTimeZoneId($timeZone) !== false;
+ }
+
+ /**
+ * Checks if a time zone is a valid IANA time zone
+ *
+ * @param string $timeZone
+ * @return boolean
+ */
+ protected function isValidIanaTimeZoneId($timeZone)
+ {
+ if (in_array($timeZone, $this->validIanaTimeZones)) {
+ return true;
+ }
+
+ $valid = array();
+ $tza = timezone_abbreviations_list();
+
+ foreach ($tza as $zone) {
+ foreach ($zone as $item) {
+ $valid[$item['timezone_id']] = true;
+ }
+ }
+
+ unset($valid['']);
+
+ if (isset($valid[$timeZone]) || in_array($timeZone, timezone_identifiers_list(\DateTimeZone::ALL_WITH_BC))) {
+ $this->validIanaTimeZones[] = $timeZone;
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks if a time zone is a valid CLDR time zone
+ *
+ * @param string $timeZone
+ * @return boolean
+ */
+ public function isValidCldrTimeZoneId($timeZone)
+ {
+ return array_key_exists(html_entity_decode($timeZone), self::$cldrTimeZonesMap);
+ }
+
+ /**
+ * Checks if a time zone is a recognised Windows (non-CLDR) time zone
+ *
+ * @param string $timeZone
+ * @return boolean
+ */
+ public function isValidWindowsTimeZoneId($timeZone)
+ {
+ return array_key_exists(html_entity_decode($timeZone), self::$windowsTimeZonesMap);
+ }
+
+ /**
+ * Parses a duration and applies it to a date
+ *
+ * @param string $date
+ * @param \DateInterval $duration
+ * @return \DateTime|false
+ */
+ protected function parseDuration($date, $duration)
+ {
+ $dateTime = date_create($date);
+
+ if ($dateTime === false) {
+ return false;
+ }
+
+ $dateTime->modify("{$duration->y} year");
+ $dateTime->modify("{$duration->m} month");
+ $dateTime->modify("{$duration->d} day");
+ $dateTime->modify("{$duration->h} hour");
+ $dateTime->modify("{$duration->i} minute");
+ $dateTime->modify("{$duration->s} second");
+
+ return $dateTime;
+ }
+
+ /**
+ * Removes unprintable ASCII and UTF-8 characters
+ *
+ * @param string $data
+ * @return string|null
+ */
+ protected function removeUnprintableChars($data)
+ {
+ return preg_replace('/[\x00-\x1F\x7F]/u', '', $data);
+ }
+
+ /**
+ * Provides a polyfill for PHP 7.2's `mb_chr()`, which is a multibyte safe version of `chr()`.
+ * Multibyte safe.
+ *
+ * @param integer $code
+ * @return string
+ */
+ protected function mb_chr($code) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
+ {
+ if (function_exists('mb_chr')) {
+ return mb_chr($code);
+ } else {
+ if (($code %= 0x200000) < 0x80) {
+ $s = chr($code);
+ } elseif ($code < 0x800) {
+ $s = chr(0xc0 | $code >> 6) . chr(0x80 | $code & 0x3f);
+ } elseif ($code < 0x10000) {
+ $s = chr(0xe0 | $code >> 12) . chr(0x80 | $code >> 6 & 0x3f) . chr(0x80 | $code & 0x3f);
+ } else {
+ $s = chr(0xf0 | $code >> 18) . chr(0x80 | $code >> 12 & 0x3f) . chr(0x80 | $code >> 6 & 0x3f) . chr(0x80 | $code & 0x3f);
+ }
+
+ return $s;
+ }
+ }
+
+ /**
+ * Places double-quotes around texts that have characters not permitted
+ * in parameter-texts, but are permitted in quoted-texts.
+ *
+ * @param string $candidateText
+ * @return string
+ */
+ protected function escapeParamText($candidateText)
+ {
+ if (strpbrk($candidateText, ':;,') !== false) {
+ return '"' . $candidateText . '"';
+ }
+
+ return $candidateText;
+ }
+
+ /**
+ * Replace curly quotes and other special characters with their standard equivalents
+ * @see https://utf8-chartable.de/unicode-utf8-table.pl?start=8211&utf8=string-literal
+ *
+ * @param string $input
+ * @return string
+ */
+ protected function cleanCharacters($input)
+ {
+ return strtr(
+ $input,
+ array(
+ "\xe2\x80\x98" => "'", // ‘
+ "\xe2\x80\x99" => "'", // ’
+ "\xe2\x80\x9a" => "'", // ‚
+ "\xe2\x80\x9b" => "'", // ‛
+ "\xe2\x80\x9c" => '"', // “
+ "\xe2\x80\x9d" => '"', // ”
+ "\xe2\x80\x9e" => '"', // „
+ "\xe2\x80\x9f" => '"', // ‟
+ "\xe2\x80\x93" => '-', // –
+ "\xe2\x80\x94" => '--', // —
+ "\xe2\x80\xa6" => '...', // …
+ $this->mb_chr(145) => "'", // ‘
+ $this->mb_chr(146) => "'", // ’
+ $this->mb_chr(147) => '"', // “
+ $this->mb_chr(148) => '"', // ”
+ $this->mb_chr(150) => '-', // –
+ $this->mb_chr(151) => '--', // —
+ $this->mb_chr(133) => '...', // …
+ )
+ );
+ }
+
+ /**
+ * Parses a list of excluded dates
+ * to be applied to an Event
+ *
+ * @param array $event
+ * @return array
+ */
+ public function parseExdates(array $event)
+ {
+ if (empty($event['EXDATE_array'])) {
+ return array();
+ } else {
+ $exdates = $event['EXDATE_array'];
+ }
+
+ $output = array();
+ $currentTimeZone = new \DateTimeZone($this->getDefaultTimeZone());
+
+ foreach ($exdates as $subArray) {
+ end($subArray);
+ $finalKey = key($subArray);
+
+ foreach (array_keys($subArray) as $key) {
+ if ($key === 'TZID') {
+ $currentTimeZone = $this->timeZoneStringToDateTimeZone($subArray[$key]);
+ } elseif (is_numeric($key)) {
+ $icalDate = $subArray[$key];
+
+ if (substr($icalDate, -1) === 'Z') {
+ $currentTimeZone = new \DateTimeZone(self::TIME_ZONE_UTC);
+ }
+
+ $output[] = new \DateTime($icalDate, $currentTimeZone);
+
+ if ($key === $finalKey) {
+ // Reset to default
+ $currentTimeZone = new \DateTimeZone($this->getDefaultTimeZone());
+ }
+ }
+ }
+ }
+
+ return $output;
+ }
+
+ /**
+ * Checks if a date string is a valid date
+ *
+ * @param string $value
+ * @return boolean
+ * @throws \Exception
+ */
+ public function isValidDate($value)
+ {
+ if (!$value) {
+ return false;
+ }
+
+ try {
+ new \DateTime($value);
+
+ return true;
+ } catch (\Exception $exception) {
+ return false;
+ }
+ }
+
+ /**
+ * Checks if a filename exists as a file or URL
+ *
+ * @param string $filename
+ * @return boolean
+ */
+ protected function isFileOrUrl($filename)
+ {
+ return (file_exists($filename) || filter_var($filename, FILTER_VALIDATE_URL)) ?: false;
+ }
+
+ /**
+ * Reads an entire file or URL into an array
+ *
+ * @param string $filename
+ * @return array
+ * @throws \Exception
+ */
+ protected function fileOrUrl($filename)
+ {
+ $options = array();
+ $options['http'] = array();
+ $options['http']['header'] = array();
+
+ if ($this->httpBasicAuth !== array() || !empty($this->httpUserAgent) || !empty($this->httpAcceptLanguage)) {
+ if ($this->httpBasicAuth !== array()) {
+ $username = $this->httpBasicAuth['username'];
+ $password = $this->httpBasicAuth['password'];
+ $basicAuth = base64_encode("{$username}:{$password}");
+
+ $options['http']['header'][] = "Authorization: Basic {$basicAuth}";
+ }
+
+ if (!empty($this->httpUserAgent)) {
+ $options['http']['header'][] = "User-Agent: {$this->httpUserAgent}";
+ }
+
+ if (!empty($this->httpAcceptLanguage)) {
+ $options['http']['header'][] = "Accept-language: {$this->httpAcceptLanguage}";
+ }
+ }
+
+ if (empty($this->httpUserAgent)) {
+ if (mb_stripos($filename, 'outlook.office365.com') !== false) {
+ $options['http']['header'][] = 'User-Agent: A User Agent';
+ }
+ }
+
+ if (!empty($this->httpProtocolVersion)) {
+ $options['http']['protocol_version'] = $this->httpProtocolVersion;
+ } else {
+ $options['http']['protocol_version'] = '1.1';
+ }
+
+ $options['http']['header'][] = 'Connection: close';
+
+ $context = stream_context_create($options);
+
+ // phpcs:ignore CustomPHPCS.ControlStructures.AssignmentInCondition
+ if (($lines = file($filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES, $context)) === false) {
+ throw new \Exception("The file path or URL '{$filename}' does not exist.");
+ }
+
+ return $lines;
+ }
+
+ /**
+ * Returns a `DateTimeZone` object based on a string containing a time zone name.
+ * Falls back to the default time zone if string passed not a recognised time zone.
+ *
+ * @param string $timeZoneString
+ * @return \DateTimeZone
+ */
+ public function timeZoneStringToDateTimeZone($timeZoneString)
+ {
+ // Some time zones contain characters that are not permitted in param-texts,
+ // but are within quoted texts. We need to remove the quotes as they're not
+ // actually part of the time zone.
+ $timeZoneString = trim($timeZoneString, '"');
+ $timeZoneString = html_entity_decode($timeZoneString);
+
+ if ($this->isValidIanaTimeZoneId($timeZoneString)) {
+ return new \DateTimeZone($timeZoneString);
+ }
+
+ if ($this->isValidCldrTimeZoneId($timeZoneString)) {
+ return new \DateTimeZone(self::$cldrTimeZonesMap[$timeZoneString]);
+ }
+
+ if ($this->isValidWindowsTimeZoneId($timeZoneString)) {
+ return new \DateTimeZone(self::$windowsTimeZonesMap[$timeZoneString]);
+ }
+
+ return new \DateTimeZone($this->getDefaultTimeZone());
+ }
+}
diff --git a/lib/composer/vendor/johngrogg/ics-parser/tests/CleanCharacterTest.php b/lib/composer/vendor/johngrogg/ics-parser/tests/CleanCharacterTest.php
new file mode 100644
index 0000000..31ee582
--- /dev/null
+++ b/lib/composer/vendor/johngrogg/ics-parser/tests/CleanCharacterTest.php
@@ -0,0 +1,53 @@
+getMethod($name);
+
+ // < PHP 8.1.0
+ $method->setAccessible(true);
+
+ return $method;
+ }
+
+ public function testCleanCharactersWithUnicodeCharacters()
+ {
+ $ical = new ICal();
+
+ self::assertSame(
+ '...',
+ self::getMethod('cleanCharacters')->invokeArgs($ical, array("\xe2\x80\xa6"))
+ );
+ }
+
+ public function testCleanCharactersWithEmojis()
+ {
+ $ical = new ICal();
+ $input = 'Test with emoji 🔴👍🏻';
+
+ self::assertSame(
+ $input,
+ self::getMethod('cleanCharacters')->invokeArgs($ical, array($input))
+ );
+ }
+
+ public function testCleanCharactersWithWindowsCharacters()
+ {
+ $ical = new ICal();
+ $input = self::getMethod('mb_chr')->invokeArgs($ical, array(133));
+
+ self::assertSame(
+ '...',
+ self::getMethod('cleanCharacters')->invokeArgs($ical, array($input))
+ );
+ }
+}
diff --git a/lib/composer/vendor/johngrogg/ics-parser/tests/DynamicPropertiesTest.php b/lib/composer/vendor/johngrogg/ics-parser/tests/DynamicPropertiesTest.php
new file mode 100644
index 0000000..c683642
--- /dev/null
+++ b/lib/composer/vendor/johngrogg/ics-parser/tests/DynamicPropertiesTest.php
@@ -0,0 +1,24 @@
+events() as $event) {
+ $this->assertTrue(isset($event->dtstart_array));
+ $this->assertTrue(isset($event->dtend_array));
+ $this->assertTrue(isset($event->dtstamp_array));
+ $this->assertTrue(isset($event->uid_array));
+ $this->assertTrue(isset($event->created_array));
+ $this->assertTrue(isset($event->last_modified_array));
+ $this->assertTrue(isset($event->summary_array));
+ }
+ }
+}
diff --git a/lib/composer/vendor/johngrogg/ics-parser/tests/KeyValueTest.php b/lib/composer/vendor/johngrogg/ics-parser/tests/KeyValueTest.php
new file mode 100644
index 0000000..6bbbe6e
--- /dev/null
+++ b/lib/composer/vendor/johngrogg/ics-parser/tests/KeyValueTest.php
@@ -0,0 +1,86 @@
+ 'ATTENDEE',
+ 1 => array(
+ 0 => 'mailto:julien@ag.com',
+ 1 => array(
+ 'PARTSTAT' => 'TENTATIVE',
+ 'CN' => 'ju: @ag.com = Ju ; ',
+ ),
+ ),
+ );
+
+ $this->assertLines(
+ 'ATTENDEE;PARTSTAT=TENTATIVE;CN="ju: @ag.com = Ju ; ":mailto:julien@ag.com',
+ $checks
+ );
+ }
+
+ public function testUtf8Characters()
+ {
+ $checks = array(
+ 0 => 'ATTENDEE',
+ 1 => array(
+ 0 => 'mailto:juëǯ@ag.com',
+ 1 => array(
+ 'PARTSTAT' => 'TENTATIVE',
+ 'CN' => 'juëǯĻ',
+ ),
+ ),
+ );
+
+ $this->assertLines(
+ 'ATTENDEE;PARTSTAT=TENTATIVE;CN=juëǯĻ:mailto:juëǯ@ag.com',
+ $checks
+ );
+
+ $checks = array(
+ 0 => 'SUMMARY',
+ 1 => ' I love emojis 😀😁😁 ë, ǯ, Ļ',
+ );
+
+ $this->assertLines(
+ 'SUMMARY: I love emojis 😀😁😁 ë, ǯ, Ļ',
+ $checks
+ );
+ }
+
+ public function testParametersOfKeysWithMultipleValues()
+ {
+ $checks = array(
+ 0 => 'ATTENDEE',
+ 1 => array(
+ 0 => 'mailto:jsmith@example.com',
+ 1 => array(
+ 'DELEGATED-TO' => array(
+ 0 => 'mailto:jdoe@example.com',
+ 1 => 'mailto:jqpublic@example.com',
+ ),
+ ),
+ ),
+ );
+
+ $this->assertLines(
+ 'ATTENDEE;DELEGATED-TO="mailto:jdoe@example.com","mailto:jqpublic@example.com":mailto:jsmith@example.com',
+ $checks
+ );
+ }
+
+ private function assertLines($lines, array $checks)
+ {
+ $ical = new ICal();
+
+ self::assertSame($ical->keyValueFromString($lines), $checks);
+ }
+}
diff --git a/lib/composer/vendor/johngrogg/ics-parser/tests/RecurrencesTest.php b/lib/composer/vendor/johngrogg/ics-parser/tests/RecurrencesTest.php
new file mode 100644
index 0000000..add13eb
--- /dev/null
+++ b/lib/composer/vendor/johngrogg/ics-parser/tests/RecurrencesTest.php
@@ -0,0 +1,580 @@
+originalTimeZone = date_default_timezone_get();
+ }
+
+ /**
+ * @after
+ */
+ public function tearDownFixtures()
+ {
+ date_default_timezone_set($this->originalTimeZone);
+ }
+
+ public function testYearlyFullDayTimeZoneBerlin()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20000301', 'message' => '1st event, CET: '),
+ array('index' => 1, 'dateString' => '20010301T000000', 'message' => '2nd event, CET: '),
+ array('index' => 2, 'dateString' => '20020301T000000', 'message' => '3rd event, CET: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ array(
+ 'DTSTART;VALUE=DATE:20000301',
+ 'DTEND;VALUE=DATE:20000302',
+ 'RRULE:FREQ=YEARLY;WKST=SU;COUNT=3',
+ ),
+ 3,
+ $checks
+ );
+ }
+
+ public function testMonthlyFullDayTimeZoneBerlin()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20000301', 'message' => '1st event, CET: '),
+ array('index' => 1, 'dateString' => '20000401T000000', 'message' => '2nd event, CEST: '),
+ array('index' => 2, 'dateString' => '20000501T000000', 'message' => '3rd event, CEST: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ array(
+ 'DTSTART;VALUE=DATE:20000301',
+ 'DTEND;VALUE=DATE:20000302',
+ 'RRULE:FREQ=MONTHLY;BYMONTHDAY=1;WKST=SU;COUNT=3',
+ ),
+ 3,
+ $checks
+ );
+ }
+
+ public function testMonthlyFullDayTimeZoneBerlinSummerTime()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20180701', 'message' => '1st event, CEST: '),
+ array('index' => 1, 'dateString' => '20180801T000000', 'message' => '2nd event, CEST: '),
+ array('index' => 2, 'dateString' => '20180901T000000', 'message' => '3rd event, CEST: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ array(
+ 'DTSTART;VALUE=DATE:20180701',
+ 'DTEND;VALUE=DATE:20180702',
+ 'RRULE:FREQ=MONTHLY;WKST=SU;COUNT=3',
+ ),
+ 3,
+ $checks
+ );
+ }
+
+ public function testMonthlyFullDayTimeZoneBerlinFromFile()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20180701', 'message' => '1st event, CEST: '),
+ array('index' => 1, 'dateString' => '20180801T000000', 'message' => '2nd event, CEST: '),
+ array('index' => 2, 'dateString' => '20180901T000000', 'message' => '3rd event, CEST: '),
+ );
+ $this->assertEventFile(
+ 'Europe/Berlin',
+ './tests/ical/ical-monthly.ics',
+ 25,
+ $checks
+ );
+ }
+
+ public function testIssue196FromFile()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20191105T190000', 'timezone' => 'Europe/Berlin', 'message' => '1st event, CEST: '),
+ array('index' => 1, 'dateString' => '20191106T190000', 'timezone' => 'Europe/Berlin', 'message' => '2nd event, CEST: '),
+ array('index' => 2, 'dateString' => '20191107T190000', 'timezone' => 'Europe/Berlin', 'message' => '3rd event, CEST: '),
+ array('index' => 3, 'dateString' => '20191108T190000', 'timezone' => 'Europe/Berlin', 'message' => '4th event, CEST: '),
+ array('index' => 4, 'dateString' => '20191109T170000', 'timezone' => 'Europe/Berlin', 'message' => '5th event, CEST: '),
+ array('index' => 5, 'dateString' => '20191110T180000', 'timezone' => 'Europe/Berlin', 'message' => '6th event, CEST: '),
+ );
+ $this->assertEventFile(
+ 'UTC',
+ './tests/ical/issue-196.ics',
+ 7,
+ $checks
+ );
+ }
+
+ public function testWeeklyFullDayTimeZoneBerlin()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20000301', 'message' => '1st event, CET: '),
+ array('index' => 1, 'dateString' => '20000308T000000', 'message' => '2nd event, CET: '),
+ array('index' => 2, 'dateString' => '20000315T000000', 'message' => '3rd event, CET: '),
+ array('index' => 3, 'dateString' => '20000322T000000', 'message' => '4th event, CET: '),
+ array('index' => 4, 'dateString' => '20000329T000000', 'message' => '5th event, CEST: '),
+ array('index' => 5, 'dateString' => '20000405T000000', 'message' => '6th event, CEST: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ array(
+ 'DTSTART;VALUE=DATE:20000301',
+ 'DTEND;VALUE=DATE:20000302',
+ 'RRULE:FREQ=WEEKLY;WKST=SU;COUNT=6',
+ ),
+ 6,
+ $checks
+ );
+ }
+
+ public function testDailyFullDayTimeZoneBerlin()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20000301', 'message' => '1st event, CET: '),
+ array('index' => 1, 'dateString' => '20000302T000000', 'message' => '2nd event, CET: '),
+ array('index' => 30, 'dateString' => '20000331T000000', 'message' => '31st event, CEST: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ array(
+ 'DTSTART;VALUE=DATE:20000301',
+ 'DTEND;VALUE=DATE:20000302',
+ 'RRULE:FREQ=DAILY;WKST=SU;COUNT=31',
+ ),
+ 31,
+ $checks
+ );
+ }
+
+ public function testWeeklyFullDayTimeZoneBerlinLocal()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20000301T000000', 'message' => '1st event, CET: '),
+ array('index' => 1, 'dateString' => '20000308T000000', 'message' => '2nd event, CET: '),
+ array('index' => 2, 'dateString' => '20000315T000000', 'message' => '3rd event, CET: '),
+ array('index' => 3, 'dateString' => '20000322T000000', 'message' => '4th event, CET: '),
+ array('index' => 4, 'dateString' => '20000329T000000', 'message' => '5th event, CEST: '),
+ array('index' => 5, 'dateString' => '20000405T000000', 'message' => '6th event, CEST: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ array(
+ 'DTSTART;TZID=Europe/Berlin:20000301T000000',
+ 'DTEND;TZID=Europe/Berlin:20000302T000000',
+ 'RRULE:FREQ=WEEKLY;WKST=SU;COUNT=6',
+ ),
+ 6,
+ $checks
+ );
+ }
+
+ public function testRFCDaily10NewYork()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'timezone' => 'America/New_York', 'message' => '1st event, EDT: '),
+ array('index' => 1, 'dateString' => '19970903T090000', 'timezone' => 'America/New_York', 'message' => '2nd event, EDT: '),
+ array('index' => 9, 'dateString' => '19970911T090000', 'timezone' => 'America/New_York', 'message' => '10th event, EDT: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=DAILY;COUNT=10',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ public function testRFCDaily10Berlin()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'timezone' => 'Europe/Berlin', 'message' => '1st event, CEST: '),
+ array('index' => 1, 'dateString' => '19970903T090000', 'timezone' => 'Europe/Berlin', 'message' => '2nd event, CEST: '),
+ array('index' => 9, 'dateString' => '19970911T090000', 'timezone' => 'Europe/Berlin', 'message' => '10th event, CEST: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ array(
+ 'DTSTART;TZID=Europe/Berlin:19970902T090000',
+ 'RRULE:FREQ=DAILY;COUNT=10',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ public function testStartDateIsExdateUsingUntil()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20190918T095000', 'timezone' => 'Europe/London', 'message' => '1st event: '),
+ array('index' => 1, 'dateString' => '20191002T095000', 'timezone' => 'Europe/London', 'message' => '2nd event: '),
+ array('index' => 2, 'dateString' => '20191016T095000', 'timezone' => 'Europe/London', 'message' => '3rd event: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/London',
+ array(
+ 'DTSTART;TZID=Europe/London:20190911T095000',
+ 'RRULE:FREQ=WEEKLY;WKST=SU;UNTIL=20191027T235959Z;BYDAY=WE',
+ 'EXDATE;TZID=Europe/London:20191023T095000',
+ 'EXDATE;TZID=Europe/London:20191009T095000',
+ 'EXDATE;TZID=Europe/London:20190925T095000',
+ 'EXDATE;TZID=Europe/London:20190911T095000',
+ ),
+ 3,
+ $checks
+ );
+ }
+
+ public function testStartDateIsExdateUsingCount()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20190918T095000', 'timezone' => 'Europe/London', 'message' => '1st event: '),
+ array('index' => 1, 'dateString' => '20191002T095000', 'timezone' => 'Europe/London', 'message' => '2nd event: '),
+ array('index' => 2, 'dateString' => '20191016T095000', 'timezone' => 'Europe/London', 'message' => '3rd event: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/London',
+ array(
+ 'DTSTART;TZID=Europe/London:20190911T095000',
+ 'RRULE:FREQ=WEEKLY;WKST=SU;COUNT=7;BYDAY=WE',
+ 'EXDATE;TZID=Europe/London:20191023T095000',
+ 'EXDATE;TZID=Europe/London:20191009T095000',
+ 'EXDATE;TZID=Europe/London:20190925T095000',
+ 'EXDATE;TZID=Europe/London:20190911T095000',
+ ),
+ 3,
+ $checks
+ );
+ }
+
+ public function testCountWithExdate()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20200323T050000', 'timezone' => 'Europe/Paris', 'message' => '1st event: '),
+ array('index' => 1, 'dateString' => '20200324T050000', 'timezone' => 'Europe/Paris', 'message' => '2nd event: '),
+ array('index' => 2, 'dateString' => '20200327T050000', 'timezone' => 'Europe/Paris', 'message' => '3rd event: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/London',
+ array(
+ 'DTSTART;TZID=Europe/Paris:20200323T050000',
+ 'DTEND;TZID=Europe/Paris:20200323T070000',
+ 'RRULE:FREQ=DAILY;COUNT=5',
+ 'EXDATE;TZID=Europe/Paris:20200326T050000',
+ 'EXDATE;TZID=Europe/Paris:20200325T050000',
+ 'DTSTAMP:20200318T141057Z',
+ ),
+ 3,
+ $checks
+ );
+ }
+
+ public function testRFCDaily10BerlinFromNewYork()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'timezone' => 'Europe/Berlin', 'message' => '1st event, CEST: '),
+ array('index' => 1, 'dateString' => '19970903T090000', 'timezone' => 'Europe/Berlin', 'message' => '2nd event, CEST: '),
+ array('index' => 9, 'dateString' => '19970911T090000', 'timezone' => 'Europe/Berlin', 'message' => '10th event, CEST: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=Europe/Berlin:19970902T090000',
+ 'RRULE:FREQ=DAILY;COUNT=10',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ public function testExdatesInDifferentTimezone()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20170503T190000', 'message' => '1st event: '),
+ array('index' => 1, 'dateString' => '20170510T190000', 'message' => '2nd event: '),
+ array('index' => 9, 'dateString' => '20170712T190000', 'message' => '10th event: '),
+ array('index' => 19, 'dateString' => '20171004T190000', 'message' => '20th event: '),
+ );
+ $this->assertVEVENT(
+ 'America/Chicago',
+ array(
+ 'DTSTART;TZID=America/Chicago:20170503T190000',
+ 'RRULE:FREQ=WEEKLY;BYDAY=WE;WKST=SU;UNTIL=20180101',
+ 'EXDATE:20170601T000000Z',
+ 'EXDATE:20170803T000000Z',
+ 'EXDATE:20170824T000000Z',
+ 'EXDATE:20171026T000000Z',
+ 'EXDATE:20171102T000000Z',
+ 'EXDATE:20171123T010000Z',
+ 'EXDATE:20171221T010000Z',
+ ),
+ 28,
+ $checks
+ );
+ }
+
+ public function testYearlyWithBySetPos()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970306T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970313T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970325T090000', 'message' => '3rd occurrence: '),
+ array('index' => 3, 'dateString' => '19980305T090000', 'message' => '4th occurrence: '),
+ array('index' => 4, 'dateString' => '19980312T090000', 'message' => '5th occurrence: '),
+ array('index' => 5, 'dateString' => '19980326T090000', 'message' => '6th occurrence: '),
+ array('index' => 9, 'dateString' => '20000307T090000', 'message' => '10th occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970306T090000',
+ 'RRULE:FREQ=YEARLY;COUNT=10;BYMONTH=3;BYDAY=TU,TH;BYSETPOS=2,4,-2',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ public function testDailyWithByMonthDay()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20000206T120000', 'message' => '1st event: '),
+ array('index' => 1, 'dateString' => '20000211T120000', 'message' => '2nd event: '),
+ array('index' => 2, 'dateString' => '20000216T120000', 'message' => '3rd event: '),
+ array('index' => 4, 'dateString' => '20000226T120000', 'message' => '5th event, transition from February to March: '),
+ array('index' => 5, 'dateString' => '20000301T120000', 'message' => '6th event, transition to March from February: '),
+ array('index' => 11, 'dateString' => '20000331T120000', 'message' => '12th event, transition from March to April: '),
+ array('index' => 12, 'dateString' => '20000401T120000', 'message' => '13th event, transition to April from March: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ array(
+ 'DTSTART:20000206T120000',
+ 'DTEND:20000206T130000',
+ 'RRULE:FREQ=DAILY;BYMONTHDAY=1,6,11,16,21,26,31;COUNT=16',
+ ),
+ 16,
+ $checks
+ );
+ }
+
+ public function testYearlyWithByMonthDay()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20001214T120000', 'message' => '1st event: '),
+ array('index' => 1, 'dateString' => '20001221T120000', 'message' => '2nd event: '),
+ array('index' => 2, 'dateString' => '20010107T120000', 'message' => '3rd event: '),
+ array('index' => 3, 'dateString' => '20010114T120000', 'message' => '4th event: '),
+ array('index' => 6, 'dateString' => '20010214T120000', 'message' => '7th event: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ array(
+ 'DTSTART:20001214T120000',
+ 'DTEND:20001214T130000',
+ 'RRULE:FREQ=YEARLY;BYMONTHDAY=7,14,21;COUNT=8',
+ ),
+ 8,
+ $checks
+ );
+ }
+
+ public function testYearlyWithByMonthDayAndByDay()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20001214T120000', 'message' => '1st event: '),
+ array('index' => 1, 'dateString' => '20001221T120000', 'message' => '2nd event: '),
+ array('index' => 2, 'dateString' => '20010607T120000', 'message' => '3rd event: '),
+ array('index' => 3, 'dateString' => '20010614T120000', 'message' => '4th event: '),
+ array('index' => 6, 'dateString' => '20020214T120000', 'message' => '7th event: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ array(
+ 'DTSTART:20001214T120000',
+ 'DTEND:20001214T130000',
+ 'RRULE:FREQ=YEARLY;BYMONTHDAY=7,14,21;BYDAY=TH;COUNT=8',
+ ),
+ 8,
+ $checks
+ );
+ }
+
+ public function testYearlyWithByMonthAndByMonthDay()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20001214T120000', 'message' => '1st event: '),
+ array('index' => 1, 'dateString' => '20001221T120000', 'message' => '2nd event: '),
+ array('index' => 2, 'dateString' => '20010607T120000', 'message' => '3rd event: '),
+ array('index' => 3, 'dateString' => '20010614T120000', 'message' => '4th event: '),
+ array('index' => 6, 'dateString' => '20011214T120000', 'message' => '7th event: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ array(
+ 'DTSTART:20001214T120000',
+ 'DTEND:20001214T130000',
+ 'RRULE:FREQ=YEARLY;BYMONTH=12,6;BYMONTHDAY=7,14,21;COUNT=8',
+ ),
+ 8,
+ $checks
+ );
+ }
+
+ public function testCountIsOne()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20211201T090000', 'message' => '1st and only expected event: '),
+ );
+ $this->assertVEVENT(
+ 'UTC',
+ array(
+ 'DTSTART:20211201T090000',
+ 'DTEND:20211201T100000',
+ 'RRULE:FREQ=DAILY;COUNT=1',
+ ),
+ 1,
+ $checks
+ );
+ }
+
+ public function test5thByDayOfMonth()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20200103T090000', 'message' => '1st event: '),
+ array('index' => 1, 'dateString' => '20200129T090000', 'message' => '2nd event: '),
+ array('index' => 2, 'dateString' => '20200429T090000', 'message' => '3rd event: '),
+ array('index' => 3, 'dateString' => '20200501T090000', 'message' => '4th event: '),
+ array('index' => 4, 'dateString' => '20200703T090000', 'message' => '5th event: '),
+ array('index' => 5, 'dateString' => '20200729T090000', 'message' => '6th event: '),
+ array('index' => 6, 'dateString' => '20200930T090000', 'message' => '7th event: '),
+ array('index' => 7, 'dateString' => '20201002T090000', 'message' => '8th event: '),
+ array('index' => 8, 'dateString' => '20201230T090000', 'message' => '9th event: '),
+ array('index' => 9, 'dateString' => '20210101T090000', 'message' => '10th and last event: '),
+ );
+ $this->assertVEVENT(
+ 'UTC',
+ array(
+ 'DTSTART:20200103T090000',
+ 'DTEND:20200103T100000',
+ 'RRULE:FREQ=MONTHLY;BYDAY=5WE,-5FR;UNTIL=20210102T090000',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ public function assertVEVENT($defaultTimezone, $veventParts, $count, $checks)
+ {
+ $options = $this->getOptions($defaultTimezone);
+
+ $testIcal = implode(PHP_EOL, $this->getIcalHeader());
+ $testIcal .= PHP_EOL;
+ $testIcal .= implode(PHP_EOL, $this->formatIcalEvent($veventParts));
+ $testIcal .= PHP_EOL;
+ $testIcal .= implode(PHP_EOL, $this->getIcalFooter());
+
+ $ical = new ICal(false, $options);
+ $ical->initString($testIcal);
+
+ $events = $ical->events();
+
+ $this->assertCount($count, $events);
+
+ foreach ($checks as $check) {
+ $this->assertEvent($events[$check['index']], $check['dateString'], $check['message'], isset($check['timezone']) ? $check['timezone'] : $defaultTimezone);
+ }
+ }
+
+ public function assertEventFile($defaultTimezone, $file, $count, $checks)
+ {
+ $options = $this->getOptions($defaultTimezone);
+
+ $ical = new ICal($file, $options);
+
+ $events = $ical->events();
+
+ $this->assertCount($count, $events);
+
+ $events = $ical->sortEventsWithOrder($events);
+
+ foreach ($checks as $check) {
+ $this->assertEvent($events[$check['index']], $check['dateString'], $check['message'], isset($check['timezone']) ? $check['timezone'] : $defaultTimezone);
+ }
+ }
+
+ public function assertEvent($event, $expectedDateString, $message, $timeZone = null)
+ {
+ if (!is_null($timeZone)) {
+ date_default_timezone_set($timeZone);
+ }
+
+ $expectedTimeStamp = strtotime($expectedDateString);
+
+ $this->assertSame($expectedTimeStamp, $event->dtstart_array[2], $message . 'timestamp mismatch (expected ' . $expectedDateString . ' vs actual ' . $event->dtstart . ')');
+ $this->assertSame($expectedDateString, $event->dtstart, $message . 'dtstart mismatch (timestamp is okay)');
+ }
+
+ public function getOptions($defaultTimezone)
+ {
+ $options = array(
+ 'defaultSpan' => 2, // Default value
+ 'defaultTimeZone' => $defaultTimezone, // Default value: UTC
+ 'defaultWeekStart' => 'MO', // Default value
+ 'disableCharacterReplacement' => false, // Default value
+ 'filterDaysAfter' => null, // Default value
+ 'filterDaysBefore' => null, // Default value
+ 'httpUserAgent' => null, // Default value
+ 'skipRecurrence' => false, // Default value
+ );
+
+ return $options;
+ }
+
+ public function formatIcalEvent($veventParts)
+ {
+ return array_merge(
+ array(
+ 'BEGIN:VEVENT',
+ 'CREATED:' . gmdate('Ymd\THis\Z'),
+ 'UID:M2CD-1-1-5FB000FB-BBE4-4F3F-9E7E-217F1FF97209',
+ ),
+ $veventParts,
+ array(
+ 'SUMMARY:test',
+ 'LAST-MODIFIED:' . gmdate('Ymd\THis\Z', filemtime(__FILE__)),
+ 'END:VEVENT',
+ )
+ );
+ }
+
+ public function getIcalHeader()
+ {
+ return array(
+ 'BEGIN:VCALENDAR',
+ 'VERSION:2.0',
+ 'PRODID:-//Google Inc//Google Calendar 70.9054//EN',
+ 'X-WR-CALNAME:Private',
+ 'X-APPLE-CALENDAR-COLOR:#FF2968',
+ 'X-WR-CALDESC:',
+ );
+ }
+
+ public function getIcalFooter()
+ {
+ return array('END:VCALENDAR');
+ }
+}
diff --git a/lib/composer/vendor/johngrogg/ics-parser/tests/Rfc5545RecurrenceTest.php b/lib/composer/vendor/johngrogg/ics-parser/tests/Rfc5545RecurrenceTest.php
new file mode 100644
index 0000000..a617445
--- /dev/null
+++ b/lib/composer/vendor/johngrogg/ics-parser/tests/Rfc5545RecurrenceTest.php
@@ -0,0 +1,1059 @@
+originalTimeZone = date_default_timezone_get();
+ }
+
+ /**
+ * @after
+ */
+ public function tearDownFixtures()
+ {
+ date_default_timezone_set($this->originalTimeZone);
+ }
+
+ // Page 123, Test 1 :: Daily, 10 Occurrences
+ public function test_page123_test1()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970903T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970904T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=DAILY;COUNT=10',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ // Page 123, Test 2 :: Daily, until December 24th
+ public function test_page123_test2()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970903T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970904T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=DAILY;UNTIL=19971224T000000Z',
+ ),
+ 113,
+ $checks
+ );
+ }
+
+ // Page 123, Test 3 :: Daily, until December 24th, with trailing semicolon
+ public function test_page123_test3()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970903T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970904T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=DAILY;UNTIL=19971224T000000Z;',
+ ),
+ 113,
+ $checks
+ );
+ }
+
+ // Page 124, Test 1 :: Daily, every other day, Forever
+ //
+ // UNTIL rule does not exist in original example
+ public function test_page124_test1()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970904T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970906T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=DAILY;INTERVAL=2;UNTIL=19971201Z',
+ ),
+ 45,
+ $checks
+ );
+ }
+
+ // Page 124, Test 2 :: Daily, 10-day intervals, 5 occurrences
+ public function test_page124_test2()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970912T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970922T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5',
+ ),
+ 5,
+ $checks
+ );
+ }
+
+ // Page 124, Test 3a :: Every January day, for 3 years (Variant A)
+ public function test_page124_test3a()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19980101T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19980102T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19980103T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19980101T090000',
+ 'RRULE:FREQ=YEARLY;UNTIL=20000131T140000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA',
+ ),
+ 93,
+ $checks
+ );
+ }
+
+ /* Requires support for BYMONTH under DAILY [No ticket]
+ *
+ // Page 124, Test 3b :: Every January day, for 3 years (Variant B)
+ public function test_page124_test3b()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19980101T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19980102T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19980103T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19980101T090000',
+ 'RRULE:FREQ=DAILY;UNTIL=20000131T140000Z;BYMONTH=1',
+ ),
+ 93,
+ $checks
+ );
+ }
+ */
+
+ // Page 124, Test 4 :: Weekly, 10 occurrences
+ public function test_page124_test4()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970909T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970916T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=WEEKLY;COUNT=10',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ // Page 125, Test 1 :: Weekly, until December 24th
+ public function test_page125_test1()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970909T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970916T090000', 'message' => '3rd occurrence: '),
+ array('index' => 16, 'dateString' => '19971223T090000', 'message' => 'last occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=WEEKLY;UNTIL=19971224T000000Z',
+ ),
+ 17,
+ $checks
+ );
+ }
+
+ // Page 125, Test 2 :: Every other week, forever
+ //
+ // UNTIL rule does not exist in original example
+ public function test_page125_test2()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970916T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970930T090000', 'message' => '3rd occurrence: '),
+ array('index' => 3, 'dateString' => '19971014T090000', 'message' => '4th occurrence: '),
+ array('index' => 4, 'dateString' => '19971028T090000', 'message' => '5th occurrence: '),
+ array('index' => 5, 'dateString' => '19971111T090000', 'message' => '6th occurrence: '),
+ array('index' => 6, 'dateString' => '19971125T090000', 'message' => '7th occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=WEEKLY;INTERVAL=2;WKST=SU;UNTIL=19971201Z',
+ ),
+ 7,
+ $checks
+ );
+ }
+
+ // Page 125, Test 3a :: Tuesday & Thursday every week, for five weeks (Variant A)
+ public function test_page125_test3a()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970904T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970909T090000', 'message' => '3rd occurrence: '),
+ array('index' => 9, 'dateString' => '19971002T090000', 'message' => 'final occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ // Page 125, Test 3b :: Tuesday & Thursday every week, for five weeks (Variant B)
+ public function test_page125_test3b()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970904T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970909T090000', 'message' => '3rd occurrence: '),
+ array('index' => 9, 'dateString' => '19971002T090000', 'message' => 'final occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=WEEKLY;COUNT=10;WKST=SU;BYDAY=TU,TH',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ // Page 125, Test 4 :: Monday, Wednesday & Friday of every other week until December 24th
+ public function test_page125_test4()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970901T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970903T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970905T090000', 'message' => '3rd occurrence: '),
+ array('index' => 24, 'dateString' => '19971222T090000', 'message' => 'final occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970901T090000',
+ 'RRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR',
+ ),
+ 25,
+ $checks
+ );
+ }
+
+ // Page 126, Test 1 :: Tuesday & Thursday, every other week, for 8 occurrences
+ public function test_page126_test1()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970904T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970916T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH',
+ ),
+ 8,
+ $checks
+ );
+ }
+
+ // Page 126, Test 2 :: First Friday of the Month, for 10 occurrences
+ public function test_page126_test2()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970905T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19971003T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19971107T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970905T090000',
+ 'RRULE:FREQ=MONTHLY;COUNT=10;BYDAY=1FR',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ // Page 126, Test 3 :: First Friday of the Month, until 24th December
+ public function test_page126_test3()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970905T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19971003T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19971107T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970905T090000',
+ 'RRULE:FREQ=MONTHLY;UNTIL=19971224T000000Z;BYDAY=1FR',
+ ),
+ 4,
+ $checks
+ );
+ }
+
+ // Page 126, Test 4 :: First and last Sunday, every other Month, for 10 occurrences
+ public function test_page126_test4()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970907T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970928T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19971102T090000', 'message' => '3rd occurrence: '),
+ array('index' => 3, 'dateString' => '19971130T090000', 'message' => '4th occurrence: '),
+ array('index' => 4, 'dateString' => '19980104T090000', 'message' => '5th occurrence: '),
+ array('index' => 5, 'dateString' => '19980125T090000', 'message' => '6th occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970907T090000',
+ 'RRULE:FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ // Page 126, Test 5 :: Second-to-last Monday of the Month, for six months
+ public function test_page126_test5()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970922T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19971020T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19971117T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970922T090000',
+ 'RRULE:FREQ=MONTHLY;COUNT=6;BYDAY=-2MO',
+ ),
+ 6,
+ $checks
+ );
+ }
+
+ // Page 127, Test 1 :: Third-to-last day of the month, forever
+ //
+ // UNTIL rule does not exist in original example.
+ public function test_page127_test1()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970928T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19971029T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19971128T090000', 'message' => '3rd occurrence: '),
+ array('index' => 3, 'dateString' => '19971229T090000', 'message' => '4th occurrence: '),
+ array('index' => 4, 'dateString' => '19980129T090000', 'message' => '5th occurrence: '),
+ array('index' => 5, 'dateString' => '19980226T090000', 'message' => '6th occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970928T090000',
+ 'RRULE:FREQ=MONTHLY;BYMONTHDAY=-3;UNTIL=19980401',
+ ),
+ 7,
+ $checks
+ );
+ }
+
+ // Page 127, Test 2 :: 2nd and 15th of each Month, for 10 occurrences
+ public function test_page127_test2()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970915T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19971002T090000', 'message' => '3rd occurrence: '),
+ array('index' => 3, 'dateString' => '19971015T090000', 'message' => '4th occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ // Page 127, Test 3 :: First and last day of the month, for 10 occurrences
+ public function test_page127_test3()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970930T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19971001T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19971031T090000', 'message' => '3rd occurrence: '),
+ array('index' => 3, 'dateString' => '19971101T090000', 'message' => '4th occurrence: '),
+ array('index' => 4, 'dateString' => '19971130T090000', 'message' => '5th occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970930T090000',
+ 'RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ // Page 127, Test 4 :: 10th through 15th, every 18 months, for 10 occurrences
+ public function test_page127_test4()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970910T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970911T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970912T090000', 'message' => '3rd occurrence: '),
+ array('index' => 6, 'dateString' => '19990310T090000', 'message' => '7th occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970910T090000',
+ 'RRULE:FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ // Page 127, Test 5 :: Every Tuesday, every other Month, forever
+ //
+ // UNTIL rule does not exist in original example.
+ public function test_page127_test5()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970909T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970916T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=MONTHLY;INTERVAL=2;BYDAY=TU;UNTIL=19980101',
+ ),
+ 9,
+ $checks
+ );
+ }
+
+ // Page 128, Test 1 :: June & July of each Year, for 10 occurrences
+ public function test_page128_test1()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970610T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970710T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19980610T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970610T090000',
+ 'RRULE:FREQ=YEARLY;COUNT=10;BYMONTH=6,7',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ // Page 128, Test 2 :: January, February, & March, every other Year, for 10 occurrences
+ public function test_page128_test2()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970310T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19990110T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19990210T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970310T090000',
+ 'RRULE:FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ // Page 128, Test 3 :: Every third Year on the 1st, 100th, & 200th day for 10 occurrences
+ public function test_page128_test3()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970101T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970410T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970719T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970101T090000',
+ 'RRULE:FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200',
+ ),
+ 10,
+ $checks
+ );
+ }
+
+ // Page 128, Test 4 :: 20th Monday of a Year, forever
+ //
+ // COUNT rule does not exist in original example.
+ public function test_page128_test4()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970519T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19980518T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19990517T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970519T090000',
+ 'RRULE:FREQ=YEARLY;BYDAY=20MO;COUNT=4',
+ ),
+ 4,
+ $checks
+ );
+ }
+
+ // Page 129, Test 1 :: Monday of Week 20, where the default start of the week is Monday, forever
+ //
+ // COUNT rule does not exist in original example.
+ public function test_page129_test1()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970512T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19980511T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19990517T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970512T090000',
+ 'RRULE:FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO;COUNT=4',
+ ),
+ 4,
+ $checks
+ );
+ }
+
+ // Page 129, Test 2 :: Every Thursday in March, forever
+ //
+ // UNTIL rule does not exist in original example.
+ public function test_page129_test2()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970313T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970320T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970327T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970313T090000',
+ 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=TH;UNTIL=19990401Z',
+ ),
+ 11,
+ $checks
+ );
+ }
+
+ // Page 129, Test 3 :: Every Thursday in June, July, & August, forever
+ //
+ // UNTIL rule does not exist in original example.
+ public function test_page129_test3()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970605T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970612T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970619T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970605T090000',
+ 'RRULE:FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8;UNTIL=19970901Z',
+ ),
+ 13,
+ $checks
+ );
+ }
+
+ /* Requires support for BYMONTHDAY and BYDAY in the same MONTHLY RRULE [No ticket]
+ *
+ // Page 129, Test 4 :: Every Friday 13th, forever
+ //
+ // COUNT rule does not exist in original example.
+ public function test_page129_test4()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19980213T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19980313T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19981113T090000', 'message' => '3rd occurrence: '),
+ array('index' => 3, 'dateString' => '19990813T090000', 'message' => '4th occurrence: '),
+ array('index' => 4, 'dateString' => '20001013T090000', 'message' => '5th occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'EXDATE;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13;COUNT=5',
+ ),
+ 5,
+ $checks
+ );
+ }
+ */
+
+ // Page 130, Test 1 :: The first Saturday that follows the first Sunday of the month, forever:
+ //
+ // COUNT rule does not exist in original example.
+ public function test_page130_test1()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970913T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19971011T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19971108T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970913T090000',
+ 'RRULE:FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13;COUNT=7',
+ ),
+ 7,
+ $checks
+ );
+ }
+
+ // Page 130, Test 2 :: The first Tuesday after a Monday in November, every 4 Years (U.S. Presidential Election Day), forever
+ //
+ // COUNT rule does not exist in original example.
+ public function test_page130_test2()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19961105T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '20001107T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '20041102T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19961105T090000',
+ 'RRULE:FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8;COUNT=4',
+ ),
+ 4,
+ $checks
+ );
+ }
+
+ // Page 130, Test 3 :: Third instance of either a Tuesday, Wednesday, or Thursday of a Month, for 3 months.
+ public function test_page130_test3()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970904T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19971007T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19971106T090000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970904T090000',
+ 'RRULE:FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3',
+ ),
+ 3,
+ $checks
+ );
+ }
+
+ // Page 130, Test 4 :: Second-to-last weekday of the month, indefinitely
+ //
+ // UNTIL rule does not exist in original example.
+ public function test_page130_test4()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970929T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19971030T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19971127T090000', 'message' => '3rd occurrence: '),
+ array('index' => 3, 'dateString' => '19971230T090000', 'message' => '4th occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970929T090000',
+ 'RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2;UNTIL=19980101',
+ ),
+ 4,
+ $checks
+ );
+ }
+
+ /* Requires support of HOURLY frequency [#101]
+ *
+ // Page 131, Test 1 :: Every 3 hours from 09:00 to 17:00 on a specific day
+ public function test_page131_test1()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970902T120000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970902T150000', 'message' => '3rd occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T170000Z',
+ ),
+ 3,
+ $checks
+ );
+ }
+ */
+
+ /* Requires support of MINUTELY frequency [#101]
+ *
+ // Page 131, Test 2 :: Every 15 minutes for 6 occurrences
+ public function test_page131_test2()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970902T091500', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970902T093000', 'message' => '3rd occurrence: '),
+ array('index' => 3, 'dateString' => '19970902T094500', 'message' => '4th occurrence: '),
+ array('index' => 4, 'dateString' => '19970902T100000', 'message' => '5th occurrence: '),
+ array('index' => 5, 'dateString' => '19970902T101500', 'message' => '6th occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=MINUTELY;INTERVAL=15;COUNT=6',
+ ),
+ 6,
+ $checks
+ );
+ }
+ */
+
+ /* Requires support of MINUTELY frequency [#101]
+ *
+ // Page 131, Test 3 :: Every hour and a half for 4 occurrences
+ public function test_page131_test3()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970902T103000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970902T120000', 'message' => '3rd occurrence: '),
+ array('index' => 3, 'dateString' => '19970902T133000', 'message' => '4th occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=MINUTELY;INTERVAL=90;COUNT=4',
+ ),
+ 4,
+ $checks
+ );
+ }
+ */
+
+ /* Requires support of BYHOUR and BYMINUTE under DAILY [#11]
+ *
+ // Page 131, Test 4a :: Every 20 minutes from 9:00 to 16:40 every day, using DAILY
+ //
+ // UNTIL rule does not exist in original example
+ public function test_page131_test4a()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence, Day 1: '),
+ array('index' => 1, 'dateString' => '19970902T092000', 'message' => '2nd occurrence, Day 1: '),
+ array('index' => 2, 'dateString' => '19970902T094000', 'message' => '3rd occurrence, Day 1: '),
+ array('index' => 3, 'dateString' => '19970902T100000', 'message' => '4th occurrence, Day 1: '),
+ array('index' => 20, 'dateString' => '19970902T164000', 'message' => 'Last occurrence, Day 1: '),
+ array('index' => 21, 'dateString' => '19970903T090000', 'message' => '1st occurrence, Day 2: '),
+ array('index' => 41, 'dateString' => '19970903T164000', 'message' => 'Last occurrence, Day 2: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40;UNTIL=19970904T000000Z',
+ ),
+ 42,
+ $checks
+ );
+ }
+ */
+
+ /* Requires support of MINUTELY frequency [#101]
+ *
+ // Page 131, Test 4b :: Every 20 minutes from 9:00 to 16:40 every day, using MINUTELY
+ //
+ // UNTIL rule does not exist in original example
+ public function test_page131_test4b()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence, Day 1: '),
+ array('index' => 1, 'dateString' => '19970902T092000', 'message' => '2nd occurrence, Day 1: '),
+ array('index' => 2, 'dateString' => '19970902T094000', 'message' => '3rd occurrence, Day 1: '),
+ array('index' => 3, 'dateString' => '19970902T100000', 'message' => '4th occurrence, Day 1: '),
+ array('index' => 20, 'dateString' => '19970902T164000', 'message' => 'Last occurrence, Day 1: '),
+ array('index' => 21, 'dateString' => '19970903T090000', 'message' => '1st occurrence, Day 2: '),
+ array('index' => 41, 'dateString' => '19970903T164000', 'message' => 'Last occurrence, Day 2: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970902T090000',
+ 'RRULE:FREQ=MINUTELY;INTERVAL=20;BYHOUR=9,10,11,12,13,14,15,16;UNTIL=19970904T000000Z',
+ ),
+ 42,
+ $checks
+ );
+ }
+ */
+
+ // Page 131, Test 5a :: Changing the passed WKST rule, before...
+ public function test_page131_test5a()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970805T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970810T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970819T090000', 'message' => '3rd occurrence: '),
+ array('index' => 3, 'dateString' => '19970824T090000', 'message' => '4th occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970805T090000',
+ 'RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO',
+ ),
+ 4,
+ $checks
+ );
+ }
+
+ // Page 131, Test 5b :: ...and after
+ public function test_page131_test5b()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '19970805T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '19970817T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '19970819T090000', 'message' => '3rd occurrence: '),
+ array('index' => 3, 'dateString' => '19970831T090000', 'message' => '4th occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:19970805T090000',
+ 'RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU',
+ ),
+ 4,
+ $checks
+ );
+ }
+
+ // Page 132, Test 1 :: Automatically ignoring an invalid date (30 February)
+ public function test_page132_test1()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20070115T090000', 'message' => '1st occurrence: '),
+ array('index' => 1, 'dateString' => '20070130T090000', 'message' => '2nd occurrence: '),
+ array('index' => 2, 'dateString' => '20070215T090000', 'message' => '3rd occurrence: '),
+ array('index' => 3, 'dateString' => '20070315T090000', 'message' => '4th occurrence: '),
+ array('index' => 4, 'dateString' => '20070330T090000', 'message' => '5th occurrence: '),
+ );
+ $this->assertVEVENT(
+ 'America/New_York',
+ array(
+ 'DTSTART;TZID=America/New_York:20070115T090000',
+ 'RRULE:FREQ=MONTHLY;BYMONTHDAY=15,30;COUNT=5',
+ ),
+ 5,
+ $checks
+ );
+ }
+
+ public function assertVEVENT($defaultTimezone, $veventParts, $count, $checks)
+ {
+ $options = $this->getOptions($defaultTimezone);
+
+ $testIcal = implode(PHP_EOL, $this->getIcalHeader());
+ $testIcal .= PHP_EOL;
+ $testIcal .= implode(PHP_EOL, $this->formatIcalEvent($veventParts));
+ $testIcal .= PHP_EOL;
+ $testIcal .= implode(PHP_EOL, $this->getIcalFooter());
+
+ $ical = new ICal(false, $options);
+ $ical->initString($testIcal);
+
+ $events = $ical->events();
+
+ $this->assertCount($count, $events);
+
+ foreach ($checks as $check) {
+ $this->assertEvent($events[$check['index']], $check['dateString'], $check['message'], isset($check['timezone']) ? $check['timezone'] : $defaultTimezone);
+ }
+ }
+
+ public function assertEvent($event, $expectedDateString, $message, $timeZone = null)
+ {
+ if (!is_null($timeZone)) {
+ date_default_timezone_set($timeZone);
+ }
+
+ $expectedTimeStamp = strtotime($expectedDateString);
+
+ $this->assertSame($expectedTimeStamp, $event->dtstart_array[2], $message . 'timestamp mismatch (expected ' . $expectedDateString . ' vs actual ' . $event->dtstart . ')');
+ $this->assertSame($expectedDateString, $event->dtstart, $message . 'dtstart mismatch (timestamp is okay)');
+ }
+
+ public function getOptions($defaultTimezone)
+ {
+ $options = array(
+ 'defaultSpan' => 2, // Default value: 2
+ 'defaultTimeZone' => $defaultTimezone, // Default value: UTC
+ 'defaultWeekStart' => 'MO', // Default value
+ 'disableCharacterReplacement' => false, // Default value
+ 'filterDaysAfter' => null, // Default value
+ 'filterDaysBefore' => null, // Default value
+ 'httpUserAgent' => null, // Default value
+ 'skipRecurrence' => false, // Default value
+ );
+
+ return $options;
+ }
+
+ public function formatIcalEvent($veventParts)
+ {
+ return array_merge(
+ array(
+ 'BEGIN:VEVENT',
+ 'CREATED:' . gmdate('Ymd\THis\Z'),
+ 'UID:RFC5545-examples-test',
+ ),
+ $veventParts,
+ array(
+ 'SUMMARY:test',
+ 'LAST-MODIFIED:' . gmdate('Ymd\THis\Z', filemtime(__FILE__)),
+ 'END:VEVENT',
+ )
+ );
+ }
+
+ public function getIcalHeader()
+ {
+ return array(
+ 'BEGIN:VCALENDAR',
+ 'VERSION:2.0',
+ 'PRODID:-//Google Inc//Google Calendar 70.9054//EN',
+ 'X-WR-CALNAME:Private',
+ 'X-APPLE-CALENDAR-COLOR:#FF2968',
+ 'X-WR-CALDESC:',
+ );
+ }
+
+ public function getIcalFooter()
+ {
+ return array('END:VCALENDAR');
+ }
+}
diff --git a/lib/composer/vendor/johngrogg/ics-parser/tests/SingleEventsTest.php b/lib/composer/vendor/johngrogg/ics-parser/tests/SingleEventsTest.php
new file mode 100644
index 0000000..fc89c67
--- /dev/null
+++ b/lib/composer/vendor/johngrogg/ics-parser/tests/SingleEventsTest.php
@@ -0,0 +1,509 @@
+originalTimeZone = date_default_timezone_get();
+ }
+
+ /**
+ * @after
+ */
+ public function tearDownFixtures()
+ {
+ date_default_timezone_set($this->originalTimeZone);
+ }
+
+ public function testFullDayTimeZoneBerlin()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20000301', 'message' => '1st event, CET: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ 'DTSTART;VALUE=DATE:20000301',
+ 'DTEND;VALUE=DATE:20000302',
+ 1,
+ $checks
+ );
+ }
+
+ public function testSeveralFullDaysTimeZoneBerlin()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20000301', 'message' => '1st event, CET: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ 'DTSTART;VALUE=DATE:20000301',
+ 'DTEND;VALUE=DATE:20000304',
+ 1,
+ $checks
+ );
+ }
+
+ public function testEventTimeZoneUTC()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20180626T070000Z', 'message' => '1st event, UTC: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ 'DTSTART:20180626T070000Z',
+ 'DTEND:20180626T110000Z',
+ 1,
+ $checks
+ );
+ }
+
+ public function testEventTimeZoneBerlin()
+ {
+ $checks = array(
+ array('index' => 0, 'dateString' => '20180626T070000', 'message' => '1st event, CEST: '),
+ );
+ $this->assertVEVENT(
+ 'Europe/Berlin',
+ 'DTSTART:20180626T070000',
+ 'DTEND:20180626T110000',
+ 1,
+ $checks
+ );
+ }
+
+ public function assertVEVENT($defaultTimezone, $dtstart, $dtend, $count, $checks)
+ {
+ $options = $this->getOptions($defaultTimezone);
+
+ $testIcal = implode(PHP_EOL, $this->getIcalHeader());
+ $testIcal .= PHP_EOL;
+ $testIcal .= implode(PHP_EOL, $this->formatIcalEvent($dtstart, $dtend));
+ $testIcal .= PHP_EOL;
+ $testIcal .= implode(PHP_EOL, $this->getIcalTimezones());
+ $testIcal .= PHP_EOL;
+ $testIcal .= implode(PHP_EOL, $this->getIcalFooter());
+
+ date_default_timezone_set('UTC');
+
+ $ical = new ICal(false, $options);
+ $ical->initString($testIcal);
+
+ $events = $ical->events();
+
+ $this->assertCount($count, $events);
+
+ foreach ($checks as $check) {
+ $this->assertEvent(
+ $events[$check['index']],
+ $check['dateString'],
+ $check['message'],
+ isset($check['timezone']) ? $check['timezone'] : $defaultTimezone
+ );
+ }
+ }
+
+ public function getOptions($defaultTimezone)
+ {
+ $options = array(
+ 'defaultSpan' => 2, // Default value
+ 'defaultTimeZone' => $defaultTimezone, // Default value: UTC
+ 'defaultWeekStart' => 'MO', // Default value
+ 'disableCharacterReplacement' => false, // Default value
+ 'filterDaysAfter' => null, // Default value
+ 'filterDaysBefore' => null, // Default value
+ 'httpUserAgent' => null, // Default value
+ 'skipRecurrence' => false, // Default value
+ );
+
+ return $options;
+ }
+
+ public function getIcalHeader()
+ {
+ return array(
+ 'BEGIN:VCALENDAR',
+ 'VERSION:2.0',
+ 'PRODID:-//Google Inc//Google Calendar 70.9054//EN',
+ 'X-WR-CALNAME:Private',
+ 'X-APPLE-CALENDAR-COLOR:#FF2968',
+ 'X-WR-CALDESC:',
+ );
+ }
+
+ public function formatIcalEvent($dtstart, $dtend)
+ {
+ return array(
+ 'BEGIN:VEVENT',
+ 'CREATED:20090213T195947Z',
+ 'UID:M2CD-1-1-5FB000FB-BBE4-4F3F-9E7E-217F1FF97209',
+ $dtstart,
+ $dtend,
+ 'SUMMARY:test',
+ 'DESCRIPTION;LANGUAGE=en-gb:This is a short description\nwith a new line. Some "special" \'s',
+ ' igns\' may be interesting\, too.',
+ ' And a non-breaking space.',
+ 'LAST-MODIFIED:20110429T222101Z',
+ 'DTSTAMP:20170630T105724Z',
+ 'SEQUENCE:0',
+ 'END:VEVENT',
+ );
+ }
+
+ public function getIcalTimezones()
+ {
+ return array(
+ 'BEGIN:VTIMEZONE',
+ 'TZID:Europe/Berlin',
+ 'X-LIC-LOCATION:Europe/Berlin',
+ 'BEGIN:STANDARD',
+ 'DTSTART:18930401T000000',
+ 'RDATE:18930401T000000',
+ 'TZNAME:CEST',
+ 'TZOFFSETFROM:+005328',
+ 'TZOFFSETTO:+0100',
+ 'END:STANDARD',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19160430T230000',
+ 'RDATE:19160430T230000',
+ 'RDATE:19400401T020000',
+ 'RDATE:19430329T020000',
+ 'RDATE:19460414T020000',
+ 'RDATE:19470406T030000',
+ 'RDATE:19480418T020000',
+ 'RDATE:19490410T020000',
+ 'RDATE:19800406T020000',
+ 'TZNAME:CEST',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0200',
+ 'END:DAYLIGHT',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19161001T010000',
+ 'RDATE:19161001T010000',
+ 'RDATE:19421102T030000',
+ 'RDATE:19431004T030000',
+ 'RDATE:19441002T030000',
+ 'RDATE:19451118T030000',
+ 'RDATE:19461007T030000',
+ 'TZNAME:CET',
+ 'TZOFFSETFROM:+0200',
+ 'TZOFFSETTO:+0100',
+ 'END:STANDARD',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19170416T020000',
+ 'RRULE:FREQ=YEARLY;UNTIL=19180415T010000Z;BYMONTH=4;BYDAY=3MO',
+ 'TZNAME:CEST',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0200',
+ 'END:DAYLIGHT',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19170917T030000',
+ 'RRULE:FREQ=YEARLY;UNTIL=19180916T010000Z;BYMONTH=9;BYDAY=3MO',
+ 'TZNAME:CET',
+ 'TZOFFSETFROM:+0200',
+ 'TZOFFSETTO:+0100',
+ 'END:STANDARD',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19440403T020000',
+ 'RRULE:FREQ=YEARLY;UNTIL=19450402T010000Z;BYMONTH=4;BYDAY=1MO',
+ 'TZNAME:CEST',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0200',
+ 'END:DAYLIGHT',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19450524T020000',
+ 'RDATE:19450524T020000',
+ 'RDATE:19470511T030000',
+ 'TZNAME:CEMT',
+ 'TZOFFSETFROM:+0200',
+ 'TZOFFSETTO:+0300',
+ 'END:DAYLIGHT',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19450924T030000',
+ 'RDATE:19450924T030000',
+ 'RDATE:19470629T030000',
+ 'TZNAME:CEST',
+ 'TZOFFSETFROM:+0300',
+ 'TZOFFSETTO:+0200',
+ 'END:DAYLIGHT',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19460101T000000',
+ 'RDATE:19460101T000000',
+ 'RDATE:19800101T000000',
+ 'TZNAME:CEST',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0100',
+ 'END:STANDARD',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19471005T030000',
+ 'RRULE:FREQ=YEARLY;UNTIL=19491002T010000Z;BYMONTH=10;BYDAY=1SU',
+ 'TZNAME:CET',
+ 'TZOFFSETFROM:+0200',
+ 'TZOFFSETTO:+0100',
+ 'END:STANDARD',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19800928T030000',
+ 'RRULE:FREQ=YEARLY;UNTIL=19950924T010000Z;BYMONTH=9;BYDAY=-1SU',
+ 'TZNAME:CET',
+ 'TZOFFSETFROM:+0200',
+ 'TZOFFSETTO:+0100',
+ 'END:STANDARD',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19810329T020000',
+ 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU',
+ 'TZNAME:CEST',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0200',
+ 'END:DAYLIGHT',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19961027T030000',
+ 'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU',
+ 'TZNAME:CET',
+ 'TZOFFSETFROM:+0200',
+ 'TZOFFSETTO:+0100',
+ 'END:STANDARD',
+ 'END:VTIMEZONE',
+ 'BEGIN:VTIMEZONE',
+ 'TZID:Europe/Paris',
+ 'X-LIC-LOCATION:Europe/Paris',
+ 'BEGIN:STANDARD',
+ 'DTSTART:18910315T000100',
+ 'RDATE:18910315T000100',
+ 'TZNAME:PMT',
+ 'TZOFFSETFROM:+000921',
+ 'TZOFFSETTO:+000921',
+ 'END:STANDARD',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19110311T000100',
+ 'RDATE:19110311T000100',
+ 'TZNAME:WEST',
+ 'TZOFFSETFROM:+000921',
+ 'TZOFFSETTO:+0000',
+ 'END:STANDARD',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19160614T230000',
+ 'RDATE:19160614T230000',
+ 'RDATE:19170324T230000',
+ 'RDATE:19180309T230000',
+ 'RDATE:19190301T230000',
+ 'RDATE:19200214T230000',
+ 'RDATE:19210314T230000',
+ 'RDATE:19220325T230000',
+ 'RDATE:19230526T230000',
+ 'RDATE:19240329T230000',
+ 'RDATE:19250404T230000',
+ 'RDATE:19260417T230000',
+ 'RDATE:19270409T230000',
+ 'RDATE:19280414T230000',
+ 'RDATE:19290420T230000',
+ 'RDATE:19300412T230000',
+ 'RDATE:19310418T230000',
+ 'RDATE:19320402T230000',
+ 'RDATE:19330325T230000',
+ 'RDATE:19340407T230000',
+ 'RDATE:19350330T230000',
+ 'RDATE:19360418T230000',
+ 'RDATE:19370403T230000',
+ 'RDATE:19380326T230000',
+ 'RDATE:19390415T230000',
+ 'RDATE:19400225T020000',
+ 'TZNAME:WEST',
+ 'TZOFFSETFROM:+0000',
+ 'TZOFFSETTO:+0100',
+ 'END:DAYLIGHT',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19161002T000000',
+ 'RRULE:FREQ=YEARLY;UNTIL=19191005T230000Z;BYMONTH=10;BYMONTHDAY=2,3,4,5,6,',
+ ' 7,8;BYDAY=MO',
+ 'TZNAME:WET',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0000',
+ 'END:STANDARD',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19201024T000000',
+ 'RDATE:19201024T000000',
+ 'RDATE:19211026T000000',
+ 'RDATE:19391119T000000',
+ 'TZNAME:WET',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0000',
+ 'END:STANDARD',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19221008T000000',
+ 'RRULE:FREQ=YEARLY;UNTIL=19381001T230000Z;BYMONTH=10;BYMONTHDAY=2,3,4,5,6,',
+ ' 7,8;BYDAY=SU',
+ 'TZNAME:WET',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0000',
+ 'END:STANDARD',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19400614T230000',
+ 'RDATE:19400614T230000',
+ 'TZNAME:CEST',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0200',
+ 'END:STANDARD',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19421102T030000',
+ 'RDATE:19421102T030000',
+ 'RDATE:19431004T030000',
+ 'RDATE:19760926T010000',
+ 'RDATE:19770925T030000',
+ 'RDATE:19781001T030000',
+ 'TZNAME:CET',
+ 'TZOFFSETFROM:+0200',
+ 'TZOFFSETTO:+0100',
+ 'END:STANDARD',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19430329T020000',
+ 'RDATE:19430329T020000',
+ 'RDATE:19440403T020000',
+ 'RDATE:19760328T010000',
+ 'TZNAME:CEST',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0200',
+ 'END:DAYLIGHT',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19440825T000000',
+ 'RDATE:19440825T000000',
+ 'TZNAME:WEST',
+ 'TZOFFSETFROM:+0200',
+ 'TZOFFSETTO:+0200',
+ 'END:STANDARD',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19441008T010000',
+ 'RDATE:19441008T010000',
+ 'TZNAME:WEST',
+ 'TZOFFSETFROM:+0200',
+ 'TZOFFSETTO:+0100',
+ 'END:DAYLIGHT',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19450402T020000',
+ 'RDATE:19450402T020000',
+ 'TZNAME:WEMT',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0200',
+ 'END:DAYLIGHT',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19450916T030000',
+ 'RDATE:19450916T030000',
+ 'TZNAME:CEST',
+ 'TZOFFSETFROM:+0200',
+ 'TZOFFSETTO:+0100',
+ 'END:STANDARD',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19770101T000000',
+ 'RDATE:19770101T000000',
+ 'TZNAME:CEST',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0100',
+ 'END:STANDARD',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19770403T020000',
+ 'RRULE:FREQ=YEARLY;UNTIL=19800406T010000Z;BYMONTH=4;BYDAY=1SU',
+ 'TZNAME:CEST',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0200',
+ 'END:DAYLIGHT',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19790930T030000',
+ 'RRULE:FREQ=YEARLY;UNTIL=19950924T010000Z;BYMONTH=9;BYDAY=-1SU',
+ 'TZNAME:CET',
+ 'TZOFFSETFROM:+0200',
+ 'TZOFFSETTO:+0100',
+ 'END:STANDARD',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19810329T020000',
+ 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU',
+ 'TZNAME:CEST',
+ 'TZOFFSETFROM:+0100',
+ 'TZOFFSETTO:+0200',
+ 'END:DAYLIGHT',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19961027T030000',
+ 'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU',
+ 'TZNAME:CET',
+ 'TZOFFSETFROM:+0200',
+ 'TZOFFSETTO:+0100',
+ 'END:STANDARD',
+ 'END:VTIMEZONE',
+ 'BEGIN:VTIMEZONE',
+ 'TZID:US-Eastern',
+ 'LAST-MODIFIED:19870101T000000Z',
+ 'TZURL:http://zones.stds_r_us.net/tz/US-Eastern',
+ 'BEGIN:STANDARD',
+ 'DTSTART:19671029T020000',
+ 'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10',
+ 'TZOFFSETFROM:-0400',
+ 'TZOFFSETTO:-0500',
+ 'TZNAME:EST',
+ 'END:STANDARD',
+ 'BEGIN:DAYLIGHT',
+ 'DTSTART:19870405T020000',
+ 'RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4',
+ 'TZOFFSETFROM:-0500',
+ 'TZOFFSETTO:-0400',
+ 'TZNAME:EDT',
+ 'END:DAYLIGHT',
+ 'END:VTIMEZONE',
+ );
+ }
+
+ public function getIcalFooter()
+ {
+ return array('END:VCALENDAR');
+ }
+
+ public function assertEvent($event, $expectedDateString, $message, $timezone = null)
+ {
+ if ($timezone !== null) {
+ date_default_timezone_set($timezone);
+ }
+
+ $expectedTimeStamp = strtotime($expectedDateString);
+
+ $this->assertSame(
+ $expectedTimeStamp,
+ $event->dtstart_array[2],
+ $message . 'timestamp mismatch (expected ' . $expectedDateString . ' vs actual ' . $event->dtstart . ')'
+ );
+ $this->assertSame(
+ $expectedDateString,
+ $event->dtstart,
+ $message . 'dtstart mismatch (timestamp is okay)'
+ );
+ }
+
+ public function assertEventFile($defaultTimezone, $file, $count, $checks)
+ {
+ $options = $this->getOptions($defaultTimezone);
+
+ date_default_timezone_set('UTC');
+
+ $ical = new ICal($file, $options);
+
+ $events = $ical->events();
+
+ $this->assertCount($count, $events);
+
+ foreach ($checks as $check) {
+ $this->assertEvent(
+ $events[$check['index']],
+ $check['dateString'],
+ $check['message'],
+ isset($check['timezone']) ? $check['timezone'] : $defaultTimezone
+ );
+ }
+ }
+}
diff --git a/lib/composer/vendor/johngrogg/ics-parser/tests/ical/ical-monthly.ics b/lib/composer/vendor/johngrogg/ics-parser/tests/ical/ical-monthly.ics
new file mode 100644
index 0000000..d80e221
--- /dev/null
+++ b/lib/composer/vendor/johngrogg/ics-parser/tests/ical/ical-monthly.ics
@@ -0,0 +1,18 @@
+BEGIN:VCALENDAR
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+VERSION:2.0
+X-WR-CALNAME:Private
+X-APPLE-CALENDAR-COLOR:#FF2968
+X-WR-CALDESC:
+BEGIN:VEVENT
+CREATED:20090213T195947Z
+UID:M2CD-1-1-5FB000FB-BBE4-4F3F-9E7E-217F1FF97208
+RRULE:FREQ=MONTHLY;BYMONTHDAY=1;WKST=SU;COUNT=25
+DTSTART;VALUE=DATE:20180701
+DTEND;VALUE=DATE:20180702
+SUMMARY:Monthly
+LAST-MODIFIED:20110429T222101Z
+DTSTAMP:20170630T105724Z
+SEQUENCE:0
+END:VEVENT
+END:VCALENDAR
diff --git a/lib/composer/vendor/johngrogg/ics-parser/tests/ical/issue-196.ics b/lib/composer/vendor/johngrogg/ics-parser/tests/ical/issue-196.ics
new file mode 100644
index 0000000..3ffcb18
--- /dev/null
+++ b/lib/composer/vendor/johngrogg/ics-parser/tests/ical/issue-196.ics
@@ -0,0 +1,64 @@
+BEGIN:VCALENDAR
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+VERSION:2.0
+X-WR-CALNAME:Test-Calendar
+X-WR-TIMEZONE:Europe/Berlin
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20180101T152047Z
+LAST-MODIFIED:20181202T202056Z
+DTSTAMP:20181202T202056Z
+UID:529b1ea3-8de8-484d-b878-c20c7fb72bf5
+SUMMARY:test
+RRULE:FREQ=DAILY;UNTIL=20191111T180000Z
+DTSTART;TZID=Europe/Berlin:20191105T190000
+DTEND;TZID=Europe/Berlin:20191105T220000
+TRANSP:OPAQUE
+SEQUENCE:24
+X-MOZ-GENERATION:37
+END:VEVENT
+BEGIN:VEVENT
+CREATED:20181202T202042Z
+LAST-MODIFIED:20181202T202053Z
+DTSTAMP:20181202T202053Z
+UID:529b1ea3-8de8-484d-b878-c20c7fb72bf5
+SUMMARY:test
+RECURRENCE-ID;TZID=Europe/Berlin:20191109T190000
+DTSTART;TZID=Europe/Berlin:20191109T170000
+DTEND;TZID=Europe/Berlin:20191109T220000
+TRANSP:OPAQUE
+SEQUENCE:25
+X-MOZ-GENERATION:37
+DURATION:PT0S
+END:VEVENT
+BEGIN:VEVENT
+CREATED:20181202T202053Z
+LAST-MODIFIED:20181202T202056Z
+DTSTAMP:20181202T202056Z
+UID:529b1ea3-8de8-484d-b878-c20c7fb72bf5
+SUMMARY:test
+RECURRENCE-ID;TZID=Europe/Berlin:20191110T190000
+DTSTART;TZID=Europe/Berlin:20191110T180000
+DTEND;TZID=Europe/Berlin:20191110T220000
+TRANSP:OPAQUE
+SEQUENCE:25
+X-MOZ-GENERATION:37
+DURATION:PT0S
+END:VEVENT
+END:VCALENDAR
diff --git a/lib/composer/vendor/psr/log/LICENSE b/lib/composer/vendor/psr/log/LICENSE
new file mode 100644
index 0000000..474c952
--- /dev/null
+++ b/lib/composer/vendor/psr/log/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2012 PHP Framework Interoperability Group
+
+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/lib/composer/vendor/psr/log/README.md b/lib/composer/vendor/psr/log/README.md
new file mode 100644
index 0000000..a9f20c4
--- /dev/null
+++ b/lib/composer/vendor/psr/log/README.md
@@ -0,0 +1,58 @@
+PSR Log
+=======
+
+This repository holds all interfaces/classes/traits related to
+[PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md).
+
+Note that this is not a logger of its own. It is merely an interface that
+describes a logger. See the specification for more details.
+
+Installation
+------------
+
+```bash
+composer require psr/log
+```
+
+Usage
+-----
+
+If you need a logger, you can use the interface like this:
+
+```php
+logger = $logger;
+ }
+
+ public function doSomething()
+ {
+ if ($this->logger) {
+ $this->logger->info('Doing work');
+ }
+
+ try {
+ $this->doSomethingElse();
+ } catch (Exception $exception) {
+ $this->logger->error('Oh no!', array('exception' => $exception));
+ }
+
+ // do something useful
+ }
+}
+```
+
+You can then pick one of the implementations of the interface to get a logger.
+
+If you want to implement the interface, you can require this package and
+implement `Psr\Log\LoggerInterface` in your code. Please read the
+[specification text](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md)
+for details.
diff --git a/lib/composer/vendor/psr/log/composer.json b/lib/composer/vendor/psr/log/composer.json
new file mode 100644
index 0000000..879fc6f
--- /dev/null
+++ b/lib/composer/vendor/psr/log/composer.json
@@ -0,0 +1,26 @@
+{
+ "name": "psr/log",
+ "description": "Common interface for logging libraries",
+ "keywords": ["psr", "psr-3", "log"],
+ "homepage": "https://github.com/php-fig/log",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "require": {
+ "php": ">=8.0.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Log\\": "src"
+ }
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ }
+}
diff --git a/lib/composer/vendor/psr/log/src/AbstractLogger.php b/lib/composer/vendor/psr/log/src/AbstractLogger.php
new file mode 100644
index 0000000..d60a091
--- /dev/null
+++ b/lib/composer/vendor/psr/log/src/AbstractLogger.php
@@ -0,0 +1,15 @@
+logger = $logger;
+ }
+}
diff --git a/lib/composer/vendor/psr/log/src/LoggerInterface.php b/lib/composer/vendor/psr/log/src/LoggerInterface.php
new file mode 100644
index 0000000..cb4cf64
--- /dev/null
+++ b/lib/composer/vendor/psr/log/src/LoggerInterface.php
@@ -0,0 +1,98 @@
+log(LogLevel::EMERGENCY, $message, $context);
+ }
+
+ /**
+ * Action must be taken immediately.
+ *
+ * Example: Entire website down, database unavailable, etc. This should
+ * trigger the SMS alerts and wake you up.
+ */
+ public function alert(string|\Stringable $message, array $context = []): void
+ {
+ $this->log(LogLevel::ALERT, $message, $context);
+ }
+
+ /**
+ * Critical conditions.
+ *
+ * Example: Application component unavailable, unexpected exception.
+ */
+ public function critical(string|\Stringable $message, array $context = []): void
+ {
+ $this->log(LogLevel::CRITICAL, $message, $context);
+ }
+
+ /**
+ * Runtime errors that do not require immediate action but should typically
+ * be logged and monitored.
+ */
+ public function error(string|\Stringable $message, array $context = []): void
+ {
+ $this->log(LogLevel::ERROR, $message, $context);
+ }
+
+ /**
+ * Exceptional occurrences that are not errors.
+ *
+ * Example: Use of deprecated APIs, poor use of an API, undesirable things
+ * that are not necessarily wrong.
+ */
+ public function warning(string|\Stringable $message, array $context = []): void
+ {
+ $this->log(LogLevel::WARNING, $message, $context);
+ }
+
+ /**
+ * Normal but significant events.
+ */
+ public function notice(string|\Stringable $message, array $context = []): void
+ {
+ $this->log(LogLevel::NOTICE, $message, $context);
+ }
+
+ /**
+ * Interesting events.
+ *
+ * Example: User logs in, SQL logs.
+ */
+ public function info(string|\Stringable $message, array $context = []): void
+ {
+ $this->log(LogLevel::INFO, $message, $context);
+ }
+
+ /**
+ * Detailed debug information.
+ */
+ public function debug(string|\Stringable $message, array $context = []): void
+ {
+ $this->log(LogLevel::DEBUG, $message, $context);
+ }
+
+ /**
+ * Logs with an arbitrary level.
+ *
+ * @param mixed $level
+ *
+ * @throws \Psr\Log\InvalidArgumentException
+ */
+ abstract public function log($level, string|\Stringable $message, array $context = []): void;
+}
diff --git a/lib/composer/vendor/psr/log/src/NullLogger.php b/lib/composer/vendor/psr/log/src/NullLogger.php
new file mode 100644
index 0000000..de0561e
--- /dev/null
+++ b/lib/composer/vendor/psr/log/src/NullLogger.php
@@ -0,0 +1,26 @@
+logger) { }`
+ * blocks.
+ */
+class NullLogger extends AbstractLogger
+{
+ /**
+ * Logs with an arbitrary level.
+ *
+ * @param mixed[] $context
+ *
+ * @throws \Psr\Log\InvalidArgumentException
+ */
+ public function log($level, string|\Stringable $message, array $context = []): void
+ {
+ // noop
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/LICENSE b/lib/composer/vendor/sabre/dav/LICENSE
new file mode 100644
index 0000000..fd3539e
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/LICENSE
@@ -0,0 +1,27 @@
+Copyright (C) 2007-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 of SabreDAV 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/lib/composer/vendor/sabre/dav/README.md b/lib/composer/vendor/sabre/dav/README.md
new file mode 100644
index 0000000..32ca1c3
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/README.md
@@ -0,0 +1,39 @@
+ sabre/dav
+=======================================================
+
+Introduction
+------------
+
+sabre/dav is the most popular WebDAV framework for PHP. Use it to create WebDAV, CalDAV and CardDAV servers.
+
+Full documentation can be found on the website:
+
+http://sabre.io/
+
+
+Build status
+------------
+
+| branch | status | PHP version |
+|------------|---------------------------------------------------------------------------|--------------------|
+| master 4.* |  | PHP 7.1 up, 8.0 up |
+| 3.2 | unmaintained | PHP 5.5 to 7.1 |
+| 3.1 | unmaintained | PHP 5.5 |
+| 3.0 | unmaintained | PHP 5.4 |
+| 2.1 | unmaintained | PHP 5.4 |
+| 2.0 | unmaintained | PHP 5.4 |
+| 1.8 | unmaintained | PHP 5.3 |
+| 1.7 | unmaintained | PHP 5.3 |
+| 1.6 | unmaintained | PHP 5.3 |
+
+Documentation
+-------------
+
+* [Introduction](http://sabre.io/dav/).
+* [Installation](http://sabre.io/dav/install/).
+
+
+Made at fruux
+-------------
+
+SabreDAV is being developed by [fruux](https://fruux.com/). Drop us a line for commercial services or enterprise support.
diff --git a/lib/composer/vendor/sabre/dav/bin/build.php b/lib/composer/vendor/sabre/dav/bin/build.php
new file mode 100755
index 0000000..4dd25d9
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/bin/build.php
@@ -0,0 +1,169 @@
+#!/usr/bin/env php
+ [
+ 'init', 'test', 'clean',
+ ],
+ 'markrelease' => [
+ 'init', 'test', 'clean',
+ ],
+ 'clean' => [],
+ 'test' => [
+ 'composerupdate',
+ ],
+ 'init' => [],
+ 'composerupdate' => [],
+ ];
+
+$default = 'buildzip';
+
+$baseDir = __DIR__.'/../';
+chdir($baseDir);
+
+$currentTask = $default;
+if ($argc > 1) {
+ $currentTask = $argv[1];
+}
+$version = null;
+if ($argc > 2) {
+ $version = $argv[2];
+}
+
+if (!isset($tasks[$currentTask])) {
+ echo 'Task not found: ', $currentTask, "\n";
+ exit(1);
+}
+
+// Creating the dependency graph
+$newTaskList = [];
+$oldTaskList = [$currentTask => true];
+
+while (count($oldTaskList) > 0) {
+ foreach ($oldTaskList as $task => $foo) {
+ if (!isset($tasks[$task])) {
+ echo 'Dependency not found: '.$task, "\n";
+ exit(1);
+ }
+ $dependencies = $tasks[$task];
+
+ $fullFilled = true;
+ foreach ($dependencies as $dependency) {
+ if (isset($newTaskList[$dependency])) {
+ // Already in the fulfilled task list.
+ continue;
+ } else {
+ $oldTaskList[$dependency] = true;
+ $fullFilled = false;
+ }
+ }
+ if ($fullFilled) {
+ unset($oldTaskList[$task]);
+ $newTaskList[$task] = 1;
+ }
+ }
+}
+
+foreach (array_keys($newTaskList) as $task) {
+ echo 'task: '.$task, "\n";
+ call_user_func($task);
+ echo "\n";
+}
+
+function init()
+{
+ global $version;
+ if (!$version) {
+ include __DIR__.'/../vendor/autoload.php';
+ $version = Sabre\DAV\Version::VERSION;
+ }
+
+ echo ' Building sabre/dav '.$version, "\n";
+}
+
+function clean()
+{
+ global $baseDir;
+ echo " Removing build files\n";
+ $outputDir = $baseDir.'/build/SabreDAV';
+ if (is_dir($outputDir)) {
+ system('rm -r '.$baseDir.'/build/SabreDAV');
+ }
+}
+
+function composerupdate()
+{
+ global $baseDir;
+ echo " Updating composer packages to latest version\n\n";
+ system('cd '.$baseDir.'; composer update');
+}
+
+function test()
+{
+ global $baseDir;
+
+ echo " Running all unittests.\n";
+ echo " This may take a while.\n\n";
+ system(__DIR__.'/phpunit --configuration '.$baseDir.'/tests/phpunit.xml.dist --stop-on-failure', $code);
+ if (0 != $code) {
+ echo "PHPUnit reported error code $code\n";
+ exit(1);
+ }
+}
+
+function buildzip()
+{
+ global $baseDir, $version;
+ echo " Generating composer.json\n";
+
+ $input = json_decode(file_get_contents(__DIR__.'/../composer.json'), true);
+ $newComposer = [
+ 'require' => $input['require'],
+ 'config' => [
+ 'bin-dir' => './bin',
+ ],
+ 'prefer-stable' => true,
+ 'minimum-stability' => 'alpha',
+ ];
+ unset(
+ $newComposer['require']['sabre/vobject'],
+ $newComposer['require']['sabre/http'],
+ $newComposer['require']['sabre/uri'],
+ $newComposer['require']['sabre/event']
+ );
+ $newComposer['require']['sabre/dav'] = $version;
+ mkdir('build/SabreDAV');
+ file_put_contents('build/SabreDAV/composer.json', json_encode($newComposer, JSON_PRETTY_PRINT));
+
+ echo " Downloading dependencies\n";
+ system('cd build/SabreDAV; composer install -n', $code);
+ if (0 !== $code) {
+ echo "Composer reported error code $code\n";
+ exit(1);
+ }
+
+ echo " Removing pointless files\n";
+ unlink('build/SabreDAV/composer.json');
+ unlink('build/SabreDAV/composer.lock');
+
+ echo " Moving important files to the root of the project\n";
+
+ $fileNames = [
+ 'CHANGELOG.md',
+ 'LICENSE',
+ 'README.md',
+ 'examples',
+ ];
+ foreach ($fileNames as $fileName) {
+ echo " $fileName\n";
+ rename('build/SabreDAV/vendor/sabre/dav/'.$fileName, 'build/SabreDAV/'.$fileName);
+ }
+
+ //
+
+ echo "\n";
+ echo "Zipping the sabredav distribution\n\n";
+ system('cd build; zip -qr sabredav-'.$version.'.zip SabreDAV');
+
+ echo 'Done.';
+}
diff --git a/lib/composer/vendor/sabre/dav/bin/migrateto20.php b/lib/composer/vendor/sabre/dav/bin/migrateto20.php
new file mode 100755
index 0000000..fb24fe5
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/bin/migrateto20.php
@@ -0,0 +1,414 @@
+#!/usr/bin/env php
+setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
+
+$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
+
+switch ($driver) {
+ case 'mysql':
+ echo "Detected MySQL.\n";
+ break;
+ case 'sqlite':
+ echo "Detected SQLite.\n";
+ break;
+ default:
+ echo 'Error: unsupported driver: '.$driver."\n";
+ exit(-1);
+}
+
+foreach (['calendar', 'addressbook'] as $itemType) {
+ $tableName = $itemType.'s';
+ $tableNameOld = $tableName.'_old';
+ $changesTable = $itemType.'changes';
+
+ echo "Upgrading '$tableName'\n";
+
+ // The only cross-db way to do this, is to just fetch a single record.
+ $row = $pdo->query("SELECT * FROM $tableName LIMIT 1")->fetch();
+
+ if (!$row) {
+ echo "No records were found in the '$tableName' table.\n";
+ echo "\n";
+ echo "We're going to rename the old table to $tableNameOld (just in case).\n";
+ echo "and re-create the new table.\n";
+
+ switch ($driver) {
+ case 'mysql':
+ $pdo->exec("RENAME TABLE $tableName TO $tableNameOld");
+ switch ($itemType) {
+ case 'calendar':
+ $pdo->exec("
+ CREATE TABLE calendars (
+ id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ principaluri VARCHAR(100),
+ displayname VARCHAR(100),
+ uri VARCHAR(200),
+ synctoken INT(11) UNSIGNED NOT NULL DEFAULT '1',
+ description TEXT,
+ calendarorder INT(11) UNSIGNED NOT NULL DEFAULT '0',
+ calendarcolor VARCHAR(10),
+ timezone TEXT,
+ components VARCHAR(20),
+ transparent TINYINT(1) NOT NULL DEFAULT '0',
+ UNIQUE(principaluri, uri)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
+ ");
+ break;
+ case 'addressbook':
+ $pdo->exec("
+ CREATE TABLE addressbooks (
+ id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ principaluri VARCHAR(255),
+ displayname VARCHAR(255),
+ uri VARCHAR(200),
+ description TEXT,
+ synctoken INT(11) UNSIGNED NOT NULL DEFAULT '1',
+ UNIQUE(principaluri, uri)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
+ ");
+ break;
+ }
+ break;
+
+ case 'sqlite':
+ $pdo->exec("ALTER TABLE $tableName RENAME TO $tableNameOld");
+
+ switch ($itemType) {
+ case 'calendar':
+ $pdo->exec('
+ CREATE TABLE calendars (
+ id integer primary key asc,
+ principaluri text,
+ displayname text,
+ uri text,
+ synctoken integer,
+ description text,
+ calendarorder integer,
+ calendarcolor text,
+ timezone text,
+ components text,
+ transparent bool
+ );
+ ');
+ break;
+ case 'addressbook':
+ $pdo->exec('
+ CREATE TABLE addressbooks (
+ id integer primary key asc,
+ principaluri text,
+ displayname text,
+ uri text,
+ description text,
+ synctoken integer
+ );
+ ');
+
+ break;
+ }
+ break;
+ }
+ echo "Creation of 2.0 $tableName table is complete\n";
+ } else {
+ // Checking if there's a synctoken field already.
+ if (array_key_exists('synctoken', $row)) {
+ echo "The 'synctoken' field already exists in the $tableName table.\n";
+ echo "It's likely you already upgraded, so we're simply leaving\n";
+ echo "the $tableName table alone\n";
+ } else {
+ echo "1.8 table schema detected\n";
+ switch ($driver) {
+ case 'mysql':
+ $pdo->exec("ALTER TABLE $tableName ADD synctoken INT(11) UNSIGNED NOT NULL DEFAULT '1'");
+ $pdo->exec("ALTER TABLE $tableName DROP ctag");
+ $pdo->exec("UPDATE $tableName SET synctoken = '1'");
+ break;
+ case 'sqlite':
+ $pdo->exec("ALTER TABLE $tableName ADD synctoken integer");
+ $pdo->exec("UPDATE $tableName SET synctoken = '1'");
+ echo "Note: there's no easy way to remove fields in sqlite.\n";
+ echo "The ctag field is no longer used, but it's kept in place\n";
+ break;
+ }
+
+ echo "Upgraded '$tableName' to 2.0 schema.\n";
+ }
+ }
+
+ try {
+ $pdo->query("SELECT * FROM $changesTable LIMIT 1");
+
+ echo "'$changesTable' already exists. Assuming that this part of the\n";
+ echo "upgrade was already completed.\n";
+ } catch (Exception $e) {
+ echo "Creating '$changesTable' table.\n";
+
+ switch ($driver) {
+ case 'mysql':
+ $pdo->exec("
+ CREATE TABLE $changesTable (
+ id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ uri VARCHAR(200) NOT NULL,
+ synctoken INT(11) UNSIGNED NOT NULL,
+ {$itemType}id INT(11) UNSIGNED NOT NULL,
+ operation TINYINT(1) NOT NULL,
+ INDEX {$itemType}id_synctoken ({$itemType}id, synctoken)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
+
+ ");
+ break;
+ case 'sqlite':
+ $pdo->exec("
+
+ CREATE TABLE $changesTable (
+ id integer primary key asc,
+ uri text,
+ synctoken integer,
+ {$itemType}id integer,
+ operation bool
+ );
+
+ ");
+ $pdo->exec("CREATE INDEX {$itemType}id_synctoken ON $changesTable ({$itemType}id, synctoken);");
+ break;
+ }
+ }
+}
+
+try {
+ $pdo->query('SELECT * FROM calendarsubscriptions LIMIT 1');
+
+ echo "'calendarsubscriptions' already exists. Assuming that this part of the\n";
+ echo "upgrade was already completed.\n";
+} catch (Exception $e) {
+ echo "Creating calendarsubscriptions table.\n";
+
+ switch ($driver) {
+ case 'mysql':
+ $pdo->exec("
+CREATE TABLE calendarsubscriptions (
+ id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ uri VARCHAR(200) NOT NULL,
+ principaluri VARCHAR(100) NOT NULL,
+ source TEXT,
+ displayname VARCHAR(100),
+ refreshrate VARCHAR(10),
+ calendarorder INT(11) UNSIGNED NOT NULL DEFAULT '0',
+ calendarcolor VARCHAR(10),
+ striptodos TINYINT(1) NULL,
+ stripalarms TINYINT(1) NULL,
+ stripattachments TINYINT(1) NULL,
+ lastmodified INT(11) UNSIGNED,
+ UNIQUE(principaluri, uri)
+);
+ ");
+ break;
+ case 'sqlite':
+ $pdo->exec('
+
+CREATE TABLE calendarsubscriptions (
+ id integer primary key asc,
+ uri text,
+ principaluri text,
+ source text,
+ displayname text,
+ refreshrate text,
+ calendarorder integer,
+ calendarcolor text,
+ striptodos bool,
+ stripalarms bool,
+ stripattachments bool,
+ lastmodified int
+);
+ ');
+
+ $pdo->exec('CREATE INDEX principaluri_uri ON calendarsubscriptions (principaluri, uri);');
+ break;
+ }
+}
+
+try {
+ $pdo->query('SELECT * FROM propertystorage LIMIT 1');
+
+ echo "'propertystorage' already exists. Assuming that this part of the\n";
+ echo "upgrade was already completed.\n";
+} catch (Exception $e) {
+ echo "Creating propertystorage table.\n";
+
+ switch ($driver) {
+ case 'mysql':
+ $pdo->exec('
+CREATE TABLE propertystorage (
+ id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ path VARBINARY(1024) NOT NULL,
+ name VARBINARY(100) NOT NULL,
+ value MEDIUMBLOB
+);
+ ');
+ $pdo->exec('
+CREATE UNIQUE INDEX path_property ON propertystorage (path(600), name(100));
+ ');
+ break;
+ case 'sqlite':
+ $pdo->exec('
+CREATE TABLE propertystorage (
+ id integer primary key asc,
+ path TEXT,
+ name TEXT,
+ value TEXT
+);
+ ');
+ $pdo->exec('
+CREATE UNIQUE INDEX path_property ON propertystorage (path, name);
+ ');
+
+ break;
+ }
+}
+
+echo "Upgrading cards table to 2.0 schema\n";
+
+try {
+ $create = false;
+ $row = $pdo->query('SELECT * FROM cards LIMIT 1')->fetch();
+ if (!$row) {
+ $random = mt_rand(1000, 9999);
+ echo "There was no data in the cards table, so we're re-creating it\n";
+ echo "The old table will be renamed to cards_old$random, just in case.\n";
+
+ $create = true;
+
+ switch ($driver) {
+ case 'mysql':
+ $pdo->exec("RENAME TABLE cards TO cards_old$random");
+ break;
+ case 'sqlite':
+ $pdo->exec("ALTER TABLE cards RENAME TO cards_old$random");
+ break;
+ }
+ }
+} catch (Exception $e) {
+ echo "Exception while checking cards table. Assuming that the table does not yet exist.\n";
+ echo 'Debug: ', $e->getMessage(), "\n";
+ $create = true;
+}
+
+if ($create) {
+ switch ($driver) {
+ case 'mysql':
+ $pdo->exec('
+CREATE TABLE cards (
+ id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ addressbookid INT(11) UNSIGNED NOT NULL,
+ carddata MEDIUMBLOB,
+ uri VARCHAR(200),
+ lastmodified INT(11) UNSIGNED,
+ etag VARBINARY(32),
+ size INT(11) UNSIGNED NOT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
+
+ ');
+ break;
+
+ case 'sqlite':
+ $pdo->exec('
+CREATE TABLE cards (
+ id integer primary key asc,
+ addressbookid integer,
+ carddata blob,
+ uri text,
+ lastmodified integer,
+ etag text,
+ size integer
+);
+ ');
+ break;
+ }
+} else {
+ switch ($driver) {
+ case 'mysql':
+ $pdo->exec('
+ ALTER TABLE cards
+ ADD etag VARBINARY(32),
+ ADD size INT(11) UNSIGNED NOT NULL;
+ ');
+ break;
+
+ case 'sqlite':
+ $pdo->exec('
+ ALTER TABLE cards ADD etag text;
+ ALTER TABLE cards ADD size integer;
+ ');
+ break;
+ }
+ echo "Reading all old vcards and populating etag and size fields.\n";
+ $result = $pdo->query('SELECT id, carddata FROM cards');
+ $stmt = $pdo->prepare('UPDATE cards SET etag = ?, size = ? WHERE id = ?');
+ while ($row = $result->fetch(\PDO::FETCH_ASSOC)) {
+ $stmt->execute([
+ md5($row['carddata']),
+ strlen($row['carddata']),
+ $row['id'],
+ ]);
+ }
+}
+
+echo "Upgrade to 2.0 schema completed.\n";
diff --git a/lib/composer/vendor/sabre/dav/bin/migrateto21.php b/lib/composer/vendor/sabre/dav/bin/migrateto21.php
new file mode 100755
index 0000000..2c15b0a
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/bin/migrateto21.php
@@ -0,0 +1,166 @@
+#!/usr/bin/env php
+setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
+
+$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
+
+switch ($driver) {
+ case 'mysql':
+ echo "Detected MySQL.\n";
+ break;
+ case 'sqlite':
+ echo "Detected SQLite.\n";
+ break;
+ default:
+ echo 'Error: unsupported driver: '.$driver."\n";
+ exit(-1);
+}
+
+echo "Upgrading 'calendarobjects'\n";
+$addUid = false;
+try {
+ $result = $pdo->query('SELECT * FROM calendarobjects LIMIT 1');
+ $row = $result->fetch(\PDO::FETCH_ASSOC);
+
+ if (!$row) {
+ echo "No data in table. Going to try to add the uid field anyway.\n";
+ $addUid = true;
+ } elseif (array_key_exists('uid', $row)) {
+ echo "uid field exists. Assuming that this part of the migration has\n";
+ echo "Already been completed.\n";
+ } else {
+ echo "2.0 schema detected.\n";
+ $addUid = true;
+ }
+} catch (Exception $e) {
+ echo "Could not find a calendarobjects table. Skipping this part of the\n";
+ echo "upgrade.\n";
+}
+
+if ($addUid) {
+ switch ($driver) {
+ case 'mysql':
+ $pdo->exec('ALTER TABLE calendarobjects ADD uid VARCHAR(200)');
+ break;
+ case 'sqlite':
+ $pdo->exec('ALTER TABLE calendarobjects ADD uid TEXT');
+ break;
+ }
+
+ $result = $pdo->query('SELECT id, calendardata FROM calendarobjects');
+ $stmt = $pdo->prepare('UPDATE calendarobjects SET uid = ? WHERE id = ?');
+ $counter = 0;
+
+ while ($row = $result->fetch(\PDO::FETCH_ASSOC)) {
+ try {
+ $vobj = \Sabre\VObject\Reader::read($row['calendardata']);
+ } catch (\Exception $e) {
+ echo "Warning! Item with id $row[id] could not be parsed!\n";
+ continue;
+ }
+ $uid = null;
+ $item = $vobj->getBaseComponent();
+ if (!isset($item->UID)) {
+ echo "Warning! Item with id $item[id] does NOT have a UID property and this is required.\n";
+ continue;
+ }
+ $uid = (string) $item->UID;
+ $stmt->execute([$uid, $row['id']]);
+ ++$counter;
+ }
+}
+
+echo "Creating 'schedulingobjects'\n";
+
+switch ($driver) {
+ case 'mysql':
+ $pdo->exec('CREATE TABLE IF NOT EXISTS schedulingobjects
+(
+ id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ principaluri VARCHAR(255),
+ calendardata MEDIUMBLOB,
+ uri VARCHAR(200),
+ lastmodified INT(11) UNSIGNED,
+ etag VARCHAR(32),
+ size INT(11) UNSIGNED NOT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
+ ');
+ break;
+
+ case 'sqlite':
+ $pdo->exec('CREATE TABLE IF NOT EXISTS schedulingobjects (
+ id integer primary key asc,
+ principaluri text,
+ calendardata blob,
+ uri text,
+ lastmodified integer,
+ etag text,
+ size integer
+)
+');
+ break;
+}
+
+echo "Done.\n";
+
+echo "Upgrade to 2.1 schema completed.\n";
diff --git a/lib/composer/vendor/sabre/dav/bin/migrateto30.php b/lib/composer/vendor/sabre/dav/bin/migrateto30.php
new file mode 100755
index 0000000..9798cad
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/bin/migrateto30.php
@@ -0,0 +1,161 @@
+#!/usr/bin/env php
+setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
+
+$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
+
+switch ($driver) {
+ case 'mysql':
+ echo "Detected MySQL.\n";
+ break;
+ case 'sqlite':
+ echo "Detected SQLite.\n";
+ break;
+ default:
+ echo 'Error: unsupported driver: '.$driver."\n";
+ exit(-1);
+}
+
+echo "Upgrading 'propertystorage'\n";
+$addValueType = false;
+try {
+ $result = $pdo->query('SELECT * FROM propertystorage LIMIT 1');
+ $row = $result->fetch(\PDO::FETCH_ASSOC);
+
+ if (!$row) {
+ echo "No data in table. Going to re-create the table.\n";
+ $random = mt_rand(1000, 9999);
+ echo "Renaming propertystorage -> propertystorage_old$random and creating new table.\n";
+
+ switch ($driver) {
+ case 'mysql':
+ $pdo->exec('RENAME TABLE propertystorage TO propertystorage_old'.$random);
+ $pdo->exec('
+ CREATE TABLE propertystorage (
+ id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ path VARBINARY(1024) NOT NULL,
+ name VARBINARY(100) NOT NULL,
+ valuetype INT UNSIGNED,
+ value MEDIUMBLOB
+ );
+ ');
+ $pdo->exec('CREATE UNIQUE INDEX path_property_'.$random.' ON propertystorage (path(600), name(100));');
+ break;
+ case 'sqlite':
+ $pdo->exec('ALTER TABLE propertystorage RENAME TO propertystorage_old'.$random);
+ $pdo->exec('
+CREATE TABLE propertystorage (
+ id integer primary key asc,
+ path text,
+ name text,
+ valuetype integer,
+ value blob
+);');
+
+ $pdo->exec('CREATE UNIQUE INDEX path_property_'.$random.' ON propertystorage (path, name);');
+ break;
+ }
+ } elseif (array_key_exists('valuetype', $row)) {
+ echo "valuetype field exists. Assuming that this part of the migration has\n";
+ echo "Already been completed.\n";
+ } else {
+ echo "2.1 schema detected. Going to perform upgrade.\n";
+ $addValueType = true;
+ }
+} catch (Exception $e) {
+ echo "Could not find a propertystorage table. Skipping this part of the\n";
+ echo "upgrade.\n";
+ echo $e->getMessage(), "\n";
+}
+
+if ($addValueType) {
+ switch ($driver) {
+ case 'mysql':
+ $pdo->exec('ALTER TABLE propertystorage ADD valuetype INT UNSIGNED');
+ break;
+ case 'sqlite':
+ $pdo->exec('ALTER TABLE propertystorage ADD valuetype INT');
+
+ break;
+ }
+
+ $pdo->exec('UPDATE propertystorage SET valuetype = 1 WHERE valuetype IS NULL ');
+}
+
+echo "Migrating vcardurl\n";
+
+$result = $pdo->query('SELECT id, uri, vcardurl FROM principals WHERE vcardurl IS NOT NULL');
+$stmt1 = $pdo->prepare('INSERT INTO propertystorage (path, name, valuetype, value) VALUES (?, ?, 3, ?)');
+
+while ($row = $result->fetch(\PDO::FETCH_ASSOC)) {
+ // Inserting the new record
+ $stmt1->execute([
+ 'addressbooks/'.basename($row['uri']),
+ '{http://calendarserver.org/ns/}me-card',
+ serialize(new Sabre\DAV\Xml\Property\Href($row['vcardurl'])),
+ ]);
+
+ echo serialize(new Sabre\DAV\Xml\Property\Href($row['vcardurl']));
+}
+
+echo "Done.\n";
+echo "Upgrade to 3.0 schema completed.\n";
diff --git a/lib/composer/vendor/sabre/dav/bin/migrateto32.php b/lib/composer/vendor/sabre/dav/bin/migrateto32.php
new file mode 100755
index 0000000..09ac55d
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/bin/migrateto32.php
@@ -0,0 +1,258 @@
+#!/usr/bin/env php
+setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
+
+$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
+
+switch ($driver) {
+ case 'mysql':
+ echo "Detected MySQL.\n";
+ break;
+ case 'sqlite':
+ echo "Detected SQLite.\n";
+ break;
+ default:
+ echo 'Error: unsupported driver: '.$driver."\n";
+ exit(-1);
+}
+
+echo "Creating 'calendarinstances'\n";
+$addValueType = false;
+try {
+ $result = $pdo->query('SELECT * FROM calendarinstances LIMIT 1');
+ $result->fetch(\PDO::FETCH_ASSOC);
+ echo "calendarinstances exists. Assuming this part of the migration has already been done.\n";
+} catch (Exception $e) {
+ echo "calendarinstances does not yet exist. Creating table and migrating data.\n";
+
+ switch ($driver) {
+ case 'mysql':
+ $pdo->exec(<<exec('
+INSERT INTO calendarinstances
+ (
+ calendarid,
+ principaluri,
+ access,
+ displayname,
+ uri,
+ description,
+ calendarorder,
+ calendarcolor,
+ transparent
+ )
+SELECT
+ id,
+ principaluri,
+ 1,
+ displayname,
+ uri,
+ description,
+ calendarorder,
+ calendarcolor,
+ transparent
+FROM calendars
+');
+ break;
+ case 'sqlite':
+ $pdo->exec(<<exec('
+INSERT INTO calendarinstances
+ (
+ calendarid,
+ principaluri,
+ access,
+ displayname,
+ uri,
+ description,
+ calendarorder,
+ calendarcolor,
+ transparent
+ )
+SELECT
+ id,
+ principaluri,
+ 1,
+ displayname,
+ uri,
+ description,
+ calendarorder,
+ calendarcolor,
+ transparent
+FROM calendars
+');
+ break;
+ }
+}
+try {
+ $result = $pdo->query('SELECT * FROM calendars LIMIT 1');
+ $row = $result->fetch(\PDO::FETCH_ASSOC);
+
+ if (!$row) {
+ echo "Source table is empty.\n";
+ $migrateCalendars = true;
+ }
+
+ $columnCount = count($row);
+ if (3 === $columnCount) {
+ echo "The calendars table has 3 columns already. Assuming this part of the migration was already done.\n";
+ $migrateCalendars = false;
+ } else {
+ echo 'The calendars table has '.$columnCount." columns.\n";
+ $migrateCalendars = true;
+ }
+} catch (Exception $e) {
+ echo "calendars table does not exist. This is a major problem. Exiting.\n";
+ exit(-1);
+}
+
+if ($migrateCalendars) {
+ $calendarBackup = 'calendars_3_1_'.$backupPostfix;
+ echo "Backing up 'calendars' to '", $calendarBackup, "'\n";
+
+ switch ($driver) {
+ case 'mysql':
+ $pdo->exec('RENAME TABLE calendars TO '.$calendarBackup);
+ break;
+ case 'sqlite':
+ $pdo->exec('ALTER TABLE calendars RENAME TO '.$calendarBackup);
+ break;
+ }
+
+ echo "Creating new calendars table.\n";
+ switch ($driver) {
+ case 'mysql':
+ $pdo->exec(<<exec(<<exec(<<0):
+ print "Bytes to go before we hit threshold:", bytes
+ else:
+ print "Threshold exceeded with:", -bytes, "bytes"
+ dir = os.listdir(cacheDir)
+ dir2 = []
+ for file in dir:
+ path = cacheDir + '/' + file
+ dir2.append({
+ "path" : path,
+ "atime": os.stat(path).st_atime,
+ "size" : os.stat(path).st_size
+ })
+
+ dir2.sort(lambda x,y: int(x["atime"]-y["atime"]))
+
+ filesunlinked = 0
+ gainedspace = 0
+
+ # Left is the amount of bytes that need to be freed up
+ # The default is the 'min_erase setting'
+ left = min_erase
+
+ # If the min_erase setting is lower than the amount of bytes over
+ # the threshold, we use that number instead.
+ if left < -bytes :
+ left = -bytes
+
+ print "Need to delete at least:", left;
+
+ for file in dir2:
+
+ # Only deleting files if we're not simulating
+ if not simulate: os.unlink(file["path"])
+ left = int(left - file["size"])
+ gainedspace = gainedspace + file["size"]
+ filesunlinked = filesunlinked + 1
+
+ if(left<0):
+ break
+
+ print "%d files deleted (%d bytes)" % (filesunlinked, gainedspace)
+
+
+ time.sleep(sleep)
+
+
+
+def main():
+ parser = OptionParser(
+ version="naturalselection v0.3",
+ description="Cache directory manager. Deletes cache entries based on accesstime and free space thresholds.\n" +
+ "This utility is distributed alongside SabreDAV.",
+ usage="usage: %prog [options] cacheDirectory",
+ )
+ parser.add_option(
+ '-s',
+ dest="simulate",
+ action="store_true",
+ help="Don't actually make changes, but just simulate the behaviour",
+ )
+ parser.add_option(
+ '-r','--runs',
+ help="How many times to check before exiting. -1 is infinite, which is the default",
+ type="int",
+ dest="runs",
+ default=-1
+ )
+ parser.add_option(
+ '-n','--interval',
+ help="Sleep time in seconds (default = 5)",
+ type="int",
+ dest="sleep",
+ default=5
+ )
+ parser.add_option(
+ '-l','--threshold',
+ help="Threshold in bytes (default = 10737418240, which is 10GB)",
+ type="int",
+ dest="threshold",
+ default=10737418240
+ )
+ parser.add_option(
+ '-m', '--min-erase',
+ help="Minimum number of bytes to erase when the threshold is reached. " +
+ "Setting this option higher will reduce the number of times the cache directory will need to be scanned. " +
+ "(the default is 1073741824, which is 1GB.)",
+ type="int",
+ dest="min_erase",
+ default=1073741824
+ )
+
+ options,args = parser.parse_args()
+ if len(args)<1:
+ parser.error("This utility requires at least 1 argument")
+ cacheDir = args[0]
+
+ print "Natural Selection"
+ print "Cache directory:", cacheDir
+ free = getfreespace(cacheDir);
+ print "Current free disk space:", free
+
+ runs = options.runs;
+ while runs!=0 :
+ run(
+ cacheDir,
+ sleep=options.sleep,
+ simulate=options.simulate,
+ threshold=options.threshold,
+ min_erase=options.min_erase
+ )
+ if runs>0:
+ runs = runs - 1
+
+if __name__ == '__main__' :
+ main()
diff --git a/lib/composer/vendor/sabre/dav/bin/sabredav b/lib/composer/vendor/sabre/dav/bin/sabredav
new file mode 100755
index 0000000..032371b
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/bin/sabredav
@@ -0,0 +1,2 @@
+#!/bin/sh
+php -S 0.0.0.0:8080 `dirname $0`/sabredav.php
diff --git a/lib/composer/vendor/sabre/dav/bin/sabredav.php b/lib/composer/vendor/sabre/dav/bin/sabredav.php
new file mode 100755
index 0000000..71047b8
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/bin/sabredav.php
@@ -0,0 +1,51 @@
+stream = fopen('php://stdout', 'w');
+ }
+
+ public function log($msg)
+ {
+ fwrite($this->stream, $msg."\n");
+ }
+}
+
+$log = new CliLog();
+
+if ('cli-server' !== php_sapi_name()) {
+ exit('This script is intended to run on the built-in php webserver');
+}
+
+// Finding composer
+
+$paths = [
+ __DIR__.'/../vendor/autoload.php',
+ __DIR__.'/../../../autoload.php',
+];
+
+foreach ($paths as $path) {
+ if (file_exists($path)) {
+ include $path;
+ break;
+ }
+}
+
+use Sabre\DAV;
+
+// Root
+$root = new DAV\FS\Directory(getcwd());
+
+// Setting up server.
+$server = new DAV\Server($root);
+
+// Browser plugin
+$server->addPlugin(new DAV\Browser\Plugin());
+
+$server->exec();
diff --git a/lib/composer/vendor/sabre/dav/composer.json b/lib/composer/vendor/sabre/dav/composer.json
new file mode 100644
index 0000000..3154173
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/composer.json
@@ -0,0 +1,81 @@
+{
+ "name": "sabre/dav",
+ "type": "library",
+ "description": "WebDAV Framework for PHP",
+ "keywords": ["Framework", "WebDAV", "CalDAV", "CardDAV", "iCalendar"],
+ "homepage": "http://sabre.io/",
+ "license" : "BSD-3-Clause",
+ "authors": [
+ {
+ "name": "Evert Pot",
+ "email": "me@evertpot.com",
+ "homepage" : "http://evertpot.com/",
+ "role" : "Developer"
+ }
+ ],
+ "require": {
+ "php": "^7.1.0 || ^8.0",
+ "sabre/vobject": "^4.2.1",
+ "sabre/event" : "^5.0",
+ "sabre/xml" : "^2.0.1",
+ "sabre/http" : "^5.0.5",
+ "sabre/uri" : "^2.0",
+ "ext-dom": "*",
+ "ext-pcre": "*",
+ "ext-spl": "*",
+ "ext-simplexml": "*",
+ "ext-mbstring" : "*",
+ "ext-ctype" : "*",
+ "ext-date" : "*",
+ "ext-iconv" : "*",
+ "lib-libxml" : ">=2.7.0",
+ "psr/log": "^1.0 || ^2.0 || ^3.0",
+ "ext-json": "*"
+ },
+ "require-dev" : {
+ "friendsofphp/php-cs-fixer": "^2.19",
+ "monolog/monolog": "^1.27 || ^2.0",
+ "phpstan/phpstan": "^0.12 || ^1.0",
+ "phpstan/phpstan-phpunit": "^1.0",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
+ },
+ "suggest" : {
+ "ext-curl" : "*",
+ "ext-pdo" : "*",
+ "ext-imap": "*"
+ },
+ "autoload": {
+ "psr-4" : {
+ "Sabre\\" : "lib/"
+ }
+ },
+ "autoload-dev" : {
+ "psr-4" : {
+ "Sabre\\" : "tests/Sabre/"
+ }
+ },
+ "support" : {
+ "forum" : "https://groups.google.com/group/sabredav-discuss",
+ "source" : "https://github.com/fruux/sabre-dav"
+ },
+ "bin" : [
+ "bin/sabredav",
+ "bin/naturalselection"
+ ],
+ "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/lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php
new file mode 100644
index 0000000..c761bff
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php
@@ -0,0 +1,216 @@
+getCalendarObject($calendarId, $uri);
+ }, $uris);
+ }
+
+ /**
+ * Performs a calendar-query on the contents of this calendar.
+ *
+ * The calendar-query is defined in RFC4791 : CalDAV. Using the
+ * calendar-query it is possible for a client to request a specific set of
+ * object, based on contents of iCalendar properties, date-ranges and
+ * iCalendar component types (VTODO, VEVENT).
+ *
+ * This method should just return a list of (relative) urls that match this
+ * query.
+ *
+ * The list of filters are specified as an array. The exact array is
+ * documented by \Sabre\CalDAV\CalendarQueryParser.
+ *
+ * Note that it is extremely likely that getCalendarObject for every path
+ * returned from this method will be called almost immediately after. You
+ * may want to anticipate this to speed up these requests.
+ *
+ * This method provides a default implementation, which parses *all* the
+ * iCalendar objects in the specified calendar.
+ *
+ * This default may well be good enough for personal use, and calendars
+ * that aren't very large. But if you anticipate high usage, big calendars
+ * or high loads, you are strongly advised to optimize certain paths.
+ *
+ * The best way to do so is override this method and to optimize
+ * specifically for 'common filters'.
+ *
+ * Requests that are extremely common are:
+ * * requests for just VEVENTS
+ * * requests for just VTODO
+ * * requests with a time-range-filter on either VEVENT or VTODO.
+ *
+ * ..and combinations of these requests. It may not be worth it to try to
+ * handle every possible situation and just rely on the (relatively
+ * easy to use) CalendarQueryValidator to handle the rest.
+ *
+ * Note that especially time-range-filters may be difficult to parse. A
+ * time-range filter specified on a VEVENT must for instance also handle
+ * recurrence rules correctly.
+ * A good example of how to interpret all these filters can also simply
+ * be found in \Sabre\CalDAV\CalendarQueryFilter. This class is as correct
+ * as possible, so it gives you a good idea on what type of stuff you need
+ * to think of.
+ *
+ * @param mixed $calendarId
+ *
+ * @return array
+ */
+ public function calendarQuery($calendarId, array $filters)
+ {
+ $result = [];
+ $objects = $this->getCalendarObjects($calendarId);
+
+ foreach ($objects as $object) {
+ if ($this->validateFilterForObject($object, $filters)) {
+ $result[] = $object['uri'];
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * This method validates if a filter (as passed to calendarQuery) matches
+ * the given object.
+ *
+ * @return bool
+ */
+ protected function validateFilterForObject(array $object, array $filters)
+ {
+ // Unfortunately, setting the 'calendardata' here is optional. If
+ // it was excluded, we actually need another call to get this as
+ // well.
+ if (!isset($object['calendardata'])) {
+ $object = $this->getCalendarObject($object['calendarid'], $object['uri']);
+ }
+
+ $vObject = VObject\Reader::read($object['calendardata']);
+
+ $validator = new CalDAV\CalendarQueryValidator();
+ $result = $validator->validate($vObject, $filters);
+
+ // Destroy circular references so PHP will GC the object.
+ $vObject->destroy();
+
+ return $result;
+ }
+
+ /**
+ * Searches through all of a users calendars and calendar objects to find
+ * an object with a specific UID.
+ *
+ * This method should return the path to this object, relative to the
+ * calendar home, so this path usually only contains two parts:
+ *
+ * calendarpath/objectpath.ics
+ *
+ * If the uid is not found, return null.
+ *
+ * This method should only consider * objects that the principal owns, so
+ * any calendars owned by other principals that also appear in this
+ * collection should be ignored.
+ *
+ * @param string $principalUri
+ * @param string $uid
+ *
+ * @return string|null
+ */
+ public function getCalendarObjectByUID($principalUri, $uid)
+ {
+ // Note: this is a super slow naive implementation of this method. You
+ // are highly recommended to optimize it, if your backend allows it.
+ foreach ($this->getCalendarsForUser($principalUri) as $calendar) {
+ // We must ignore calendars owned by other principals.
+ if ($calendar['principaluri'] !== $principalUri) {
+ continue;
+ }
+
+ // Ignore calendars that are shared.
+ if (isset($calendar['{http://sabredav.org/ns}owner-principal']) && $calendar['{http://sabredav.org/ns}owner-principal'] !== $principalUri) {
+ continue;
+ }
+
+ $results = $this->calendarQuery(
+ $calendar['id'],
+ [
+ 'name' => 'VCALENDAR',
+ 'prop-filters' => [],
+ 'comp-filters' => [
+ [
+ 'name' => 'VEVENT',
+ 'is-not-defined' => false,
+ 'time-range' => null,
+ 'comp-filters' => [],
+ 'prop-filters' => [
+ [
+ 'name' => 'UID',
+ 'is-not-defined' => false,
+ 'time-range' => null,
+ 'text-match' => [
+ 'value' => $uid,
+ 'negate-condition' => false,
+ 'collation' => 'i;octet',
+ ],
+ 'param-filters' => [],
+ ],
+ ],
+ ],
+ ],
+ ]
+ );
+ if ($results) {
+ // We have a match
+ return $calendar['uri'].'/'.$results[0];
+ }
+ }
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/BackendInterface.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/BackendInterface.php
new file mode 100644
index 0000000..ccaa251
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/BackendInterface.php
@@ -0,0 +1,273 @@
+ 'displayname',
+ '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description',
+ '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => 'timezone',
+ '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder',
+ '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor',
+ ];
+
+ /**
+ * List of subscription properties, and how they map to database fieldnames.
+ *
+ * @var array
+ */
+ public $subscriptionPropertyMap = [
+ '{DAV:}displayname' => 'displayname',
+ '{http://apple.com/ns/ical/}refreshrate' => 'refreshrate',
+ '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder',
+ '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor',
+ '{http://calendarserver.org/ns/}subscribed-strip-todos' => 'striptodos',
+ '{http://calendarserver.org/ns/}subscribed-strip-alarms' => 'stripalarms',
+ '{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments',
+ ];
+
+ /**
+ * Creates the backend.
+ */
+ public function __construct(\PDO $pdo)
+ {
+ $this->pdo = $pdo;
+ }
+
+ /**
+ * Returns a list of calendars for a principal.
+ *
+ * Every project is an array with the following keys:
+ * * id, a unique id that will be used by other functions to modify the
+ * calendar. This can be the same as the uri or a database key.
+ * * uri. This is just the 'base uri' or 'filename' of the calendar.
+ * * principaluri. The owner of the calendar. Almost always the same as
+ * principalUri passed to this method.
+ *
+ * Furthermore it can contain webdav properties in clark notation. A very
+ * common one is '{DAV:}displayname'.
+ *
+ * Many clients also require:
+ * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
+ * For this property, you can just return an instance of
+ * Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet.
+ *
+ * If you return {http://sabredav.org/ns}read-only and set the value to 1,
+ * ACL will automatically be put in read-only mode.
+ *
+ * @param string $principalUri
+ *
+ * @return array
+ */
+ public function getCalendarsForUser($principalUri)
+ {
+ $fields = array_values($this->propertyMap);
+ $fields[] = 'calendarid';
+ $fields[] = 'uri';
+ $fields[] = 'synctoken';
+ $fields[] = 'components';
+ $fields[] = 'principaluri';
+ $fields[] = 'transparent';
+ $fields[] = 'access';
+
+ // Making fields a comma-delimited list
+ $fields = implode(', ', $fields);
+ $stmt = $this->pdo->prepare(<<calendarInstancesTableName}.id as id, $fields FROM {$this->calendarInstancesTableName}
+ LEFT JOIN {$this->calendarTableName} ON
+ {$this->calendarInstancesTableName}.calendarid = {$this->calendarTableName}.id
+WHERE principaluri = ? ORDER BY calendarorder ASC
+SQL
+ );
+ $stmt->execute([$principalUri]);
+
+ $calendars = [];
+ while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $components = [];
+ if ($row['components']) {
+ $components = explode(',', $row['components']);
+ }
+
+ $calendar = [
+ 'id' => [(int) $row['calendarid'], (int) $row['id']],
+ 'uri' => $row['uri'],
+ 'principaluri' => $row['principaluri'],
+ '{'.CalDAV\Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ? $row['synctoken'] : '0'),
+ '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
+ '{'.CalDAV\Plugin::NS_CALDAV.'}supported-calendar-component-set' => new CalDAV\Xml\Property\SupportedCalendarComponentSet($components),
+ '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-calendar-transp' => new CalDAV\Xml\Property\ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'),
+ 'share-resource-uri' => '/ns/share/'.$row['calendarid'],
+ ];
+
+ $calendar['share-access'] = (int) $row['access'];
+ // 1 = owner, 2 = readonly, 3 = readwrite
+ if ($row['access'] > 1) {
+ // We need to find more information about the original owner.
+ //$stmt2 = $this->pdo->prepare('SELECT principaluri FROM ' . $this->calendarInstancesTableName . ' WHERE access = 1 AND id = ?');
+ //$stmt2->execute([$row['id']]);
+
+ // read-only is for backwards compatibility. Might go away in
+ // the future.
+ $calendar['read-only'] = \Sabre\DAV\Sharing\Plugin::ACCESS_READ === (int) $row['access'];
+ }
+
+ foreach ($this->propertyMap as $xmlName => $dbName) {
+ $calendar[$xmlName] = $row[$dbName];
+ }
+
+ $calendars[] = $calendar;
+ }
+
+ return $calendars;
+ }
+
+ /**
+ * Creates a new calendar for a principal.
+ *
+ * If the creation was a success, an id must be returned that can be used
+ * to reference this calendar in other methods, such as updateCalendar.
+ *
+ * @param string $principalUri
+ * @param string $calendarUri
+ *
+ * @return string
+ */
+ public function createCalendar($principalUri, $calendarUri, array $properties)
+ {
+ $fieldNames = [
+ 'principaluri',
+ 'uri',
+ 'transparent',
+ 'calendarid',
+ ];
+ $values = [
+ ':principaluri' => $principalUri,
+ ':uri' => $calendarUri,
+ ':transparent' => 0,
+ ];
+
+ $sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
+ if (!isset($properties[$sccs])) {
+ // Default value
+ $components = 'VEVENT,VTODO';
+ } else {
+ if (!($properties[$sccs] instanceof CalDAV\Xml\Property\SupportedCalendarComponentSet)) {
+ throw new DAV\Exception('The '.$sccs.' property must be of type: \Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet');
+ }
+ $components = implode(',', $properties[$sccs]->getValue());
+ }
+ $transp = '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-calendar-transp';
+ if (isset($properties[$transp])) {
+ $values[':transparent'] = 'transparent' === $properties[$transp]->getValue() ? 1 : 0;
+ }
+ $stmt = $this->pdo->prepare('INSERT INTO '.$this->calendarTableName.' (synctoken, components) VALUES (1, ?)');
+ $stmt->execute([$components]);
+
+ $calendarId = $this->pdo->lastInsertId(
+ $this->calendarTableName.'_id_seq'
+ );
+
+ $values[':calendarid'] = $calendarId;
+
+ foreach ($this->propertyMap as $xmlName => $dbName) {
+ if (isset($properties[$xmlName])) {
+ $values[':'.$dbName] = $properties[$xmlName];
+ $fieldNames[] = $dbName;
+ }
+ }
+
+ $stmt = $this->pdo->prepare('INSERT INTO '.$this->calendarInstancesTableName.' ('.implode(', ', $fieldNames).') VALUES ('.implode(', ', array_keys($values)).')');
+
+ $stmt->execute($values);
+
+ return [
+ $calendarId,
+ $this->pdo->lastInsertId($this->calendarInstancesTableName.'_id_seq'),
+ ];
+ }
+
+ /**
+ * Updates properties for a calendar.
+ *
+ * The list of mutations is stored in a Sabre\DAV\PropPatch object.
+ * To do the actual updates, you must tell this object which properties
+ * you're going to process with the handle() method.
+ *
+ * Calling the handle method is like telling the PropPatch object "I
+ * promise I can handle updating this property".
+ *
+ * Read the PropPatch documentation for more info and examples.
+ *
+ * @param mixed $calendarId
+ */
+ public function updateCalendar($calendarId, PropPatch $propPatch)
+ {
+ if (!is_array($calendarId)) {
+ throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId');
+ }
+ list($calendarId, $instanceId) = $calendarId;
+
+ $supportedProperties = array_keys($this->propertyMap);
+ $supportedProperties[] = '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-calendar-transp';
+
+ $propPatch->handle($supportedProperties, function ($mutations) use ($calendarId, $instanceId) {
+ $newValues = [];
+ foreach ($mutations as $propertyName => $propertyValue) {
+ switch ($propertyName) {
+ case '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-calendar-transp':
+ $fieldName = 'transparent';
+ $newValues[$fieldName] = 'transparent' === $propertyValue->getValue();
+ break;
+ default:
+ $fieldName = $this->propertyMap[$propertyName];
+ $newValues[$fieldName] = $propertyValue;
+ break;
+ }
+ }
+ $valuesSql = [];
+ foreach ($newValues as $fieldName => $value) {
+ $valuesSql[] = $fieldName.' = ?';
+ }
+
+ $stmt = $this->pdo->prepare('UPDATE '.$this->calendarInstancesTableName.' SET '.implode(', ', $valuesSql).' WHERE id = ?');
+ $newValues['id'] = $instanceId;
+ $stmt->execute(array_values($newValues));
+
+ $this->addChange($calendarId, '', 2);
+
+ return true;
+ });
+ }
+
+ /**
+ * Delete a calendar and all it's objects.
+ *
+ * @param mixed $calendarId
+ */
+ public function deleteCalendar($calendarId)
+ {
+ if (!is_array($calendarId)) {
+ throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId');
+ }
+ list($calendarId, $instanceId) = $calendarId;
+
+ $stmt = $this->pdo->prepare('SELECT access FROM '.$this->calendarInstancesTableName.' where id = ?');
+ $stmt->execute([$instanceId]);
+ $access = (int) $stmt->fetchColumn();
+
+ if (\Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER === $access) {
+ /**
+ * If the user is the owner of the calendar, we delete all data and all
+ * instances.
+ **/
+ $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarObjectTableName.' WHERE calendarid = ?');
+ $stmt->execute([$calendarId]);
+
+ $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarChangesTableName.' WHERE calendarid = ?');
+ $stmt->execute([$calendarId]);
+
+ $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarInstancesTableName.' WHERE calendarid = ?');
+ $stmt->execute([$calendarId]);
+
+ $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarTableName.' WHERE id = ?');
+ $stmt->execute([$calendarId]);
+ } else {
+ /**
+ * If it was an instance of a shared calendar, we only delete that
+ * instance.
+ */
+ $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarInstancesTableName.' WHERE id = ?');
+ $stmt->execute([$instanceId]);
+ }
+ }
+
+ /**
+ * Returns all calendar objects within a calendar.
+ *
+ * Every item contains an array with the following keys:
+ * * calendardata - The iCalendar-compatible calendar data
+ * * uri - a unique key which will be used to construct the uri. This can
+ * be any arbitrary string, but making sure it ends with '.ics' is a
+ * good idea. This is only the basename, or filename, not the full
+ * path.
+ * * lastmodified - a timestamp of the last modification time
+ * * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
+ * ' "abcdef"')
+ * * size - The size of the calendar objects, in bytes.
+ * * component - optional, a string containing the type of object, such
+ * as 'vevent' or 'vtodo'. If specified, this will be used to populate
+ * the Content-Type header.
+ *
+ * Note that the etag is optional, but it's highly encouraged to return for
+ * speed reasons.
+ *
+ * The calendardata is also optional. If it's not returned
+ * 'getCalendarObject' will be called later, which *is* expected to return
+ * calendardata.
+ *
+ * If neither etag or size are specified, the calendardata will be
+ * used/fetched to determine these numbers. If both are specified the
+ * amount of times this is needed is reduced by a great degree.
+ *
+ * @param mixed $calendarId
+ *
+ * @return array
+ */
+ public function getCalendarObjects($calendarId)
+ {
+ if (!is_array($calendarId)) {
+ throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId');
+ }
+ list($calendarId, $instanceId) = $calendarId;
+
+ $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size, componenttype FROM '.$this->calendarObjectTableName.' WHERE calendarid = ?');
+ $stmt->execute([$calendarId]);
+
+ $result = [];
+ foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
+ $result[] = [
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ 'lastmodified' => (int) $row['lastmodified'],
+ 'etag' => '"'.$row['etag'].'"',
+ 'size' => (int) $row['size'],
+ 'component' => strtolower($row['componenttype']),
+ ];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns information from a single calendar object, based on it's object
+ * uri.
+ *
+ * The object uri is only the basename, or filename and not a full path.
+ *
+ * The returned array must have the same keys as getCalendarObjects. The
+ * 'calendardata' object is required here though, while it's not required
+ * for getCalendarObjects.
+ *
+ * This method must return null if the object did not exist.
+ *
+ * @param mixed $calendarId
+ * @param string $objectUri
+ *
+ * @return array|null
+ */
+ public function getCalendarObject($calendarId, $objectUri)
+ {
+ if (!is_array($calendarId)) {
+ throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId');
+ }
+ list($calendarId, $instanceId) = $calendarId;
+
+ $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM '.$this->calendarObjectTableName.' WHERE calendarid = ? AND uri = ?');
+ $stmt->execute([$calendarId, $objectUri]);
+ $row = $stmt->fetch(\PDO::FETCH_ASSOC);
+
+ if (!$row) {
+ return null;
+ }
+
+ return [
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ 'lastmodified' => (int) $row['lastmodified'],
+ 'etag' => '"'.$row['etag'].'"',
+ 'size' => (int) $row['size'],
+ 'calendardata' => $row['calendardata'],
+ 'component' => strtolower($row['componenttype']),
+ ];
+ }
+
+ /**
+ * Returns a list of calendar objects.
+ *
+ * This method should work identical to getCalendarObject, but instead
+ * return all the calendar objects in the list as an array.
+ *
+ * If the backend supports this, it may allow for some speed-ups.
+ *
+ * @param mixed $calendarId
+ *
+ * @return array
+ */
+ public function getMultipleCalendarObjects($calendarId, array $uris)
+ {
+ if (!is_array($calendarId)) {
+ throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId');
+ }
+ list($calendarId, $instanceId) = $calendarId;
+
+ $result = [];
+ foreach (array_chunk($uris, 900) as $chunk) {
+ $query = 'SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM '.$this->calendarObjectTableName.' WHERE calendarid = ? AND uri IN (';
+ // Inserting a whole bunch of question marks
+ $query .= implode(',', array_fill(0, count($chunk), '?'));
+ $query .= ')';
+
+ $stmt = $this->pdo->prepare($query);
+ $stmt->execute(array_merge([$calendarId], $chunk));
+
+ while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $result[] = [
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ 'lastmodified' => (int) $row['lastmodified'],
+ 'etag' => '"'.$row['etag'].'"',
+ 'size' => (int) $row['size'],
+ 'calendardata' => $row['calendardata'],
+ 'component' => strtolower($row['componenttype']),
+ ];
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Creates a new calendar object.
+ *
+ * The object uri is only the basename, or filename and not a full path.
+ *
+ * It is possible return an etag from this function, which will be used in
+ * the response to this PUT request. Note that the ETag must be surrounded
+ * by double-quotes.
+ *
+ * However, you should only really return this ETag if you don't mangle the
+ * calendar-data. If the result of a subsequent GET to this object is not
+ * the exact same as this request body, you should omit the ETag.
+ *
+ * @param mixed $calendarId
+ * @param string $objectUri
+ * @param string $calendarData
+ *
+ * @return string|null
+ */
+ public function createCalendarObject($calendarId, $objectUri, $calendarData)
+ {
+ if (!is_array($calendarId)) {
+ throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId');
+ }
+ list($calendarId, $instanceId) = $calendarId;
+
+ $extraData = $this->getDenormalizedData($calendarData);
+
+ $stmt = $this->pdo->prepare('INSERT INTO '.$this->calendarObjectTableName.' (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES (?,?,?,?,?,?,?,?,?,?)');
+ $stmt->execute([
+ $calendarId,
+ $objectUri,
+ $calendarData,
+ time(),
+ $extraData['etag'],
+ $extraData['size'],
+ $extraData['componentType'],
+ $extraData['firstOccurence'],
+ $extraData['lastOccurence'],
+ $extraData['uid'],
+ ]);
+ $this->addChange($calendarId, $objectUri, 1);
+
+ return '"'.$extraData['etag'].'"';
+ }
+
+ /**
+ * Updates an existing calendarobject, based on it's uri.
+ *
+ * The object uri is only the basename, or filename and not a full path.
+ *
+ * It is possible return an etag from this function, which will be used in
+ * the response to this PUT request. Note that the ETag must be surrounded
+ * by double-quotes.
+ *
+ * However, you should only really return this ETag if you don't mangle the
+ * calendar-data. If the result of a subsequent GET to this object is not
+ * the exact same as this request body, you should omit the ETag.
+ *
+ * @param mixed $calendarId
+ * @param string $objectUri
+ * @param string $calendarData
+ *
+ * @return string|null
+ */
+ public function updateCalendarObject($calendarId, $objectUri, $calendarData)
+ {
+ if (!is_array($calendarId)) {
+ throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId');
+ }
+ list($calendarId, $instanceId) = $calendarId;
+
+ $extraData = $this->getDenormalizedData($calendarData);
+
+ $stmt = $this->pdo->prepare('UPDATE '.$this->calendarObjectTableName.' SET calendardata = ?, lastmodified = ?, etag = ?, size = ?, componenttype = ?, firstoccurence = ?, lastoccurence = ?, uid = ? WHERE calendarid = ? AND uri = ?');
+ $stmt->execute([$calendarData, time(), $extraData['etag'], $extraData['size'], $extraData['componentType'], $extraData['firstOccurence'], $extraData['lastOccurence'], $extraData['uid'], $calendarId, $objectUri]);
+
+ $this->addChange($calendarId, $objectUri, 2);
+
+ return '"'.$extraData['etag'].'"';
+ }
+
+ /**
+ * Parses some information from calendar objects, used for optimized
+ * calendar-queries.
+ *
+ * Returns an array with the following keys:
+ * * etag - An md5 checksum of the object without the quotes.
+ * * size - Size of the object in bytes
+ * * componentType - VEVENT, VTODO or VJOURNAL
+ * * firstOccurence
+ * * lastOccurence
+ * * uid - value of the UID property
+ *
+ * @param string $calendarData
+ *
+ * @return array
+ */
+ protected function getDenormalizedData($calendarData)
+ {
+ $vObject = VObject\Reader::read($calendarData);
+ $componentType = null;
+ $component = null;
+ $firstOccurence = null;
+ $lastOccurence = null;
+ $uid = null;
+ foreach ($vObject->getComponents() as $component) {
+ if ('VTIMEZONE' !== $component->name) {
+ $componentType = $component->name;
+ $uid = (string) $component->UID;
+ break;
+ }
+ }
+ if (!$componentType) {
+ throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
+ }
+ if ('VEVENT' === $componentType) {
+ $firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp();
+ // Finding the last occurence is a bit harder
+ if (!isset($component->RRULE)) {
+ if (isset($component->DTEND)) {
+ $lastOccurence = $component->DTEND->getDateTime()->getTimeStamp();
+ } elseif (isset($component->DURATION)) {
+ $endDate = clone $component->DTSTART->getDateTime();
+ $endDate = $endDate->add(VObject\DateTimeParser::parse($component->DURATION->getValue()));
+ $lastOccurence = $endDate->getTimeStamp();
+ } elseif (!$component->DTSTART->hasTime()) {
+ $endDate = clone $component->DTSTART->getDateTime();
+ $endDate = $endDate->modify('+1 day');
+ $lastOccurence = $endDate->getTimeStamp();
+ } else {
+ $lastOccurence = $firstOccurence;
+ }
+ } else {
+ $it = new VObject\Recur\EventIterator($vObject, (string) $component->UID);
+ $maxDate = new \DateTime(self::MAX_DATE);
+ if ($it->isInfinite()) {
+ $lastOccurence = $maxDate->getTimeStamp();
+ } else {
+ $end = $it->getDtEnd();
+ while ($it->valid() && $end < $maxDate) {
+ $end = $it->getDtEnd();
+ $it->next();
+ }
+ $lastOccurence = $end->getTimeStamp();
+ }
+ }
+
+ // Ensure Occurence values are positive
+ if ($firstOccurence < 0) {
+ $firstOccurence = 0;
+ }
+ if ($lastOccurence < 0) {
+ $lastOccurence = 0;
+ }
+ }
+
+ // Destroy circular references to PHP will GC the object.
+ $vObject->destroy();
+
+ return [
+ 'etag' => md5($calendarData),
+ 'size' => strlen($calendarData),
+ 'componentType' => $componentType,
+ 'firstOccurence' => $firstOccurence,
+ 'lastOccurence' => $lastOccurence,
+ 'uid' => $uid,
+ ];
+ }
+
+ /**
+ * Deletes an existing calendar object.
+ *
+ * The object uri is only the basename, or filename and not a full path.
+ *
+ * @param mixed $calendarId
+ * @param string $objectUri
+ */
+ public function deleteCalendarObject($calendarId, $objectUri)
+ {
+ if (!is_array($calendarId)) {
+ throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId');
+ }
+ list($calendarId, $instanceId) = $calendarId;
+
+ $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarObjectTableName.' WHERE calendarid = ? AND uri = ?');
+ $stmt->execute([$calendarId, $objectUri]);
+
+ $this->addChange($calendarId, $objectUri, 3);
+ }
+
+ /**
+ * Performs a calendar-query on the contents of this calendar.
+ *
+ * The calendar-query is defined in RFC4791 : CalDAV. Using the
+ * calendar-query it is possible for a client to request a specific set of
+ * object, based on contents of iCalendar properties, date-ranges and
+ * iCalendar component types (VTODO, VEVENT).
+ *
+ * This method should just return a list of (relative) urls that match this
+ * query.
+ *
+ * The list of filters are specified as an array. The exact array is
+ * documented by \Sabre\CalDAV\CalendarQueryParser.
+ *
+ * Note that it is extremely likely that getCalendarObject for every path
+ * returned from this method will be called almost immediately after. You
+ * may want to anticipate this to speed up these requests.
+ *
+ * This method provides a default implementation, which parses *all* the
+ * iCalendar objects in the specified calendar.
+ *
+ * This default may well be good enough for personal use, and calendars
+ * that aren't very large. But if you anticipate high usage, big calendars
+ * or high loads, you are strongly advised to optimize certain paths.
+ *
+ * The best way to do so is override this method and to optimize
+ * specifically for 'common filters'.
+ *
+ * Requests that are extremely common are:
+ * * requests for just VEVENTS
+ * * requests for just VTODO
+ * * requests with a time-range-filter on a VEVENT.
+ *
+ * ..and combinations of these requests. It may not be worth it to try to
+ * handle every possible situation and just rely on the (relatively
+ * easy to use) CalendarQueryValidator to handle the rest.
+ *
+ * Note that especially time-range-filters may be difficult to parse. A
+ * time-range filter specified on a VEVENT must for instance also handle
+ * recurrence rules correctly.
+ * A good example of how to interpret all these filters can also simply
+ * be found in \Sabre\CalDAV\CalendarQueryFilter. This class is as correct
+ * as possible, so it gives you a good idea on what type of stuff you need
+ * to think of.
+ *
+ * This specific implementation (for the PDO) backend optimizes filters on
+ * specific components, and VEVENT time-ranges.
+ *
+ * @param mixed $calendarId
+ *
+ * @return array
+ */
+ public function calendarQuery($calendarId, array $filters)
+ {
+ if (!is_array($calendarId)) {
+ throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId');
+ }
+ list($calendarId, $instanceId) = $calendarId;
+
+ $componentType = null;
+ $requirePostFilter = true;
+ $timeRange = null;
+
+ // if no filters were specified, we don't need to filter after a query
+ if (!$filters['prop-filters'] && !$filters['comp-filters']) {
+ $requirePostFilter = false;
+ }
+
+ // Figuring out if there's a component filter
+ if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
+ $componentType = $filters['comp-filters'][0]['name'];
+
+ // Checking if we need post-filters
+ $has_time_range = array_key_exists('time-range', $filters['comp-filters'][0]) && $filters['comp-filters'][0]['time-range'];
+ if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$has_time_range && !$filters['comp-filters'][0]['prop-filters']) {
+ $requirePostFilter = false;
+ }
+ // There was a time-range filter
+ if ('VEVENT' == $componentType && $has_time_range) {
+ $timeRange = $filters['comp-filters'][0]['time-range'];
+
+ // If start time OR the end time is not specified, we can do a
+ // 100% accurate mysql query.
+ if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && $timeRange) {
+ if ((array_key_exists('start', $timeRange) && !$timeRange['start']) || (array_key_exists('end', $timeRange) && !$timeRange['end'])) {
+ $requirePostFilter = false;
+ }
+ }
+ }
+ }
+
+ if ($requirePostFilter) {
+ $query = 'SELECT uri, calendardata FROM '.$this->calendarObjectTableName.' WHERE calendarid = :calendarid';
+ } else {
+ $query = 'SELECT uri FROM '.$this->calendarObjectTableName.' WHERE calendarid = :calendarid';
+ }
+
+ $values = [
+ 'calendarid' => $calendarId,
+ ];
+
+ if ($componentType) {
+ $query .= ' AND componenttype = :componenttype';
+ $values['componenttype'] = $componentType;
+ }
+
+ if ($timeRange && array_key_exists('start', $timeRange) && $timeRange['start']) {
+ $query .= ' AND lastoccurence > :startdate';
+ $values['startdate'] = $timeRange['start']->getTimeStamp();
+ }
+ if ($timeRange && array_key_exists('end', $timeRange) && $timeRange['end']) {
+ $query .= ' AND firstoccurence < :enddate';
+ $values['enddate'] = $timeRange['end']->getTimeStamp();
+ }
+
+ $stmt = $this->pdo->prepare($query);
+ $stmt->execute($values);
+
+ $result = [];
+ while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ if ($requirePostFilter) {
+ if (!$this->validateFilterForObject($row, $filters)) {
+ continue;
+ }
+ }
+ $result[] = $row['uri'];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Searches through all of a users calendars and calendar objects to find
+ * an object with a specific UID.
+ *
+ * This method should return the path to this object, relative to the
+ * calendar home, so this path usually only contains two parts:
+ *
+ * calendarpath/objectpath.ics
+ *
+ * If the uid is not found, return null.
+ *
+ * This method should only consider * objects that the principal owns, so
+ * any calendars owned by other principals that also appear in this
+ * collection should be ignored.
+ *
+ * @param string $principalUri
+ * @param string $uid
+ *
+ * @return string|null
+ */
+ public function getCalendarObjectByUID($principalUri, $uid)
+ {
+ $query = <<calendarObjectTableName AS calendarobjects
+LEFT JOIN
+ $this->calendarInstancesTableName AS calendar_instances
+ ON calendarobjects.calendarid = calendar_instances.calendarid
+WHERE
+ calendar_instances.principaluri = ?
+ AND
+ calendarobjects.uid = ?
+ AND
+ calendar_instances.access = 1
+SQL;
+
+ $stmt = $this->pdo->prepare($query);
+ $stmt->execute([$principalUri, $uid]);
+
+ if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ return $row['calendaruri'].'/'.$row['objecturi'];
+ }
+ }
+
+ /**
+ * The getChanges method returns all the changes that have happened, since
+ * the specified syncToken in the specified calendar.
+ *
+ * This function should return an array, such as the following:
+ *
+ * [
+ * 'syncToken' => 'The current synctoken',
+ * 'added' => [
+ * 'new.txt',
+ * ],
+ * 'modified' => [
+ * 'modified.txt',
+ * ],
+ * 'deleted' => [
+ * 'foo.php.bak',
+ * 'old.txt'
+ * ]
+ * ];
+ *
+ * The returned syncToken property should reflect the *current* syncToken
+ * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
+ * property this is needed here too, to ensure the operation is atomic.
+ *
+ * If the $syncToken argument is specified as null, this is an initial
+ * sync, and all members should be reported.
+ *
+ * The modified property is an array of nodenames that have changed since
+ * the last token.
+ *
+ * The deleted property is an array with nodenames, that have been deleted
+ * from collection.
+ *
+ * The $syncLevel argument is basically the 'depth' of the report. If it's
+ * 1, you only have to report changes that happened only directly in
+ * immediate descendants. If it's 2, it should also include changes from
+ * the nodes below the child collections. (grandchildren)
+ *
+ * The $limit argument allows a client to specify how many results should
+ * be returned at most. If the limit is not specified, it should be treated
+ * as infinite.
+ *
+ * If the limit (infinite or not) is higher than you're willing to return,
+ * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
+ *
+ * If the syncToken is expired (due to data cleanup) or unknown, you must
+ * return null.
+ *
+ * The limit is 'suggestive'. You are free to ignore it.
+ *
+ * @param mixed $calendarId
+ * @param string $syncToken
+ * @param int $syncLevel
+ * @param int $limit
+ *
+ * @return array|null
+ */
+ public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null)
+ {
+ if (!is_array($calendarId)) {
+ throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId');
+ }
+ list($calendarId, $instanceId) = $calendarId;
+
+ $result = [
+ 'added' => [],
+ 'modified' => [],
+ 'deleted' => [],
+ ];
+
+ if ($syncToken) {
+ $query = 'SELECT uri, operation, synctoken FROM '.$this->calendarChangesTableName.' WHERE synctoken >= ? AND calendarid = ? ORDER BY synctoken';
+ if ($limit > 0) {
+ // Fetch one more raw to detect result truncation
+ $query .= ' LIMIT '.((int) $limit + 1);
+ }
+
+ // Fetching all changes
+ $stmt = $this->pdo->prepare($query);
+ $stmt->execute([$syncToken, $calendarId]);
+
+ $changes = [];
+
+ // This loop ensures that any duplicates are overwritten, only the
+ // last change on a node is relevant.
+ while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $changes[$row['uri']] = $row;
+ }
+ $currentToken = null;
+
+ $result_count = 0;
+ foreach ($changes as $uri => $operation) {
+ if (!is_null($limit) && $result_count >= $limit) {
+ $result['result_truncated'] = true;
+ break;
+ }
+
+ if (null === $currentToken || $currentToken < $operation['synctoken'] + 1) {
+ // SyncToken in CalDAV perspective is consistently the next number of the last synced change event in this class.
+ $currentToken = $operation['synctoken'] + 1;
+ }
+
+ ++$result_count;
+ switch ($operation['operation']) {
+ case 1:
+ $result['added'][] = $uri;
+ break;
+ case 2:
+ $result['modified'][] = $uri;
+ break;
+ case 3:
+ $result['deleted'][] = $uri;
+ break;
+ }
+ }
+
+ if (!is_null($currentToken)) {
+ $result['syncToken'] = $currentToken;
+ } else {
+ // This means returned value is equivalent to syncToken
+ $result['syncToken'] = $syncToken;
+ }
+ } else {
+ // Current synctoken
+ $stmt = $this->pdo->prepare('SELECT synctoken FROM '.$this->calendarTableName.' WHERE id = ?');
+ $stmt->execute([$calendarId]);
+ $currentToken = $stmt->fetchColumn(0);
+
+ if (is_null($currentToken)) {
+ return null;
+ }
+ $result['syncToken'] = $currentToken;
+
+ // No synctoken supplied, this is the initial sync.
+ $query = 'SELECT uri FROM '.$this->calendarObjectTableName.' WHERE calendarid = ?';
+ $stmt = $this->pdo->prepare($query);
+ $stmt->execute([$calendarId]);
+
+ $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Adds a change record to the calendarchanges table.
+ *
+ * @param mixed $calendarId
+ * @param string $objectUri
+ * @param int $operation 1 = add, 2 = modify, 3 = delete
+ */
+ protected function addChange($calendarId, $objectUri, $operation)
+ {
+ $stmt = $this->pdo->prepare('INSERT INTO '.$this->calendarChangesTableName.' (uri, synctoken, calendarid, operation) SELECT ?, synctoken, ?, ? FROM '.$this->calendarTableName.' WHERE id = ?');
+ $stmt->execute([
+ $objectUri,
+ $calendarId,
+ $operation,
+ $calendarId,
+ ]);
+ $stmt = $this->pdo->prepare('UPDATE '.$this->calendarTableName.' SET synctoken = synctoken + 1 WHERE id = ?');
+ $stmt->execute([
+ $calendarId,
+ ]);
+ }
+
+ /**
+ * Returns a list of subscriptions for a principal.
+ *
+ * Every subscription is an array with the following keys:
+ * * id, a unique id that will be used by other functions to modify the
+ * subscription. This can be the same as the uri or a database key.
+ * * uri. This is just the 'base uri' or 'filename' of the subscription.
+ * * principaluri. The owner of the subscription. Almost always the same as
+ * principalUri passed to this method.
+ * * source. Url to the actual feed
+ *
+ * Furthermore, all the subscription info must be returned too:
+ *
+ * 1. {DAV:}displayname
+ * 2. {http://apple.com/ns/ical/}refreshrate
+ * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
+ * should not be stripped).
+ * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
+ * should not be stripped).
+ * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
+ * attachments should not be stripped).
+ * 7. {http://apple.com/ns/ical/}calendar-color
+ * 8. {http://apple.com/ns/ical/}calendar-order
+ * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
+ * (should just be an instance of
+ * Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
+ * default components).
+ *
+ * @param string $principalUri
+ *
+ * @return array
+ */
+ public function getSubscriptionsForUser($principalUri)
+ {
+ $fields = array_values($this->subscriptionPropertyMap);
+ $fields[] = 'id';
+ $fields[] = 'uri';
+ $fields[] = 'source';
+ $fields[] = 'principaluri';
+ $fields[] = 'lastmodified';
+
+ // Making fields a comma-delimited list
+ $fields = implode(', ', $fields);
+ $stmt = $this->pdo->prepare('SELECT '.$fields.' FROM '.$this->calendarSubscriptionsTableName.' WHERE principaluri = ? ORDER BY calendarorder ASC');
+ $stmt->execute([$principalUri]);
+
+ $subscriptions = [];
+ while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $subscription = [
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ 'principaluri' => $row['principaluri'],
+ 'source' => $row['source'],
+ 'lastmodified' => $row['lastmodified'],
+
+ '{'.CalDAV\Plugin::NS_CALDAV.'}supported-calendar-component-set' => new CalDAV\Xml\Property\SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
+ ];
+
+ foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) {
+ if (!is_null($row[$dbName])) {
+ $subscription[$xmlName] = $row[$dbName];
+ }
+ }
+
+ $subscriptions[] = $subscription;
+ }
+
+ return $subscriptions;
+ }
+
+ /**
+ * Creates a new subscription for a principal.
+ *
+ * If the creation was a success, an id must be returned that can be used to reference
+ * this subscription in other methods, such as updateSubscription.
+ *
+ * @param string $principalUri
+ * @param string $uri
+ *
+ * @return mixed
+ */
+ public function createSubscription($principalUri, $uri, array $properties)
+ {
+ $fieldNames = [
+ 'principaluri',
+ 'uri',
+ 'source',
+ 'lastmodified',
+ ];
+
+ if (!isset($properties['{http://calendarserver.org/ns/}source'])) {
+ throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions');
+ }
+
+ $values = [
+ ':principaluri' => $principalUri,
+ ':uri' => $uri,
+ ':source' => $properties['{http://calendarserver.org/ns/}source']->getHref(),
+ ':lastmodified' => time(),
+ ];
+
+ foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) {
+ if (isset($properties[$xmlName])) {
+ $values[':'.$dbName] = $properties[$xmlName];
+ $fieldNames[] = $dbName;
+ }
+ }
+
+ $stmt = $this->pdo->prepare('INSERT INTO '.$this->calendarSubscriptionsTableName.' ('.implode(', ', $fieldNames).') VALUES ('.implode(', ', array_keys($values)).')');
+ $stmt->execute($values);
+
+ return $this->pdo->lastInsertId(
+ $this->calendarSubscriptionsTableName.'_id_seq'
+ );
+ }
+
+ /**
+ * Updates a subscription.
+ *
+ * The list of mutations is stored in a Sabre\DAV\PropPatch object.
+ * To do the actual updates, you must tell this object which properties
+ * you're going to process with the handle() method.
+ *
+ * Calling the handle method is like telling the PropPatch object "I
+ * promise I can handle updating this property".
+ *
+ * Read the PropPatch documentation for more info and examples.
+ *
+ * @param mixed $subscriptionId
+ */
+ public function updateSubscription($subscriptionId, PropPatch $propPatch)
+ {
+ $supportedProperties = array_keys($this->subscriptionPropertyMap);
+ $supportedProperties[] = '{http://calendarserver.org/ns/}source';
+
+ $propPatch->handle($supportedProperties, function ($mutations) use ($subscriptionId) {
+ $newValues = [];
+
+ foreach ($mutations as $propertyName => $propertyValue) {
+ if ('{http://calendarserver.org/ns/}source' === $propertyName) {
+ $newValues['source'] = $propertyValue->getHref();
+ } else {
+ $fieldName = $this->subscriptionPropertyMap[$propertyName];
+ $newValues[$fieldName] = $propertyValue;
+ }
+ }
+
+ // Now we're generating the sql query.
+ $valuesSql = [];
+ foreach ($newValues as $fieldName => $value) {
+ $valuesSql[] = $fieldName.' = ?';
+ }
+
+ $stmt = $this->pdo->prepare('UPDATE '.$this->calendarSubscriptionsTableName.' SET '.implode(', ', $valuesSql).', lastmodified = ? WHERE id = ?');
+ $newValues['lastmodified'] = time();
+ $newValues['id'] = $subscriptionId;
+ $stmt->execute(array_values($newValues));
+
+ return true;
+ });
+ }
+
+ /**
+ * Deletes a subscription.
+ *
+ * @param mixed $subscriptionId
+ */
+ public function deleteSubscription($subscriptionId)
+ {
+ $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarSubscriptionsTableName.' WHERE id = ?');
+ $stmt->execute([$subscriptionId]);
+ }
+
+ /**
+ * Returns a single scheduling object.
+ *
+ * The returned array should contain the following elements:
+ * * uri - A unique basename for the object. This will be used to
+ * construct a full uri.
+ * * calendardata - The iCalendar object
+ * * lastmodified - The last modification date. Can be an int for a unix
+ * timestamp, or a PHP DateTime object.
+ * * etag - A unique token that must change if the object changed.
+ * * size - The size of the object, in bytes.
+ *
+ * @param string $principalUri
+ * @param string $objectUri
+ *
+ * @return array
+ */
+ public function getSchedulingObject($principalUri, $objectUri)
+ {
+ $stmt = $this->pdo->prepare('SELECT uri, calendardata, lastmodified, etag, size FROM '.$this->schedulingObjectTableName.' WHERE principaluri = ? AND uri = ?');
+ $stmt->execute([$principalUri, $objectUri]);
+ $row = $stmt->fetch(\PDO::FETCH_ASSOC);
+
+ if (!$row) {
+ return null;
+ }
+
+ return [
+ 'uri' => $row['uri'],
+ 'calendardata' => $row['calendardata'],
+ 'lastmodified' => $row['lastmodified'],
+ 'etag' => '"'.$row['etag'].'"',
+ 'size' => (int) $row['size'],
+ ];
+ }
+
+ /**
+ * Returns all scheduling objects for the inbox collection.
+ *
+ * These objects should be returned as an array. Every item in the array
+ * should follow the same structure as returned from getSchedulingObject.
+ *
+ * The main difference is that 'calendardata' is optional.
+ *
+ * @param string $principalUri
+ *
+ * @return array
+ */
+ public function getSchedulingObjects($principalUri)
+ {
+ $stmt = $this->pdo->prepare('SELECT id, calendardata, uri, lastmodified, etag, size FROM '.$this->schedulingObjectTableName.' WHERE principaluri = ?');
+ $stmt->execute([$principalUri]);
+
+ $result = [];
+ foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
+ $result[] = [
+ 'calendardata' => $row['calendardata'],
+ 'uri' => $row['uri'],
+ 'lastmodified' => $row['lastmodified'],
+ 'etag' => '"'.$row['etag'].'"',
+ 'size' => (int) $row['size'],
+ ];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Deletes a scheduling object.
+ *
+ * @param string $principalUri
+ * @param string $objectUri
+ */
+ public function deleteSchedulingObject($principalUri, $objectUri)
+ {
+ $stmt = $this->pdo->prepare('DELETE FROM '.$this->schedulingObjectTableName.' WHERE principaluri = ? AND uri = ?');
+ $stmt->execute([$principalUri, $objectUri]);
+ }
+
+ /**
+ * Creates a new scheduling object. This should land in a users' inbox.
+ *
+ * @param string $principalUri
+ * @param string $objectUri
+ * @param string|resource $objectData
+ */
+ public function createSchedulingObject($principalUri, $objectUri, $objectData)
+ {
+ $stmt = $this->pdo->prepare('INSERT INTO '.$this->schedulingObjectTableName.' (principaluri, calendardata, uri, lastmodified, etag, size) VALUES (?, ?, ?, ?, ?, ?)');
+
+ if (is_resource($objectData)) {
+ $objectData = stream_get_contents($objectData);
+ }
+
+ $stmt->execute([$principalUri, $objectData, $objectUri, time(), md5($objectData), strlen($objectData)]);
+ }
+
+ /**
+ * Updates the list of shares.
+ *
+ * @param mixed $calendarId
+ * @param \Sabre\DAV\Xml\Element\Sharee[] $sharees
+ */
+ public function updateInvites($calendarId, array $sharees)
+ {
+ if (!is_array($calendarId)) {
+ throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId');
+ }
+ $currentInvites = $this->getInvites($calendarId);
+ list($calendarId, $instanceId) = $calendarId;
+
+ $removeStmt = $this->pdo->prepare('DELETE FROM '.$this->calendarInstancesTableName.' WHERE calendarid = ? AND share_href = ? AND access IN (2,3)');
+ $updateStmt = $this->pdo->prepare('UPDATE '.$this->calendarInstancesTableName.' SET access = ?, share_displayname = ?, share_invitestatus = ? WHERE calendarid = ? AND share_href = ?');
+
+ $insertStmt = $this->pdo->prepare('
+INSERT INTO '.$this->calendarInstancesTableName.'
+ (
+ calendarid,
+ principaluri,
+ access,
+ displayname,
+ uri,
+ description,
+ calendarorder,
+ calendarcolor,
+ timezone,
+ transparent,
+ share_href,
+ share_displayname,
+ share_invitestatus
+ )
+ SELECT
+ ?,
+ ?,
+ ?,
+ displayname,
+ ?,
+ description,
+ calendarorder,
+ calendarcolor,
+ timezone,
+ 1,
+ ?,
+ ?,
+ ?
+ FROM '.$this->calendarInstancesTableName.' WHERE id = ?');
+
+ foreach ($sharees as $sharee) {
+ if (\Sabre\DAV\Sharing\Plugin::ACCESS_NOACCESS === $sharee->access) {
+ // if access was set no NOACCESS, it means access for an
+ // existing sharee was removed.
+ $removeStmt->execute([$calendarId, $sharee->href]);
+ continue;
+ }
+
+ if (is_null($sharee->principal)) {
+ // If the server could not determine the principal automatically,
+ // we will mark the invite status as invalid.
+ $sharee->inviteStatus = \Sabre\DAV\Sharing\Plugin::INVITE_INVALID;
+ } else {
+ // Because sabre/dav does not yet have an invitation system,
+ // every invite is automatically accepted for now.
+ $sharee->inviteStatus = \Sabre\DAV\Sharing\Plugin::INVITE_ACCEPTED;
+ }
+
+ foreach ($currentInvites as $oldSharee) {
+ if ($oldSharee->href === $sharee->href) {
+ // This is an update
+ $sharee->properties = array_merge(
+ $oldSharee->properties,
+ $sharee->properties
+ );
+ $updateStmt->execute([
+ $sharee->access,
+ isset($sharee->properties['{DAV:}displayname']) ? $sharee->properties['{DAV:}displayname'] : null,
+ $sharee->inviteStatus ?: $oldSharee->inviteStatus,
+ $calendarId,
+ $sharee->href,
+ ]);
+ continue 2;
+ }
+ }
+ // If we got here, it means it was a new sharee
+ $insertStmt->execute([
+ $calendarId,
+ $sharee->principal,
+ $sharee->access,
+ \Sabre\DAV\UUIDUtil::getUUID(),
+ $sharee->href,
+ isset($sharee->properties['{DAV:}displayname']) ? $sharee->properties['{DAV:}displayname'] : null,
+ $sharee->inviteStatus ?: \Sabre\DAV\Sharing\Plugin::INVITE_NORESPONSE,
+ $instanceId,
+ ]);
+ }
+ }
+
+ /**
+ * Returns the list of people whom a calendar is shared with.
+ *
+ * Every item in the returned list must be a Sharee object with at
+ * least the following properties set:
+ * $href
+ * $shareAccess
+ * $inviteStatus
+ *
+ * and optionally:
+ * $properties
+ *
+ * @param mixed $calendarId
+ *
+ * @return \Sabre\DAV\Xml\Element\Sharee[]
+ */
+ public function getInvites($calendarId)
+ {
+ if (!is_array($calendarId)) {
+ throw new \InvalidArgumentException('The value passed to getInvites() is expected to be an array with a calendarId and an instanceId');
+ }
+ list($calendarId, $instanceId) = $calendarId;
+
+ $query = <<calendarInstancesTableName}
+WHERE
+ calendarid = ?
+SQL;
+
+ $stmt = $this->pdo->prepare($query);
+ $stmt->execute([$calendarId]);
+
+ $result = [];
+ while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $result[] = new Sharee([
+ 'href' => isset($row['share_href']) ? $row['share_href'] : \Sabre\HTTP\encodePath($row['principaluri']),
+ 'access' => (int) $row['access'],
+ /// Everyone is always immediately accepted, for now.
+ 'inviteStatus' => (int) $row['share_invitestatus'],
+ 'properties' => !empty($row['share_displayname'])
+ ? ['{DAV:}displayname' => $row['share_displayname']]
+ : [],
+ 'principal' => $row['principaluri'],
+ ]);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Publishes a calendar.
+ *
+ * @param mixed $calendarId
+ * @param bool $value
+ */
+ public function setPublishStatus($calendarId, $value)
+ {
+ throw new DAV\Exception\NotImplemented('Not implemented');
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php
new file mode 100644
index 0000000..69467e5
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php
@@ -0,0 +1,66 @@
+pdo = $pdo;
+ }
+
+ /**
+ * Returns a list of calendars for a principal.
+ *
+ * Every project is an array with the following keys:
+ * * id, a unique id that will be used by other functions to modify the
+ * calendar. This can be the same as the uri or a database key.
+ * * uri. This is just the 'base uri' or 'filename' of the calendar.
+ * * principaluri. The owner of the calendar. Almost always the same as
+ * principalUri passed to this method.
+ *
+ * Furthermore it can contain webdav properties in clark notation. A very
+ * common one is '{DAV:}displayname'.
+ *
+ * Many clients also require:
+ * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
+ * For this property, you can just return an instance of
+ * Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet.
+ *
+ * If you return {http://sabredav.org/ns}read-only and set the value to 1,
+ * ACL will automatically be put in read-only mode.
+ *
+ * @param string $principalUri
+ *
+ * @return array
+ */
+ public function getCalendarsForUser($principalUri)
+ {
+ // Making fields a comma-delimited list
+ $stmt = $this->pdo->prepare('SELECT id, uri FROM simple_calendars WHERE principaluri = ? ORDER BY id ASC');
+ $stmt->execute([$principalUri]);
+
+ $calendars = [];
+ while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $calendars[] = [
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ 'principaluri' => $principalUri,
+ ];
+ }
+
+ return $calendars;
+ }
+
+ /**
+ * Creates a new calendar for a principal.
+ *
+ * If the creation was a success, an id must be returned that can be used
+ * to reference this calendar in other methods, such as updateCalendar.
+ *
+ * @param string $principalUri
+ * @param string $calendarUri
+ *
+ * @return string
+ */
+ public function createCalendar($principalUri, $calendarUri, array $properties)
+ {
+ $stmt = $this->pdo->prepare('INSERT INTO simple_calendars (principaluri, uri) VALUES (?, ?)');
+ $stmt->execute([$principalUri, $calendarUri]);
+
+ return $this->pdo->lastInsertId();
+ }
+
+ /**
+ * Delete a calendar and all it's objects.
+ *
+ * @param string $calendarId
+ */
+ public function deleteCalendar($calendarId)
+ {
+ $stmt = $this->pdo->prepare('DELETE FROM simple_calendarobjects WHERE calendarid = ?');
+ $stmt->execute([$calendarId]);
+
+ $stmt = $this->pdo->prepare('DELETE FROM simple_calendars WHERE id = ?');
+ $stmt->execute([$calendarId]);
+ }
+
+ /**
+ * Returns all calendar objects within a calendar.
+ *
+ * Every item contains an array with the following keys:
+ * * calendardata - The iCalendar-compatible calendar data
+ * * uri - a unique key which will be used to construct the uri. This can
+ * be any arbitrary string, but making sure it ends with '.ics' is a
+ * good idea. This is only the basename, or filename, not the full
+ * path.
+ * * lastmodified - a timestamp of the last modification time
+ * * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
+ * ' "abcdef"')
+ * * size - The size of the calendar objects, in bytes.
+ * * component - optional, a string containing the type of object, such
+ * as 'vevent' or 'vtodo'. If specified, this will be used to populate
+ * the Content-Type header.
+ *
+ * Note that the etag is optional, but it's highly encouraged to return for
+ * speed reasons.
+ *
+ * The calendardata is also optional. If it's not returned
+ * 'getCalendarObject' will be called later, which *is* expected to return
+ * calendardata.
+ *
+ * If neither etag or size are specified, the calendardata will be
+ * used/fetched to determine these numbers. If both are specified the
+ * amount of times this is needed is reduced by a great degree.
+ *
+ * @param string $calendarId
+ *
+ * @return array
+ */
+ public function getCalendarObjects($calendarId)
+ {
+ $stmt = $this->pdo->prepare('SELECT id, uri, calendardata FROM simple_calendarobjects WHERE calendarid = ?');
+ $stmt->execute([$calendarId]);
+
+ $result = [];
+ foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
+ $result[] = [
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ 'etag' => '"'.md5($row['calendardata']).'"',
+ 'calendarid' => $calendarId,
+ 'size' => strlen($row['calendardata']),
+ 'calendardata' => $row['calendardata'],
+ ];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns information from a single calendar object, based on it's object
+ * uri.
+ *
+ * The object uri is only the basename, or filename and not a full path.
+ *
+ * The returned array must have the same keys as getCalendarObjects. The
+ * 'calendardata' object is required here though, while it's not required
+ * for getCalendarObjects.
+ *
+ * This method must return null if the object did not exist.
+ *
+ * @param string $calendarId
+ * @param string $objectUri
+ *
+ * @return array|null
+ */
+ public function getCalendarObject($calendarId, $objectUri)
+ {
+ $stmt = $this->pdo->prepare('SELECT id, uri, calendardata FROM simple_calendarobjects WHERE calendarid = ? AND uri = ?');
+ $stmt->execute([$calendarId, $objectUri]);
+ $row = $stmt->fetch(\PDO::FETCH_ASSOC);
+
+ if (!$row) {
+ return null;
+ }
+
+ return [
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ 'etag' => '"'.md5($row['calendardata']).'"',
+ 'calendarid' => $calendarId,
+ 'size' => strlen($row['calendardata']),
+ 'calendardata' => $row['calendardata'],
+ ];
+ }
+
+ /**
+ * Creates a new calendar object.
+ *
+ * The object uri is only the basename, or filename and not a full path.
+ *
+ * It is possible return an etag from this function, which will be used in
+ * the response to this PUT request. Note that the ETag must be surrounded
+ * by double-quotes.
+ *
+ * However, you should only really return this ETag if you don't mangle the
+ * calendar-data. If the result of a subsequent GET to this object is not
+ * the exact same as this request body, you should omit the ETag.
+ *
+ * @param mixed $calendarId
+ * @param string $objectUri
+ * @param string $calendarData
+ *
+ * @return string|null
+ */
+ public function createCalendarObject($calendarId, $objectUri, $calendarData)
+ {
+ $stmt = $this->pdo->prepare('INSERT INTO simple_calendarobjects (calendarid, uri, calendardata) VALUES (?,?,?)');
+ $stmt->execute([
+ $calendarId,
+ $objectUri,
+ $calendarData,
+ ]);
+
+ return '"'.md5($calendarData).'"';
+ }
+
+ /**
+ * Updates an existing calendarobject, based on it's uri.
+ *
+ * The object uri is only the basename, or filename and not a full path.
+ *
+ * It is possible return an etag from this function, which will be used in
+ * the response to this PUT request. Note that the ETag must be surrounded
+ * by double-quotes.
+ *
+ * However, you should only really return this ETag if you don't mangle the
+ * calendar-data. If the result of a subsequent GET to this object is not
+ * the exact same as this request body, you should omit the ETag.
+ *
+ * @param mixed $calendarId
+ * @param string $objectUri
+ * @param string $calendarData
+ *
+ * @return string|null
+ */
+ public function updateCalendarObject($calendarId, $objectUri, $calendarData)
+ {
+ $stmt = $this->pdo->prepare('UPDATE simple_calendarobjects SET calendardata = ? WHERE calendarid = ? AND uri = ?');
+ $stmt->execute([$calendarData, $calendarId, $objectUri]);
+
+ return '"'.md5($calendarData).'"';
+ }
+
+ /**
+ * Deletes an existing calendar object.
+ *
+ * The object uri is only the basename, or filename and not a full path.
+ *
+ * @param string $calendarId
+ * @param string $objectUri
+ */
+ public function deleteCalendarObject($calendarId, $objectUri)
+ {
+ $stmt = $this->pdo->prepare('DELETE FROM simple_calendarobjects WHERE calendarid = ? AND uri = ?');
+ $stmt->execute([$calendarId, $objectUri]);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php
new file mode 100644
index 0000000..7655c2e
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php
@@ -0,0 +1,89 @@
+ 'The current synctoken',
+ * 'added' => [
+ * 'new.txt',
+ * ],
+ * 'modified' => [
+ * 'modified.txt',
+ * ],
+ * 'deleted' => [
+ * 'foo.php.bak',
+ * 'old.txt'
+ * ]
+ * );
+ *
+ * The returned syncToken property should reflect the *current* syncToken
+ * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
+ * property This is * needed here too, to ensure the operation is atomic.
+ *
+ * If the $syncToken argument is specified as null, this is an initial
+ * sync, and all members should be reported.
+ *
+ * The modified property is an array of nodenames that have changed since
+ * the last token.
+ *
+ * The deleted property is an array with nodenames, that have been deleted
+ * from collection.
+ *
+ * The $syncLevel argument is basically the 'depth' of the report. If it's
+ * 1, you only have to report changes that happened only directly in
+ * immediate descendants. If it's 2, it should also include changes from
+ * the nodes below the child collections. (grandchildren)
+ *
+ * The $limit argument allows a client to specify how many results should
+ * be returned at most. If the limit is not specified, it should be treated
+ * as infinite.
+ *
+ * If the limit (infinite or not) is higher than you're willing to return,
+ * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
+ *
+ * If the syncToken is expired (due to data cleanup) or unknown, you must
+ * return null.
+ *
+ * The limit is 'suggestive'. You are free to ignore it.
+ *
+ * @param string $calendarId
+ * @param string $syncToken
+ * @param int $syncLevel
+ * @param int $limit
+ *
+ * @return array|null
+ */
+ public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null);
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Calendar.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Calendar.php
new file mode 100644
index 0000000..ba8c704
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Calendar.php
@@ -0,0 +1,460 @@
+caldavBackend = $caldavBackend;
+ $this->calendarInfo = $calendarInfo;
+ }
+
+ /**
+ * Returns the name of the calendar.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->calendarInfo['uri'];
+ }
+
+ /**
+ * Updates properties on this node.
+ *
+ * This method received a PropPatch object, which contains all the
+ * information about the update.
+ *
+ * To update specific properties, call the 'handle' method on this object.
+ * Read the PropPatch documentation for more information.
+ */
+ public function propPatch(PropPatch $propPatch)
+ {
+ return $this->caldavBackend->updateCalendar($this->calendarInfo['id'], $propPatch);
+ }
+
+ /**
+ * Returns the list of properties.
+ *
+ * @param array $requestedProperties
+ *
+ * @return array
+ */
+ public function getProperties($requestedProperties)
+ {
+ $response = [];
+
+ foreach ($this->calendarInfo as $propName => $propValue) {
+ if (!is_null($propValue) && '{' === $propName[0]) {
+ $response[$propName] = $this->calendarInfo[$propName];
+ }
+ }
+
+ return $response;
+ }
+
+ /**
+ * Returns a calendar object.
+ *
+ * The contained calendar objects are for example Events or Todo's.
+ *
+ * @param string $name
+ *
+ * @return \Sabre\CalDAV\ICalendarObject
+ */
+ public function getChild($name)
+ {
+ $obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name);
+
+ if (!$obj) {
+ throw new DAV\Exception\NotFound('Calendar object not found');
+ }
+ $obj['acl'] = $this->getChildACL();
+
+ return new CalendarObject($this->caldavBackend, $this->calendarInfo, $obj);
+ }
+
+ /**
+ * Returns the full list of calendar objects.
+ *
+ * @return array
+ */
+ public function getChildren()
+ {
+ $objs = $this->caldavBackend->getCalendarObjects($this->calendarInfo['id']);
+ $children = [];
+ foreach ($objs as $obj) {
+ $obj['acl'] = $this->getChildACL();
+ $children[] = new CalendarObject($this->caldavBackend, $this->calendarInfo, $obj);
+ }
+
+ return $children;
+ }
+
+ /**
+ * This method receives a list of paths in it's first argument.
+ * It must return an array with Node objects.
+ *
+ * If any children are not found, you do not have to return them.
+ *
+ * @param string[] $paths
+ *
+ * @return array
+ */
+ public function getMultipleChildren(array $paths)
+ {
+ $objs = $this->caldavBackend->getMultipleCalendarObjects($this->calendarInfo['id'], $paths);
+ $children = [];
+ foreach ($objs as $obj) {
+ $obj['acl'] = $this->getChildACL();
+ $children[] = new CalendarObject($this->caldavBackend, $this->calendarInfo, $obj);
+ }
+
+ return $children;
+ }
+
+ /**
+ * Checks if a child-node exists.
+ *
+ * @param string $name
+ *
+ * @return bool
+ */
+ public function childExists($name)
+ {
+ $obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name);
+ if (!$obj) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * Creates a new directory.
+ *
+ * We actually block this, as subdirectories are not allowed in calendars.
+ *
+ * @param string $name
+ */
+ public function createDirectory($name)
+ {
+ throw new DAV\Exception\MethodNotAllowed('Creating collections in calendar objects is not allowed');
+ }
+
+ /**
+ * Creates a new file.
+ *
+ * The contents of the new file must be a valid ICalendar string.
+ *
+ * @param string $name
+ * @param resource $data
+ *
+ * @return string|null
+ */
+ public function createFile($name, $data = null)
+ {
+ if (is_resource($data)) {
+ $data = stream_get_contents($data);
+ }
+
+ return $this->caldavBackend->createCalendarObject($this->calendarInfo['id'], $name, $data);
+ }
+
+ /**
+ * Deletes the calendar.
+ */
+ public function delete()
+ {
+ $this->caldavBackend->deleteCalendar($this->calendarInfo['id']);
+ }
+
+ /**
+ * Renames the calendar. Note that most calendars use the
+ * {DAV:}displayname to display a name to display a name.
+ *
+ * @param string $newName
+ */
+ public function setName($newName)
+ {
+ throw new DAV\Exception\MethodNotAllowed('Renaming calendars is not yet supported');
+ }
+
+ /**
+ * Returns the last modification date as a unix timestamp.
+ */
+ public function getLastModified()
+ {
+ return null;
+ }
+
+ /**
+ * Returns the owner principal.
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner()
+ {
+ return $this->calendarInfo['principaluri'];
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ public function getACL()
+ {
+ $acl = [
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getOwner().'/calendar-proxy-write',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getOwner().'/calendar-proxy-read',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{'.Plugin::NS_CALDAV.'}read-free-busy',
+ 'principal' => '{DAV:}authenticated',
+ 'protected' => true,
+ ],
+ ];
+ if (empty($this->calendarInfo['{http://sabredav.org/ns}read-only'])) {
+ $acl[] = [
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ];
+ $acl[] = [
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->getOwner().'/calendar-proxy-write',
+ 'protected' => true,
+ ];
+ }
+
+ return $acl;
+ }
+
+ /**
+ * This method returns the ACL's for calendar objects in this calendar.
+ * The result of this method automatically gets passed to the
+ * calendar-object nodes in the calendar.
+ *
+ * @return array
+ */
+ public function getChildACL()
+ {
+ $acl = [
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ],
+
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getOwner().'/calendar-proxy-write',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getOwner().'/calendar-proxy-read',
+ 'protected' => true,
+ ],
+ ];
+ if (empty($this->calendarInfo['{http://sabredav.org/ns}read-only'])) {
+ $acl[] = [
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ];
+ $acl[] = [
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->getOwner().'/calendar-proxy-write',
+ 'protected' => true,
+ ];
+ }
+
+ return $acl;
+ }
+
+ /**
+ * Performs a calendar-query on the contents of this calendar.
+ *
+ * The calendar-query is defined in RFC4791 : CalDAV. Using the
+ * calendar-query it is possible for a client to request a specific set of
+ * object, based on contents of iCalendar properties, date-ranges and
+ * iCalendar component types (VTODO, VEVENT).
+ *
+ * This method should just return a list of (relative) urls that match this
+ * query.
+ *
+ * The list of filters are specified as an array. The exact array is
+ * documented by Sabre\CalDAV\CalendarQueryParser.
+ *
+ * @return array
+ */
+ public function calendarQuery(array $filters)
+ {
+ return $this->caldavBackend->calendarQuery($this->calendarInfo['id'], $filters);
+ }
+
+ /**
+ * This method returns the current sync-token for this collection.
+ * This can be any string.
+ *
+ * If null is returned from this function, the plugin assumes there's no
+ * sync information available.
+ *
+ * @return string|null
+ */
+ public function getSyncToken()
+ {
+ if (
+ $this->caldavBackend instanceof Backend\SyncSupport &&
+ isset($this->calendarInfo['{DAV:}sync-token'])
+ ) {
+ return $this->calendarInfo['{DAV:}sync-token'];
+ }
+ if (
+ $this->caldavBackend instanceof Backend\SyncSupport &&
+ isset($this->calendarInfo['{http://sabredav.org/ns}sync-token'])
+ ) {
+ return $this->calendarInfo['{http://sabredav.org/ns}sync-token'];
+ }
+ }
+
+ /**
+ * The getChanges method returns all the changes that have happened, since
+ * the specified syncToken and the current collection.
+ *
+ * This function should return an array, such as the following:
+ *
+ * [
+ * 'syncToken' => 'The current synctoken',
+ * 'added' => [
+ * 'new.txt',
+ * ],
+ * 'modified' => [
+ * 'modified.txt',
+ * ],
+ * 'deleted' => [
+ * 'foo.php.bak',
+ * 'old.txt'
+ * ],
+ * 'result_truncated' : true
+ * ];
+ *
+ * The syncToken property should reflect the *current* syncToken of the
+ * collection, as reported getSyncToken(). This is needed here too, to
+ * ensure the operation is atomic.
+ *
+ * If the syncToken is specified as null, this is an initial sync, and all
+ * members should be reported.
+ *
+ * If result is truncated due to server limitation or limit by client,
+ * set result_truncated to true, otherwise set to false or do not add the key.
+ *
+ * The modified property is an array of nodenames that have changed since
+ * the last token.
+ *
+ * The deleted property is an array with nodenames, that have been deleted
+ * from collection.
+ *
+ * The second argument is basically the 'depth' of the report. If it's 1,
+ * you only have to report changes that happened only directly in immediate
+ * descendants. If it's 2, it should also include changes from the nodes
+ * below the child collections. (grandchildren)
+ *
+ * The third (optional) argument allows a client to specify how many
+ * results should be returned at most. If the limit is not specified, it
+ * should be treated as infinite.
+ *
+ * If the limit (infinite or not) is higher than you're willing to return,
+ * the result should be truncated to fit the limit.
+ * Note that even when the result is truncated, syncToken must be consistent
+ * with the truncated result, not the result before truncation.
+ * (See RFC6578 Section 3.6 for detail)
+ *
+ * If the syncToken is expired (due to data cleanup) or unknown, you must
+ * return null.
+ *
+ * The limit is 'suggestive'. You are free to ignore it.
+ * TODO: RFC6578 Section 3.7 says that the server must fail when the server
+ * cannot truncate according to the limit, so it may not be just suggestive.
+ *
+ * @param string $syncToken
+ * @param int $syncLevel
+ * @param int $limit
+ *
+ * @return array|null
+ */
+ public function getChanges($syncToken, $syncLevel, $limit = null)
+ {
+ if (!$this->caldavBackend instanceof Backend\SyncSupport) {
+ return null;
+ }
+
+ return $this->caldavBackend->getChangesForCalendar(
+ $this->calendarInfo['id'],
+ $syncToken,
+ $syncLevel,
+ $limit
+ );
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/CalendarHome.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/CalendarHome.php
new file mode 100644
index 0000000..49c54a3
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/CalendarHome.php
@@ -0,0 +1,356 @@
+caldavBackend = $caldavBackend;
+ $this->principalInfo = $principalInfo;
+ }
+
+ /**
+ * Returns the name of this object.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ list(, $name) = Uri\split($this->principalInfo['uri']);
+
+ return $name;
+ }
+
+ /**
+ * Updates the name of this object.
+ *
+ * @param string $name
+ */
+ public function setName($name)
+ {
+ throw new DAV\Exception\Forbidden();
+ }
+
+ /**
+ * Deletes this object.
+ */
+ public function delete()
+ {
+ throw new DAV\Exception\Forbidden();
+ }
+
+ /**
+ * Returns the last modification date.
+ *
+ * @return int
+ */
+ public function getLastModified()
+ {
+ return null;
+ }
+
+ /**
+ * Creates a new file under this object.
+ *
+ * This is currently not allowed
+ *
+ * @param string $name
+ * @param resource $data
+ */
+ public function createFile($name, $data = null)
+ {
+ throw new DAV\Exception\MethodNotAllowed('Creating new files in this collection is not supported');
+ }
+
+ /**
+ * Creates a new directory under this object.
+ *
+ * This is currently not allowed.
+ *
+ * @param string $filename
+ */
+ public function createDirectory($filename)
+ {
+ throw new DAV\Exception\MethodNotAllowed('Creating new collections in this collection is not supported');
+ }
+
+ /**
+ * Returns a single calendar, by name.
+ *
+ * @param string $name
+ *
+ * @return Calendar
+ */
+ public function getChild($name)
+ {
+ // Special nodes
+ if ('inbox' === $name && $this->caldavBackend instanceof Backend\SchedulingSupport) {
+ return new Schedule\Inbox($this->caldavBackend, $this->principalInfo['uri']);
+ }
+ if ('outbox' === $name && $this->caldavBackend instanceof Backend\SchedulingSupport) {
+ return new Schedule\Outbox($this->principalInfo['uri']);
+ }
+ if ('notifications' === $name && $this->caldavBackend instanceof Backend\NotificationSupport) {
+ return new Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']);
+ }
+
+ // Calendars
+ foreach ($this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']) as $calendar) {
+ if ($calendar['uri'] === $name) {
+ if ($this->caldavBackend instanceof Backend\SharingSupport) {
+ return new SharedCalendar($this->caldavBackend, $calendar);
+ } else {
+ return new Calendar($this->caldavBackend, $calendar);
+ }
+ }
+ }
+
+ if ($this->caldavBackend instanceof Backend\SubscriptionSupport) {
+ foreach ($this->caldavBackend->getSubscriptionsForUser($this->principalInfo['uri']) as $subscription) {
+ if ($subscription['uri'] === $name) {
+ return new Subscriptions\Subscription($this->caldavBackend, $subscription);
+ }
+ }
+ }
+
+ throw new NotFound('Node with name \''.$name.'\' could not be found');
+ }
+
+ /**
+ * Checks if a calendar exists.
+ *
+ * @param string $name
+ *
+ * @return bool
+ */
+ public function childExists($name)
+ {
+ try {
+ return (bool) $this->getChild($name);
+ } catch (NotFound $e) {
+ return false;
+ }
+ }
+
+ /**
+ * Returns a list of calendars.
+ *
+ * @return array
+ */
+ public function getChildren()
+ {
+ $calendars = $this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']);
+ $objs = [];
+ foreach ($calendars as $calendar) {
+ if ($this->caldavBackend instanceof Backend\SharingSupport) {
+ $objs[] = new SharedCalendar($this->caldavBackend, $calendar);
+ } else {
+ $objs[] = new Calendar($this->caldavBackend, $calendar);
+ }
+ }
+
+ if ($this->caldavBackend instanceof Backend\SchedulingSupport) {
+ $objs[] = new Schedule\Inbox($this->caldavBackend, $this->principalInfo['uri']);
+ $objs[] = new Schedule\Outbox($this->principalInfo['uri']);
+ }
+
+ // We're adding a notifications node, if it's supported by the backend.
+ if ($this->caldavBackend instanceof Backend\NotificationSupport) {
+ $objs[] = new Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']);
+ }
+
+ // If the backend supports subscriptions, we'll add those as well,
+ if ($this->caldavBackend instanceof Backend\SubscriptionSupport) {
+ foreach ($this->caldavBackend->getSubscriptionsForUser($this->principalInfo['uri']) as $subscription) {
+ $objs[] = new Subscriptions\Subscription($this->caldavBackend, $subscription);
+ }
+ }
+
+ return $objs;
+ }
+
+ /**
+ * Creates a new calendar or subscription.
+ *
+ * @param string $name
+ *
+ * @throws DAV\Exception\InvalidResourceType
+ */
+ public function createExtendedCollection($name, MkCol $mkCol)
+ {
+ $isCalendar = false;
+ $isSubscription = false;
+ foreach ($mkCol->getResourceType() as $rt) {
+ switch ($rt) {
+ case '{DAV:}collection':
+ case '{http://calendarserver.org/ns/}shared-owner':
+ // ignore
+ break;
+ case '{urn:ietf:params:xml:ns:caldav}calendar':
+ $isCalendar = true;
+ break;
+ case '{http://calendarserver.org/ns/}subscribed':
+ $isSubscription = true;
+ break;
+ default:
+ throw new DAV\Exception\InvalidResourceType('Unknown resourceType: '.$rt);
+ }
+ }
+
+ $properties = $mkCol->getRemainingValues();
+ $mkCol->setRemainingResultCode(201);
+
+ if ($isSubscription) {
+ if (!$this->caldavBackend instanceof Backend\SubscriptionSupport) {
+ throw new DAV\Exception\InvalidResourceType('This backend does not support subscriptions');
+ }
+ $this->caldavBackend->createSubscription($this->principalInfo['uri'], $name, $properties);
+ } elseif ($isCalendar) {
+ $this->caldavBackend->createCalendar($this->principalInfo['uri'], $name, $properties);
+ } else {
+ throw new DAV\Exception\InvalidResourceType('You can only create calendars and subscriptions in this collection');
+ }
+ }
+
+ /**
+ * Returns the owner of the calendar home.
+ *
+ * @return string
+ */
+ public function getOwner()
+ {
+ return $this->principalInfo['uri'];
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ public function getACL()
+ {
+ return [
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->principalInfo['uri'],
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->principalInfo['uri'],
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->principalInfo['uri'].'/calendar-proxy-write',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->principalInfo['uri'].'/calendar-proxy-write',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->principalInfo['uri'].'/calendar-proxy-read',
+ 'protected' => true,
+ ],
+ ];
+ }
+
+ /**
+ * This method is called when a user replied to a request to share.
+ *
+ * This method should return the url of the newly created calendar if the
+ * share was accepted.
+ *
+ * @param string $href The sharee who is replying (often a mailto: address)
+ * @param int $status One of the SharingPlugin::STATUS_* constants
+ * @param string $calendarUri The url to the calendar thats being shared
+ * @param string $inReplyTo The unique id this message is a response to
+ * @param string $summary A description of the reply
+ *
+ * @return string|null
+ */
+ public function shareReply($href, $status, $calendarUri, $inReplyTo, $summary = null)
+ {
+ if (!$this->caldavBackend instanceof Backend\SharingSupport) {
+ throw new DAV\Exception\NotImplemented('Sharing support is not implemented by this backend.');
+ }
+
+ return $this->caldavBackend->shareReply($href, $status, $calendarUri, $inReplyTo, $summary);
+ }
+
+ /**
+ * Searches through all of a users calendars and calendar objects to find
+ * an object with a specific UID.
+ *
+ * This method should return the path to this object, relative to the
+ * calendar home, so this path usually only contains two parts:
+ *
+ * calendarpath/objectpath.ics
+ *
+ * If the uid is not found, return null.
+ *
+ * This method should only consider * objects that the principal owns, so
+ * any calendars owned by other principals that also appear in this
+ * collection should be ignored.
+ *
+ * @param string $uid
+ *
+ * @return string|null
+ */
+ public function getCalendarObjectByUID($uid)
+ {
+ return $this->caldavBackend->getCalendarObjectByUID($this->principalInfo['uri'], $uid);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/CalendarObject.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/CalendarObject.php
new file mode 100644
index 0000000..671f4b5
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/CalendarObject.php
@@ -0,0 +1,223 @@
+caldavBackend = $caldavBackend;
+
+ if (!isset($objectData['uri'])) {
+ throw new \InvalidArgumentException('The objectData argument must contain an \'uri\' property');
+ }
+
+ $this->calendarInfo = $calendarInfo;
+ $this->objectData = $objectData;
+ }
+
+ /**
+ * Returns the uri for this object.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->objectData['uri'];
+ }
+
+ /**
+ * Returns the ICalendar-formatted object.
+ *
+ * @return string
+ */
+ public function get()
+ {
+ // Pre-populating the 'calendardata' is optional, if we don't have it
+ // already we fetch it from the backend.
+ if (!isset($this->objectData['calendardata'])) {
+ $this->objectData = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $this->objectData['uri']);
+ }
+
+ return $this->objectData['calendardata'];
+ }
+
+ /**
+ * Updates the ICalendar-formatted object.
+ *
+ * @param string|resource $calendarData
+ *
+ * @return string
+ */
+ public function put($calendarData)
+ {
+ if (is_resource($calendarData)) {
+ $calendarData = stream_get_contents($calendarData);
+ }
+ $etag = $this->caldavBackend->updateCalendarObject($this->calendarInfo['id'], $this->objectData['uri'], $calendarData);
+ $this->objectData['calendardata'] = $calendarData;
+ $this->objectData['etag'] = $etag;
+
+ return $etag;
+ }
+
+ /**
+ * Deletes the calendar object.
+ */
+ public function delete()
+ {
+ $this->caldavBackend->deleteCalendarObject($this->calendarInfo['id'], $this->objectData['uri']);
+ }
+
+ /**
+ * Returns the mime content-type.
+ *
+ * @return string
+ */
+ public function getContentType()
+ {
+ $mime = 'text/calendar; charset=utf-8';
+ if (isset($this->objectData['component']) && $this->objectData['component']) {
+ $mime .= '; component='.$this->objectData['component'];
+ }
+
+ return $mime;
+ }
+
+ /**
+ * Returns an ETag for this object.
+ *
+ * The ETag is an arbitrary string, but MUST be surrounded by double-quotes.
+ *
+ * @return string
+ */
+ public function getETag()
+ {
+ if (isset($this->objectData['etag'])) {
+ return $this->objectData['etag'];
+ } else {
+ return '"'.md5($this->get()).'"';
+ }
+ }
+
+ /**
+ * Returns the last modification date as a unix timestamp.
+ *
+ * @return int
+ */
+ public function getLastModified()
+ {
+ return $this->objectData['lastmodified'];
+ }
+
+ /**
+ * Returns the size of this object in bytes.
+ *
+ * @return int
+ */
+ public function getSize()
+ {
+ if (array_key_exists('size', $this->objectData)) {
+ return $this->objectData['size'];
+ } else {
+ return strlen($this->get());
+ }
+ }
+
+ /**
+ * Returns the owner principal.
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner()
+ {
+ return $this->calendarInfo['principaluri'];
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ public function getACL()
+ {
+ // An alternative acl may be specified in the object data.
+ if (isset($this->objectData['acl'])) {
+ return $this->objectData['acl'];
+ }
+
+ // The default ACL
+ return [
+ [
+ 'privilege' => '{DAV:}all',
+ 'principal' => $this->calendarInfo['principaluri'],
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}all',
+ 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-read',
+ 'protected' => true,
+ ],
+ ];
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/CalendarQueryValidator.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/CalendarQueryValidator.php
new file mode 100644
index 0000000..ee525da
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/CalendarQueryValidator.php
@@ -0,0 +1,354 @@
+name !== $filters['name']) {
+ return false;
+ }
+
+ return
+ $this->validateCompFilters($vObject, $filters['comp-filters']) &&
+ $this->validatePropFilters($vObject, $filters['prop-filters']);
+ }
+
+ /**
+ * This method checks the validity of comp-filters.
+ *
+ * A list of comp-filters needs to be specified. Also the parent of the
+ * component we're checking should be specified, not the component to check
+ * itself.
+ *
+ * @return bool
+ */
+ protected function validateCompFilters(VObject\Component $parent, array $filters)
+ {
+ foreach ($filters as $filter) {
+ $isDefined = isset($parent->{$filter['name']});
+
+ if ($filter['is-not-defined']) {
+ if ($isDefined) {
+ return false;
+ } else {
+ continue;
+ }
+ }
+ if (!$isDefined) {
+ return false;
+ }
+
+ if (array_key_exists('time-range', $filter) && $filter['time-range']) {
+ foreach ($parent->{$filter['name']} as $subComponent) {
+ $start = null;
+ $end = null;
+ if (array_key_exists('start', $filter['time-range'])) {
+ $start = $filter['time-range']['start'];
+ }
+ if (array_key_exists('end', $filter['time-range'])) {
+ $end = $filter['time-range']['end'];
+ }
+ if ($this->validateTimeRange($subComponent, $start, $end)) {
+ continue 2;
+ }
+ }
+
+ return false;
+ }
+
+ if (!$filter['comp-filters'] && !$filter['prop-filters']) {
+ continue;
+ }
+
+ // If there are sub-filters, we need to find at least one component
+ // for which the subfilters hold true.
+ foreach ($parent->{$filter['name']} as $subComponent) {
+ if (
+ $this->validateCompFilters($subComponent, $filter['comp-filters']) &&
+ $this->validatePropFilters($subComponent, $filter['prop-filters'])) {
+ // We had a match, so this comp-filter succeeds
+ continue 2;
+ }
+ }
+
+ // If we got here it means there were sub-comp-filters or
+ // sub-prop-filters and there was no match. This means this filter
+ // needs to return false.
+ return false;
+ }
+
+ // If we got here it means we got through all comp-filters alive so the
+ // filters were all true.
+ return true;
+ }
+
+ /**
+ * This method checks the validity of prop-filters.
+ *
+ * A list of prop-filters needs to be specified. Also the parent of the
+ * property we're checking should be specified, not the property to check
+ * itself.
+ *
+ * @return bool
+ */
+ protected function validatePropFilters(VObject\Component $parent, array $filters)
+ {
+ foreach ($filters as $filter) {
+ $isDefined = isset($parent->{$filter['name']});
+
+ if ($filter['is-not-defined']) {
+ if ($isDefined) {
+ return false;
+ } else {
+ continue;
+ }
+ }
+ if (!$isDefined) {
+ return false;
+ }
+
+ if (array_key_exists('time-range', $filter) && $filter['time-range']) {
+ foreach ($parent->{$filter['name']} as $subComponent) {
+ $start = null;
+ $end = null;
+ if (array_key_exists('start', $filter['time-range'])) {
+ $start = $filter['time-range']['start'];
+ }
+ if (array_key_exists('end', $filter['time-range'])) {
+ $end = $filter['time-range']['end'];
+ }
+ if ($this->validateTimeRange($subComponent, $start, $end)) {
+ continue 2;
+ }
+ }
+
+ return false;
+ }
+
+ if (!$filter['param-filters'] && !$filter['text-match']) {
+ continue;
+ }
+
+ // If there are sub-filters, we need to find at least one property
+ // for which the subfilters hold true.
+ foreach ($parent->{$filter['name']} as $subComponent) {
+ if (
+ $this->validateParamFilters($subComponent, $filter['param-filters']) &&
+ (!$filter['text-match'] || $this->validateTextMatch($subComponent, $filter['text-match']))
+ ) {
+ // We had a match, so this prop-filter succeeds
+ continue 2;
+ }
+ }
+
+ // If we got here it means there were sub-param-filters or
+ // text-match filters and there was no match. This means the
+ // filter needs to return false.
+ return false;
+ }
+
+ // If we got here it means we got through all prop-filters alive so the
+ // filters were all true.
+ return true;
+ }
+
+ /**
+ * This method checks the validity of param-filters.
+ *
+ * A list of param-filters needs to be specified. Also the parent of the
+ * parameter we're checking should be specified, not the parameter to check
+ * itself.
+ *
+ * @return bool
+ */
+ protected function validateParamFilters(VObject\Property $parent, array $filters)
+ {
+ foreach ($filters as $filter) {
+ $isDefined = isset($parent[$filter['name']]);
+
+ if ($filter['is-not-defined']) {
+ if ($isDefined) {
+ return false;
+ } else {
+ continue;
+ }
+ }
+ if (!$isDefined) {
+ return false;
+ }
+
+ if (!$filter['text-match']) {
+ continue;
+ }
+
+ // If there are sub-filters, we need to find at least one parameter
+ // for which the subfilters hold true.
+ foreach ($parent[$filter['name']]->getParts() as $paramPart) {
+ if ($this->validateTextMatch($paramPart, $filter['text-match'])) {
+ // We had a match, so this param-filter succeeds
+ continue 2;
+ }
+ }
+
+ // If we got here it means there was a text-match filter and there
+ // were no matches. This means the filter needs to return false.
+ return false;
+ }
+
+ // If we got here it means we got through all param-filters alive so the
+ // filters were all true.
+ return true;
+ }
+
+ /**
+ * This method checks the validity of a text-match.
+ *
+ * A single text-match should be specified as well as the specific property
+ * or parameter we need to validate.
+ *
+ * @param VObject\Node|string $check value to check against
+ *
+ * @return bool
+ */
+ protected function validateTextMatch($check, array $textMatch)
+ {
+ if ($check instanceof VObject\Node) {
+ $check = $check->getValue();
+ }
+
+ $isMatching = \Sabre\DAV\StringUtil::textMatch($check, $textMatch['value'], $textMatch['collation']);
+
+ return $textMatch['negate-condition'] xor $isMatching;
+ }
+
+ /**
+ * Validates if a component matches the given time range.
+ *
+ * This is all based on the rules specified in rfc4791, which are quite
+ * complex.
+ *
+ * @param DateTime $start
+ * @param DateTime $end
+ *
+ * @return bool
+ */
+ protected function validateTimeRange(VObject\Node $component, $start, $end)
+ {
+ if (is_null($start)) {
+ $start = new DateTime('1900-01-01');
+ }
+ if (is_null($end)) {
+ $end = new DateTime('3000-01-01');
+ }
+
+ switch ($component->name) {
+ case 'VEVENT':
+ case 'VTODO':
+ case 'VJOURNAL':
+ return $component->isInTimeRange($start, $end);
+
+ case 'VALARM':
+ // If the valarm is wrapped in a recurring event, we need to
+ // expand the recursions, and validate each.
+ //
+ // Our datamodel doesn't easily allow us to do this straight
+ // in the VALARM component code, so this is a hack, and an
+ // expensive one too.
+ if ('VEVENT' === $component->parent->name && $component->parent->RRULE) {
+ // Fire up the iterator!
+ $it = new VObject\Recur\EventIterator($component->parent->parent, (string) $component->parent->UID);
+ while ($it->valid()) {
+ $expandedEvent = $it->getEventObject();
+
+ // We need to check from these expanded alarms, which
+ // one is the first to trigger. Based on this, we can
+ // determine if we can 'give up' expanding events.
+ $firstAlarm = null;
+ if (null !== $expandedEvent->VALARM) {
+ foreach ($expandedEvent->VALARM as $expandedAlarm) {
+ $effectiveTrigger = $expandedAlarm->getEffectiveTriggerTime();
+ if ($expandedAlarm->isInTimeRange($start, $end)) {
+ return true;
+ }
+
+ if ('DATE-TIME' === (string) $expandedAlarm->TRIGGER['VALUE']) {
+ // This is an alarm with a non-relative trigger
+ // time, likely created by a buggy client. The
+ // implication is that every alarm in this
+ // recurring event trigger at the exact same
+ // time. It doesn't make sense to traverse
+ // further.
+ } else {
+ // We store the first alarm as a means to
+ // figure out when we can stop traversing.
+ if (!$firstAlarm || $effectiveTrigger < $firstAlarm) {
+ $firstAlarm = $effectiveTrigger;
+ }
+ }
+ }
+ }
+ if (is_null($firstAlarm)) {
+ // No alarm was found.
+ //
+ // Or technically: No alarm that will change for
+ // every instance of the recurrence was found,
+ // which means we can assume there was no match.
+ return false;
+ }
+ if ($firstAlarm > $end) {
+ return false;
+ }
+ $it->next();
+ }
+
+ return false;
+ } else {
+ return $component->isInTimeRange($start, $end);
+ }
+
+ // no break
+ case 'VFREEBUSY':
+ throw new \Sabre\DAV\Exception\NotImplemented('time-range filters are currently not supported on '.$component->name.' components');
+ case 'COMPLETED':
+ case 'CREATED':
+ case 'DTEND':
+ case 'DTSTAMP':
+ case 'DTSTART':
+ case 'DUE':
+ case 'LAST-MODIFIED':
+ return $start <= $component->getDateTime() && $end >= $component->getDateTime();
+
+ default:
+ throw new \Sabre\DAV\Exception\BadRequest('You cannot create a time-range filter on a '.$component->name.' component');
+ }
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/CalendarRoot.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/CalendarRoot.php
new file mode 100644
index 0000000..3038d21
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/CalendarRoot.php
@@ -0,0 +1,75 @@
+caldavBackend = $caldavBackend;
+ }
+
+ /**
+ * Returns the nodename.
+ *
+ * We're overriding this, because the default will be the 'principalPrefix',
+ * and we want it to be Sabre\CalDAV\Plugin::CALENDAR_ROOT
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return Plugin::CALENDAR_ROOT;
+ }
+
+ /**
+ * This method returns a node for a principal.
+ *
+ * The passed array contains principal information, and is guaranteed to
+ * at least contain a uri item. Other properties may or may not be
+ * supplied by the authentication backend.
+ *
+ * @return \Sabre\DAV\INode
+ */
+ public function getChildForPrincipal(array $principal)
+ {
+ return new CalendarHome($this->caldavBackend, $principal);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php
new file mode 100644
index 0000000..e94378a
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php
@@ -0,0 +1,31 @@
+ownerDocument;
+
+ $np = $doc->createElementNS(CalDAV\Plugin::NS_CALDAV, 'cal:supported-calendar-component');
+ $errorNode->appendChild($np);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/ICSExportPlugin.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/ICSExportPlugin.php
new file mode 100644
index 0000000..9171e36
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/ICSExportPlugin.php
@@ -0,0 +1,377 @@
+server = $server;
+ $server->on('method:GET', [$this, 'httpGet'], 90);
+ $server->on('browserButtonActions', function ($path, $node, &$actions) {
+ if ($node instanceof ICalendar) {
+ $actions .= '';
+ }
+ });
+ }
+
+ /**
+ * Intercepts GET requests on calendar urls ending with ?export.
+ *
+ * @throws BadRequest
+ * @throws DAV\Exception\NotFound
+ * @throws VObject\InvalidDataException
+ *
+ * @return bool
+ */
+ public function httpGet(RequestInterface $request, ResponseInterface $response)
+ {
+ $queryParams = $request->getQueryParameters();
+ if (!array_key_exists('export', $queryParams)) {
+ return;
+ }
+
+ $path = $request->getPath();
+
+ $node = $this->server->getProperties($path, [
+ '{DAV:}resourcetype',
+ '{DAV:}displayname',
+ '{http://sabredav.org/ns}sync-token',
+ '{DAV:}sync-token',
+ '{http://apple.com/ns/ical/}calendar-color',
+ ]);
+
+ if (!isset($node['{DAV:}resourcetype']) || !$node['{DAV:}resourcetype']->is('{'.Plugin::NS_CALDAV.'}calendar')) {
+ return;
+ }
+ // Marking the transactionType, for logging purposes.
+ $this->server->transactionType = 'get-calendar-export';
+
+ $properties = $node;
+
+ $start = null;
+ $end = null;
+ $expand = false;
+ $componentType = false;
+ if (isset($queryParams['start'])) {
+ if (!ctype_digit($queryParams['start'])) {
+ throw new BadRequest('The start= parameter must contain a unix timestamp');
+ }
+ $start = DateTime::createFromFormat('U', $queryParams['start']);
+ }
+ if (isset($queryParams['end'])) {
+ if (!ctype_digit($queryParams['end'])) {
+ throw new BadRequest('The end= parameter must contain a unix timestamp');
+ }
+ $end = DateTime::createFromFormat('U', $queryParams['end']);
+ }
+ if (isset($queryParams['expand']) && (bool) $queryParams['expand']) {
+ if (!$start || !$end) {
+ throw new BadRequest('If you\'d like to expand recurrences, you must specify both a start= and end= parameter.');
+ }
+ $expand = true;
+ $componentType = 'VEVENT';
+ }
+ if (isset($queryParams['componentType'])) {
+ if (!in_array($queryParams['componentType'], ['VEVENT', 'VTODO', 'VJOURNAL'])) {
+ throw new BadRequest('You are not allowed to search for components of type: '.$queryParams['componentType'].' here');
+ }
+ $componentType = $queryParams['componentType'];
+ }
+
+ $format = \Sabre\HTTP\negotiateContentType(
+ $request->getHeader('Accept'),
+ [
+ 'text/calendar',
+ 'application/calendar+json',
+ ]
+ );
+
+ if (isset($queryParams['accept'])) {
+ if ('application/calendar+json' === $queryParams['accept'] || 'jcal' === $queryParams['accept']) {
+ $format = 'application/calendar+json';
+ }
+ }
+ if (!$format) {
+ $format = 'text/calendar';
+ }
+
+ $this->generateResponse($path, $start, $end, $expand, $componentType, $format, $properties, $response);
+
+ // Returning false to break the event chain
+ return false;
+ }
+
+ /**
+ * This method is responsible for generating the actual, full response.
+ *
+ * @param string $path
+ * @param DateTime|null $start
+ * @param DateTime|null $end
+ * @param bool $expand
+ * @param string $componentType
+ * @param string $format
+ * @param array $properties
+ *
+ * @throws DAV\Exception\NotFound
+ * @throws VObject\InvalidDataException
+ */
+ protected function generateResponse($path, $start, $end, $expand, $componentType, $format, $properties, ResponseInterface $response)
+ {
+ $calDataProp = '{'.Plugin::NS_CALDAV.'}calendar-data';
+ $calendarNode = $this->server->tree->getNodeForPath($path);
+
+ $blobs = [];
+ if ($start || $end || $componentType) {
+ // If there was a start or end filter, we need to enlist
+ // calendarQuery for speed.
+ $queryResult = $calendarNode->calendarQuery([
+ 'name' => 'VCALENDAR',
+ 'comp-filters' => [
+ [
+ 'name' => $componentType,
+ 'comp-filters' => [],
+ 'prop-filters' => [],
+ 'is-not-defined' => false,
+ 'time-range' => [
+ 'start' => $start,
+ 'end' => $end,
+ ],
+ ],
+ ],
+ 'prop-filters' => [],
+ 'is-not-defined' => false,
+ 'time-range' => null,
+ ]);
+
+ // queryResult is just a list of base urls. We need to prefix the
+ // calendar path.
+ $queryResult = array_map(
+ function ($item) use ($path) {
+ return $path.'/'.$item;
+ },
+ $queryResult
+ );
+ $nodes = $this->server->getPropertiesForMultiplePaths($queryResult, [$calDataProp]);
+ unset($queryResult);
+ } else {
+ $nodes = $this->server->getPropertiesForPath($path, [$calDataProp], 1);
+ }
+
+ // Flattening the arrays
+ foreach ($nodes as $node) {
+ if (isset($node[200][$calDataProp])) {
+ $blobs[$node['href']] = $node[200][$calDataProp];
+ }
+ }
+ unset($nodes);
+
+ $mergedCalendar = $this->mergeObjects(
+ $properties,
+ $blobs
+ );
+
+ if ($expand) {
+ $calendarTimeZone = null;
+ // We're expanding, and for that we need to figure out the
+ // calendar's timezone.
+ $tzProp = '{'.Plugin::NS_CALDAV.'}calendar-timezone';
+ $tzResult = $this->server->getProperties($path, [$tzProp]);
+ if (isset($tzResult[$tzProp])) {
+ // This property contains a VCALENDAR with a single
+ // VTIMEZONE.
+ $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]);
+ $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
+ // Destroy circular references to PHP will GC the object.
+ $vtimezoneObj->destroy();
+ unset($vtimezoneObj);
+ } else {
+ // Defaulting to UTC.
+ $calendarTimeZone = new DateTimeZone('UTC');
+ }
+
+ $mergedCalendar = $mergedCalendar->expand($start, $end, $calendarTimeZone);
+ }
+
+ $filenameExtension = '.ics';
+
+ switch ($format) {
+ case 'text/calendar':
+ $mergedCalendar = $mergedCalendar->serialize();
+ $filenameExtension = '.ics';
+ break;
+ case 'application/calendar+json':
+ $mergedCalendar = json_encode($mergedCalendar->jsonSerialize());
+ $filenameExtension = '.json';
+ break;
+ }
+
+ $filename = preg_replace(
+ '/[^a-zA-Z0-9-_ ]/um',
+ '',
+ $calendarNode->getName()
+ );
+ $filename .= '-'.date('Y-m-d').$filenameExtension;
+
+ $response->setHeader('Content-Disposition', 'attachment; filename="'.$filename.'"');
+ $response->setHeader('Content-Type', $format);
+
+ $response->setStatus(200);
+ $response->setBody($mergedCalendar);
+ }
+
+ /**
+ * Merges all calendar objects, and builds one big iCalendar blob.
+ *
+ * @param array $properties Some CalDAV properties
+ *
+ * @return VObject\Component\VCalendar
+ */
+ public function mergeObjects(array $properties, array $inputObjects)
+ {
+ $calendar = new VObject\Component\VCalendar();
+ $calendar->VERSION = '2.0';
+ if (DAV\Server::$exposeVersion) {
+ $calendar->PRODID = '-//SabreDAV//SabreDAV '.DAV\Version::VERSION.'//EN';
+ } else {
+ $calendar->PRODID = '-//SabreDAV//SabreDAV//EN';
+ }
+ if (isset($properties['{DAV:}displayname'])) {
+ $calendar->{'X-WR-CALNAME'} = $properties['{DAV:}displayname'];
+ }
+ if (isset($properties['{http://apple.com/ns/ical/}calendar-color'])) {
+ $calendar->{'X-APPLE-CALENDAR-COLOR'} = $properties['{http://apple.com/ns/ical/}calendar-color'];
+ }
+
+ $collectedTimezones = [];
+
+ $timezones = [];
+ $objects = [];
+
+ foreach ($inputObjects as $href => $inputObject) {
+ $nodeComp = VObject\Reader::read($inputObject);
+
+ foreach ($nodeComp->children() as $child) {
+ switch ($child->name) {
+ case 'VEVENT':
+ case 'VTODO':
+ case 'VJOURNAL':
+ $objects[] = clone $child;
+ break;
+
+ // VTIMEZONE is special, because we need to filter out the duplicates
+ case 'VTIMEZONE':
+ // Naively just checking tzid.
+ if (in_array((string) $child->TZID, $collectedTimezones)) {
+ break;
+ }
+
+ $timezones[] = clone $child;
+ $collectedTimezones[] = $child->TZID;
+ break;
+ }
+ }
+ // Destroy circular references to PHP will GC the object.
+ $nodeComp->destroy();
+ unset($nodeComp);
+ }
+
+ foreach ($timezones as $tz) {
+ $calendar->add($tz);
+ }
+ foreach ($objects as $obj) {
+ $calendar->add($obj);
+ }
+
+ return $calendar;
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using \Sabre\DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ public function getPluginName()
+ {
+ return 'ics-export';
+ }
+
+ /**
+ * Returns a bunch of meta-data about the plugin.
+ *
+ * Providing this information is optional, and is mainly displayed by the
+ * Browser plugin.
+ *
+ * The description key in the returned array may contain html and will not
+ * be sanitized.
+ *
+ * @return array
+ */
+ public function getPluginInfo()
+ {
+ return [
+ 'name' => $this->getPluginName(),
+ 'description' => 'Adds the ability to export CalDAV calendars as a single iCalendar file.',
+ 'link' => 'http://sabre.io/dav/ics-export-plugin/',
+ ];
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/ICalendar.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/ICalendar.php
new file mode 100644
index 0000000..8636e0b
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/ICalendar.php
@@ -0,0 +1,20 @@
+caldavBackend = $caldavBackend;
+ $this->principalUri = $principalUri;
+ }
+
+ /**
+ * Returns all notifications for a principal.
+ *
+ * @return array
+ */
+ public function getChildren()
+ {
+ $children = [];
+ $notifications = $this->caldavBackend->getNotificationsForPrincipal($this->principalUri);
+
+ foreach ($notifications as $notification) {
+ $children[] = new Node(
+ $this->caldavBackend,
+ $this->principalUri,
+ $notification
+ );
+ }
+
+ return $children;
+ }
+
+ /**
+ * Returns the name of this object.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return 'notifications';
+ }
+
+ /**
+ * Returns the owner principal.
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner()
+ {
+ return $this->principalUri;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Notifications/ICollection.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Notifications/ICollection.php
new file mode 100644
index 0000000..b12fb39
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Notifications/ICollection.php
@@ -0,0 +1,25 @@
+caldavBackend = $caldavBackend;
+ $this->principalUri = $principalUri;
+ $this->notification = $notification;
+ }
+
+ /**
+ * Returns the path name for this notification.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->notification->getId().'.xml';
+ }
+
+ /**
+ * Returns the etag for the notification.
+ *
+ * The etag must be surrounded by literal double-quotes.
+ *
+ * @return string
+ */
+ public function getETag()
+ {
+ return $this->notification->getETag();
+ }
+
+ /**
+ * This method must return an xml element, using the
+ * Sabre\CalDAV\Xml\Notification\NotificationInterface classes.
+ *
+ * @return NotificationInterface
+ */
+ public function getNotificationType()
+ {
+ return $this->notification;
+ }
+
+ /**
+ * Deletes this notification.
+ */
+ public function delete()
+ {
+ $this->caldavBackend->deleteNotification($this->getOwner(), $this->notification);
+ }
+
+ /**
+ * Returns the owner principal.
+ *
+ * This must be an url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner()
+ {
+ return $this->principalUri;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Notifications/Plugin.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Notifications/Plugin.php
new file mode 100644
index 0000000..56b2fe9
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Notifications/Plugin.php
@@ -0,0 +1,161 @@
+server = $server;
+ $server->on('method:GET', [$this, 'httpGet'], 90);
+ $server->on('propFind', [$this, 'propFind']);
+
+ $server->xml->namespaceMap[self::NS_CALENDARSERVER] = 'cs';
+ $server->resourceTypeMapping['\\Sabre\\CalDAV\\Notifications\\ICollection'] = '{'.self::NS_CALENDARSERVER.'}notification';
+
+ array_push($server->protectedProperties,
+ '{'.self::NS_CALENDARSERVER.'}notification-URL',
+ '{'.self::NS_CALENDARSERVER.'}notificationtype'
+ );
+ }
+
+ /**
+ * PropFind.
+ */
+ public function propFind(PropFind $propFind, BaseINode $node)
+ {
+ $caldavPlugin = $this->server->getPlugin('caldav');
+
+ if ($node instanceof DAVACL\IPrincipal) {
+ $principalUrl = $node->getPrincipalUrl();
+
+ // notification-URL property
+ $propFind->handle('{'.self::NS_CALENDARSERVER.'}notification-URL', function () use ($principalUrl, $caldavPlugin) {
+ $notificationPath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl).'/notifications/';
+
+ return new DAV\Xml\Property\Href($notificationPath);
+ });
+ }
+
+ if ($node instanceof INode) {
+ $propFind->handle(
+ '{'.self::NS_CALENDARSERVER.'}notificationtype',
+ [$node, 'getNotificationType']
+ );
+ }
+ }
+
+ /**
+ * This event is triggered before the usual GET request handler.
+ *
+ * We use this to intercept GET calls to notification nodes, and return the
+ * proper response.
+ */
+ public function httpGet(RequestInterface $request, ResponseInterface $response)
+ {
+ $path = $request->getPath();
+
+ try {
+ $node = $this->server->tree->getNodeForPath($path);
+ } catch (DAV\Exception\NotFound $e) {
+ return;
+ }
+
+ if (!$node instanceof INode) {
+ return;
+ }
+
+ $writer = $this->server->xml->getWriter();
+ $writer->contextUri = $this->server->getBaseUri();
+ $writer->openMemory();
+ $writer->startDocument('1.0', 'UTF-8');
+ $writer->startElement('{http://calendarserver.org/ns/}notification');
+ $node->getNotificationType()->xmlSerializeFull($writer);
+ $writer->endElement();
+
+ $response->setHeader('Content-Type', 'application/xml');
+ $response->setHeader('ETag', $node->getETag());
+ $response->setStatus(200);
+ $response->setBody($writer->outputMemory());
+
+ // Return false to break the event chain.
+ return false;
+ }
+
+ /**
+ * Returns a bunch of meta-data about the plugin.
+ *
+ * Providing this information is optional, and is mainly displayed by the
+ * Browser plugin.
+ *
+ * The description key in the returned array may contain html and will not
+ * be sanitized.
+ *
+ * @return array
+ */
+ public function getPluginInfo()
+ {
+ return [
+ 'name' => $this->getPluginName(),
+ 'description' => 'Adds support for caldav-notifications, which is required to enable caldav-sharing.',
+ 'link' => 'http://sabre.io/dav/caldav-sharing/',
+ ];
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Plugin.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Plugin.php
new file mode 100644
index 0000000..ccb722f
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Plugin.php
@@ -0,0 +1,1011 @@
+server->tree->getNodeForPath($parent);
+
+ if ($node instanceof DAV\IExtendedCollection) {
+ try {
+ $node->getChild($name);
+ } catch (DAV\Exception\NotFound $e) {
+ return ['MKCALENDAR'];
+ }
+ }
+
+ return [];
+ }
+
+ /**
+ * Returns the path to a principal's calendar home.
+ *
+ * The return url must not end with a slash.
+ * This function should return null in case a principal did not have
+ * a calendar home.
+ *
+ * @param string $principalUrl
+ *
+ * @return string
+ */
+ public function getCalendarHomeForPrincipal($principalUrl)
+ {
+ // The default behavior for most sabre/dav servers is that there is a
+ // principals root node, which contains users directly under it.
+ //
+ // This function assumes that there are two components in a principal
+ // path. If there's more, we don't return a calendar home. This
+ // excludes things like the calendar-proxy-read principal (which it
+ // should).
+ $parts = explode('/', trim($principalUrl, '/'));
+ if (2 !== count($parts)) {
+ return;
+ }
+ if ('principals' !== $parts[0]) {
+ return;
+ }
+
+ return self::CALENDAR_ROOT.'/'.$parts[1];
+ }
+
+ /**
+ * Returns a list of features for the DAV: HTTP header.
+ *
+ * @return array
+ */
+ public function getFeatures()
+ {
+ return ['calendar-access', 'calendar-proxy'];
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ public function getPluginName()
+ {
+ return 'caldav';
+ }
+
+ /**
+ * Returns a list of reports this plugin supports.
+ *
+ * This will be used in the {DAV:}supported-report-set property.
+ * Note that you still need to subscribe to the 'report' event to actually
+ * implement them
+ *
+ * @param string $uri
+ *
+ * @return array
+ */
+ public function getSupportedReportSet($uri)
+ {
+ $node = $this->server->tree->getNodeForPath($uri);
+
+ $reports = [];
+ if ($node instanceof ICalendarObjectContainer || $node instanceof ICalendarObject) {
+ $reports[] = '{'.self::NS_CALDAV.'}calendar-multiget';
+ $reports[] = '{'.self::NS_CALDAV.'}calendar-query';
+ }
+ if ($node instanceof ICalendar) {
+ $reports[] = '{'.self::NS_CALDAV.'}free-busy-query';
+ }
+ // iCal has a bug where it assumes that sync support is enabled, only
+ // if we say we support it on the calendar-home, even though this is
+ // not actually the case.
+ if ($node instanceof CalendarHome && $this->server->getPlugin('sync')) {
+ $reports[] = '{DAV:}sync-collection';
+ }
+
+ return $reports;
+ }
+
+ /**
+ * Initializes the plugin.
+ */
+ public function initialize(DAV\Server $server)
+ {
+ $this->server = $server;
+
+ $server->on('method:MKCALENDAR', [$this, 'httpMkCalendar']);
+ $server->on('report', [$this, 'report']);
+ $server->on('propFind', [$this, 'propFind']);
+ $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']);
+ $server->on('beforeCreateFile', [$this, 'beforeCreateFile']);
+ $server->on('beforeWriteContent', [$this, 'beforeWriteContent']);
+ $server->on('afterMethod:GET', [$this, 'httpAfterGET']);
+ $server->on('getSupportedPrivilegeSet', [$this, 'getSupportedPrivilegeSet']);
+
+ $server->xml->namespaceMap[self::NS_CALDAV] = 'cal';
+ $server->xml->namespaceMap[self::NS_CALENDARSERVER] = 'cs';
+
+ $server->xml->elementMap['{'.self::NS_CALDAV.'}supported-calendar-component-set'] = 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet';
+ $server->xml->elementMap['{'.self::NS_CALDAV.'}calendar-query'] = 'Sabre\\CalDAV\\Xml\\Request\\CalendarQueryReport';
+ $server->xml->elementMap['{'.self::NS_CALDAV.'}calendar-multiget'] = 'Sabre\\CalDAV\\Xml\\Request\\CalendarMultiGetReport';
+ $server->xml->elementMap['{'.self::NS_CALDAV.'}free-busy-query'] = 'Sabre\\CalDAV\\Xml\\Request\\FreeBusyQueryReport';
+ $server->xml->elementMap['{'.self::NS_CALDAV.'}mkcalendar'] = 'Sabre\\CalDAV\\Xml\\Request\\MkCalendar';
+ $server->xml->elementMap['{'.self::NS_CALDAV.'}schedule-calendar-transp'] = 'Sabre\\CalDAV\\Xml\\Property\\ScheduleCalendarTransp';
+ $server->xml->elementMap['{'.self::NS_CALDAV.'}supported-calendar-component-set'] = 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet';
+
+ $server->resourceTypeMapping['\\Sabre\\CalDAV\\ICalendar'] = '{urn:ietf:params:xml:ns:caldav}calendar';
+
+ $server->resourceTypeMapping['\\Sabre\\CalDAV\\Principal\\IProxyRead'] = '{http://calendarserver.org/ns/}calendar-proxy-read';
+ $server->resourceTypeMapping['\\Sabre\\CalDAV\\Principal\\IProxyWrite'] = '{http://calendarserver.org/ns/}calendar-proxy-write';
+
+ array_push($server->protectedProperties,
+ '{'.self::NS_CALDAV.'}supported-calendar-component-set',
+ '{'.self::NS_CALDAV.'}supported-calendar-data',
+ '{'.self::NS_CALDAV.'}max-resource-size',
+ '{'.self::NS_CALDAV.'}min-date-time',
+ '{'.self::NS_CALDAV.'}max-date-time',
+ '{'.self::NS_CALDAV.'}max-instances',
+ '{'.self::NS_CALDAV.'}max-attendees-per-instance',
+ '{'.self::NS_CALDAV.'}calendar-home-set',
+ '{'.self::NS_CALDAV.'}supported-collation-set',
+ '{'.self::NS_CALDAV.'}calendar-data',
+
+ // CalendarServer extensions
+ '{'.self::NS_CALENDARSERVER.'}getctag',
+ '{'.self::NS_CALENDARSERVER.'}calendar-proxy-read-for',
+ '{'.self::NS_CALENDARSERVER.'}calendar-proxy-write-for'
+ );
+
+ if ($aclPlugin = $server->getPlugin('acl')) {
+ $aclPlugin->principalSearchPropertySet['{'.self::NS_CALDAV.'}calendar-user-address-set'] = 'Calendar address';
+ }
+ }
+
+ /**
+ * This functions handles REPORT requests specific to CalDAV.
+ *
+ * @param string $reportName
+ * @param mixed $report
+ * @param mixed $path
+ *
+ * @return bool|null
+ */
+ public function report($reportName, $report, $path)
+ {
+ switch ($reportName) {
+ case '{'.self::NS_CALDAV.'}calendar-multiget':
+ $this->server->transactionType = 'report-calendar-multiget';
+ $this->calendarMultiGetReport($report);
+
+ return false;
+ case '{'.self::NS_CALDAV.'}calendar-query':
+ $this->server->transactionType = 'report-calendar-query';
+ $this->calendarQueryReport($report);
+
+ return false;
+ case '{'.self::NS_CALDAV.'}free-busy-query':
+ $this->server->transactionType = 'report-free-busy-query';
+ $this->freeBusyQueryReport($report);
+
+ return false;
+ }
+ }
+
+ /**
+ * This function handles the MKCALENDAR HTTP method, which creates
+ * a new calendar.
+ *
+ * @return bool
+ */
+ public function httpMkCalendar(RequestInterface $request, ResponseInterface $response)
+ {
+ $body = $request->getBodyAsString();
+ $path = $request->getPath();
+
+ $properties = [];
+
+ if ($body) {
+ try {
+ $mkcalendar = $this->server->xml->expect(
+ '{urn:ietf:params:xml:ns:caldav}mkcalendar',
+ $body
+ );
+ } catch (\Sabre\Xml\ParseException $e) {
+ throw new BadRequest($e->getMessage(), 0, $e);
+ }
+ $properties = $mkcalendar->getProperties();
+ }
+
+ // iCal abuses MKCALENDAR since iCal 10.9.2 to create server-stored
+ // subscriptions. Before that it used MKCOL which was the correct way
+ // to do this.
+ //
+ // If the body had a {DAV:}resourcetype, it means we stumbled upon this
+ // request, and we simply use it instead of the pre-defined list.
+ if (isset($properties['{DAV:}resourcetype'])) {
+ $resourceType = $properties['{DAV:}resourcetype']->getValue();
+ } else {
+ $resourceType = ['{DAV:}collection', '{urn:ietf:params:xml:ns:caldav}calendar'];
+ }
+
+ $this->server->createCollection($path, new MkCol($resourceType, $properties));
+
+ $response->setStatus(201);
+ $response->setHeader('Content-Length', 0);
+
+ // This breaks the method chain.
+ return false;
+ }
+
+ /**
+ * PropFind.
+ *
+ * This method handler is invoked before any after properties for a
+ * resource are fetched. This allows us to add in any CalDAV specific
+ * properties.
+ */
+ public function propFind(DAV\PropFind $propFind, DAV\INode $node)
+ {
+ $ns = '{'.self::NS_CALDAV.'}';
+
+ if ($node instanceof ICalendarObjectContainer) {
+ $propFind->handle($ns.'max-resource-size', $this->maxResourceSize);
+ $propFind->handle($ns.'supported-calendar-data', function () {
+ return new Xml\Property\SupportedCalendarData();
+ });
+ $propFind->handle($ns.'supported-collation-set', function () {
+ return new Xml\Property\SupportedCollationSet();
+ });
+ }
+
+ if ($node instanceof DAVACL\IPrincipal) {
+ $principalUrl = $node->getPrincipalUrl();
+
+ $propFind->handle('{'.self::NS_CALDAV.'}calendar-home-set', function () use ($principalUrl) {
+ $calendarHomePath = $this->getCalendarHomeForPrincipal($principalUrl);
+ if (is_null($calendarHomePath)) {
+ return null;
+ }
+
+ return new LocalHref($calendarHomePath.'/');
+ });
+ // The calendar-user-address-set property is basically mapped to
+ // the {DAV:}alternate-URI-set property.
+ $propFind->handle('{'.self::NS_CALDAV.'}calendar-user-address-set', function () use ($node) {
+ $addresses = $node->getAlternateUriSet();
+ $addresses[] = $this->server->getBaseUri().$node->getPrincipalUrl().'/';
+
+ return new LocalHref($addresses);
+ });
+ // For some reason somebody thought it was a good idea to add
+ // another one of these properties. We're supporting it too.
+ $propFind->handle('{'.self::NS_CALENDARSERVER.'}email-address-set', function () use ($node) {
+ $addresses = $node->getAlternateUriSet();
+ $emails = [];
+ foreach ($addresses as $address) {
+ if ('mailto:' === substr($address, 0, 7)) {
+ $emails[] = substr($address, 7);
+ }
+ }
+
+ return new Xml\Property\EmailAddressSet($emails);
+ });
+
+ // These two properties are shortcuts for ical to easily find
+ // other principals this principal has access to.
+ $propRead = '{'.self::NS_CALENDARSERVER.'}calendar-proxy-read-for';
+ $propWrite = '{'.self::NS_CALENDARSERVER.'}calendar-proxy-write-for';
+
+ if (404 === $propFind->getStatus($propRead) || 404 === $propFind->getStatus($propWrite)) {
+ $aclPlugin = $this->server->getPlugin('acl');
+ $membership = $aclPlugin->getPrincipalMembership($propFind->getPath());
+ $readList = [];
+ $writeList = [];
+
+ foreach ($membership as $group) {
+ $groupNode = $this->server->tree->getNodeForPath($group);
+
+ $listItem = Uri\split($group)[0].'/';
+
+ // If the node is either ap proxy-read or proxy-write
+ // group, we grab the parent principal and add it to the
+ // list.
+ if ($groupNode instanceof Principal\IProxyRead) {
+ $readList[] = $listItem;
+ }
+ if ($groupNode instanceof Principal\IProxyWrite) {
+ $writeList[] = $listItem;
+ }
+ }
+
+ $propFind->set($propRead, new LocalHref($readList));
+ $propFind->set($propWrite, new LocalHref($writeList));
+ }
+ } // instanceof IPrincipal
+
+ if ($node instanceof ICalendarObject) {
+ // The calendar-data property is not supposed to be a 'real'
+ // property, but in large chunks of the spec it does act as such.
+ // Therefore we simply expose it as a property.
+ $propFind->handle('{'.self::NS_CALDAV.'}calendar-data', function () use ($node) {
+ $val = $node->get();
+ if (is_resource($val)) {
+ $val = stream_get_contents($val);
+ }
+
+ // Taking out \r to not screw up the xml output
+ return str_replace("\r", '', $val);
+ });
+ }
+ }
+
+ /**
+ * This function handles the calendar-multiget REPORT.
+ *
+ * This report is used by the client to fetch the content of a series
+ * of urls. Effectively avoiding a lot of redundant requests.
+ *
+ * @param CalendarMultiGetReport $report
+ */
+ public function calendarMultiGetReport($report)
+ {
+ $needsJson = 'application/calendar+json' === $report->contentType;
+
+ $timeZones = [];
+ $propertyList = [];
+
+ $paths = array_map(
+ [$this->server, 'calculateUri'],
+ $report->hrefs
+ );
+
+ foreach ($this->server->getPropertiesForMultiplePaths($paths, $report->properties) as $uri => $objProps) {
+ if (($needsJson || $report->expand) && isset($objProps[200]['{'.self::NS_CALDAV.'}calendar-data'])) {
+ $vObject = VObject\Reader::read($objProps[200]['{'.self::NS_CALDAV.'}calendar-data']);
+
+ if ($report->expand) {
+ // We're expanding, and for that we need to figure out the
+ // calendar's timezone.
+ list($calendarPath) = Uri\split($uri);
+ if (!isset($timeZones[$calendarPath])) {
+ // Checking the calendar-timezone property.
+ $tzProp = '{'.self::NS_CALDAV.'}calendar-timezone';
+ $tzResult = $this->server->getProperties($calendarPath, [$tzProp]);
+ if (isset($tzResult[$tzProp])) {
+ // This property contains a VCALENDAR with a single
+ // VTIMEZONE.
+ $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]);
+ $timeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
+ } else {
+ // Defaulting to UTC.
+ $timeZone = new DateTimeZone('UTC');
+ }
+ $timeZones[$calendarPath] = $timeZone;
+ }
+
+ $vObject = $vObject->expand($report->expand['start'], $report->expand['end'], $timeZones[$calendarPath]);
+ }
+ if ($needsJson) {
+ $objProps[200]['{'.self::NS_CALDAV.'}calendar-data'] = json_encode($vObject->jsonSerialize());
+ } else {
+ $objProps[200]['{'.self::NS_CALDAV.'}calendar-data'] = $vObject->serialize();
+ }
+ // Destroy circular references so PHP will garbage collect the
+ // object.
+ $vObject->destroy();
+ }
+
+ $propertyList[] = $objProps;
+ }
+
+ $prefer = $this->server->getHTTPPrefer();
+
+ $this->server->httpResponse->setStatus(207);
+ $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
+ $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer');
+ $this->server->httpResponse->setBody($this->server->generateMultiStatus($propertyList, 'minimal' === $prefer['return']));
+ }
+
+ /**
+ * This function handles the calendar-query REPORT.
+ *
+ * This report is used by clients to request calendar objects based on
+ * complex conditions.
+ *
+ * @param Xml\Request\CalendarQueryReport $report
+ */
+ public function calendarQueryReport($report)
+ {
+ $path = $this->server->getRequestUri();
+
+ $needsJson = 'application/calendar+json' === $report->contentType;
+
+ $node = $this->server->tree->getNodeForPath($this->server->getRequestUri());
+ $depth = $this->server->getHTTPDepth(0);
+
+ // The default result is an empty array
+ $result = [];
+
+ $calendarTimeZone = null;
+ if ($report->expand) {
+ // We're expanding, and for that we need to figure out the
+ // calendar's timezone.
+ $tzProp = '{'.self::NS_CALDAV.'}calendar-timezone';
+ $tzResult = $this->server->getProperties($path, [$tzProp]);
+ if (isset($tzResult[$tzProp])) {
+ // This property contains a VCALENDAR with a single
+ // VTIMEZONE.
+ $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]);
+ $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
+
+ // Destroy circular references so PHP will garbage collect the
+ // object.
+ $vtimezoneObj->destroy();
+ } else {
+ // Defaulting to UTC.
+ $calendarTimeZone = new DateTimeZone('UTC');
+ }
+ }
+
+ // The calendarobject was requested directly. In this case we handle
+ // this locally.
+ if (0 == $depth && $node instanceof ICalendarObject) {
+ $requestedCalendarData = true;
+ $requestedProperties = $report->properties;
+
+ if (!in_array('{urn:ietf:params:xml:ns:caldav}calendar-data', $requestedProperties)) {
+ // We always retrieve calendar-data, as we need it for filtering.
+ $requestedProperties[] = '{urn:ietf:params:xml:ns:caldav}calendar-data';
+
+ // If calendar-data wasn't explicitly requested, we need to remove
+ // it after processing.
+ $requestedCalendarData = false;
+ }
+
+ $properties = $this->server->getPropertiesForPath(
+ $path,
+ $requestedProperties,
+ 0
+ );
+
+ // This array should have only 1 element, the first calendar
+ // object.
+ $properties = current($properties);
+
+ // If there wasn't any calendar-data returned somehow, we ignore
+ // this.
+ if (isset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data'])) {
+ $validator = new CalendarQueryValidator();
+
+ $vObject = VObject\Reader::read($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
+ if ($validator->validate($vObject, $report->filters)) {
+ // If the client didn't require the calendar-data property,
+ // we won't give it back.
+ if (!$requestedCalendarData) {
+ unset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
+ } else {
+ if ($report->expand) {
+ $vObject = $vObject->expand($report->expand['start'], $report->expand['end'], $calendarTimeZone);
+ }
+ if ($needsJson) {
+ $properties[200]['{'.self::NS_CALDAV.'}calendar-data'] = json_encode($vObject->jsonSerialize());
+ } elseif ($report->expand) {
+ $properties[200]['{'.self::NS_CALDAV.'}calendar-data'] = $vObject->serialize();
+ }
+ }
+
+ $result = [$properties];
+ }
+ // Destroy circular references so PHP will garbage collect the
+ // object.
+ $vObject->destroy();
+ }
+ }
+
+ if ($node instanceof ICalendarObjectContainer && 0 === $depth) {
+ if (0 === strpos((string) $this->server->httpRequest->getHeader('User-Agent'), 'MSFT-')) {
+ // Microsoft clients incorrectly supplied depth as 0, when it actually
+ // should have set depth to 1. We're implementing a workaround here
+ // to deal with this.
+ //
+ // This targets at least the following clients:
+ // Windows 10
+ // Windows Phone 8, 10
+ $depth = 1;
+ } else {
+ throw new BadRequest('A calendar-query REPORT on a calendar with a Depth: 0 is undefined. Set Depth to 1');
+ }
+ }
+
+ // If we're dealing with a calendar, the calendar itself is responsible
+ // for the calendar-query.
+ if ($node instanceof ICalendarObjectContainer && 1 == $depth) {
+ $nodePaths = $node->calendarQuery($report->filters);
+
+ foreach ($nodePaths as $path) {
+ list($properties) =
+ $this->server->getPropertiesForPath($this->server->getRequestUri().'/'.$path, $report->properties);
+
+ if (($needsJson || $report->expand)) {
+ $vObject = VObject\Reader::read($properties[200]['{'.self::NS_CALDAV.'}calendar-data']);
+
+ if ($report->expand) {
+ $vObject = $vObject->expand($report->expand['start'], $report->expand['end'], $calendarTimeZone);
+ }
+
+ if ($needsJson) {
+ $properties[200]['{'.self::NS_CALDAV.'}calendar-data'] = json_encode($vObject->jsonSerialize());
+ } else {
+ $properties[200]['{'.self::NS_CALDAV.'}calendar-data'] = $vObject->serialize();
+ }
+
+ // Destroy circular references so PHP will garbage collect the
+ // object.
+ $vObject->destroy();
+ }
+ $result[] = $properties;
+ }
+ }
+
+ $prefer = $this->server->getHTTPPrefer();
+
+ $this->server->httpResponse->setStatus(207);
+ $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
+ $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer');
+ $this->server->httpResponse->setBody($this->server->generateMultiStatus($result, 'minimal' === $prefer['return']));
+ }
+
+ /**
+ * This method is responsible for parsing the request and generating the
+ * response for the CALDAV:free-busy-query REPORT.
+ */
+ protected function freeBusyQueryReport(Xml\Request\FreeBusyQueryReport $report)
+ {
+ $uri = $this->server->getRequestUri();
+
+ $acl = $this->server->getPlugin('acl');
+ if ($acl) {
+ $acl->checkPrivileges($uri, '{'.self::NS_CALDAV.'}read-free-busy');
+ }
+
+ $calendar = $this->server->tree->getNodeForPath($uri);
+ if (!$calendar instanceof ICalendar) {
+ throw new DAV\Exception\NotImplemented('The free-busy-query REPORT is only implemented on calendars');
+ }
+
+ $tzProp = '{'.self::NS_CALDAV.'}calendar-timezone';
+
+ // Figuring out the default timezone for the calendar, for floating
+ // times.
+ $calendarProps = $this->server->getProperties($uri, [$tzProp]);
+
+ if (isset($calendarProps[$tzProp])) {
+ $vtimezoneObj = VObject\Reader::read($calendarProps[$tzProp]);
+ $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
+ // Destroy circular references so PHP will garbage collect the object.
+ $vtimezoneObj->destroy();
+ } else {
+ $calendarTimeZone = new DateTimeZone('UTC');
+ }
+
+ // Doing a calendar-query first, to make sure we get the most
+ // performance.
+ $urls = $calendar->calendarQuery([
+ 'name' => 'VCALENDAR',
+ 'comp-filters' => [
+ [
+ 'name' => 'VEVENT',
+ 'comp-filters' => [],
+ 'prop-filters' => [],
+ 'is-not-defined' => false,
+ 'time-range' => [
+ 'start' => $report->start,
+ 'end' => $report->end,
+ ],
+ ],
+ ],
+ 'prop-filters' => [],
+ 'is-not-defined' => false,
+ 'time-range' => null,
+ ]);
+
+ $objects = array_map(function ($url) use ($calendar) {
+ $obj = $calendar->getChild($url)->get();
+
+ return $obj;
+ }, $urls);
+
+ $generator = new VObject\FreeBusyGenerator();
+ $generator->setObjects($objects);
+ $generator->setTimeRange($report->start, $report->end);
+ $generator->setTimeZone($calendarTimeZone);
+ $result = $generator->getResult();
+ $result = $result->serialize();
+
+ $this->server->httpResponse->setStatus(200);
+ $this->server->httpResponse->setHeader('Content-Type', 'text/calendar');
+ $this->server->httpResponse->setHeader('Content-Length', strlen($result));
+ $this->server->httpResponse->setBody($result);
+ }
+
+ /**
+ * This method is triggered before a file gets updated with new content.
+ *
+ * This plugin uses this method to ensure that CalDAV objects receive
+ * valid calendar data.
+ *
+ * @param string $path
+ * @param resource $data
+ * @param bool $modified should be set to true, if this event handler
+ * changed &$data
+ */
+ public function beforeWriteContent($path, DAV\IFile $node, &$data, &$modified)
+ {
+ if (!$node instanceof ICalendarObject) {
+ return;
+ }
+
+ // We're only interested in ICalendarObject nodes that are inside of a
+ // real calendar. This is to avoid triggering validation and scheduling
+ // for non-calendars (such as an inbox).
+ list($parent) = Uri\split($path);
+ $parentNode = $this->server->tree->getNodeForPath($parent);
+
+ if (!$parentNode instanceof ICalendar) {
+ return;
+ }
+
+ $this->validateICalendar(
+ $data,
+ $path,
+ $modified,
+ $this->server->httpRequest,
+ $this->server->httpResponse,
+ false
+ );
+ }
+
+ /**
+ * This method is triggered before a new file is created.
+ *
+ * This plugin uses this method to ensure that newly created calendar
+ * objects contain valid calendar data.
+ *
+ * @param string $path
+ * @param resource $data
+ * @param bool $modified should be set to true, if this event handler
+ * changed &$data
+ */
+ public function beforeCreateFile($path, &$data, DAV\ICollection $parentNode, &$modified)
+ {
+ if (!$parentNode instanceof ICalendar) {
+ return;
+ }
+
+ $this->validateICalendar(
+ $data,
+ $path,
+ $modified,
+ $this->server->httpRequest,
+ $this->server->httpResponse,
+ true
+ );
+ }
+
+ /**
+ * Checks if the submitted iCalendar data is in fact, valid.
+ *
+ * An exception is thrown if it's not.
+ *
+ * @param resource|string $data
+ * @param string $path
+ * @param bool $modified should be set to true, if this event handler
+ * changed &$data
+ * @param RequestInterface $request the http request
+ * @param ResponseInterface $response the http response
+ * @param bool $isNew is the item a new one, or an update
+ */
+ protected function validateICalendar(&$data, $path, &$modified, RequestInterface $request, ResponseInterface $response, $isNew)
+ {
+ // If it's a stream, we convert it to a string first.
+ if (is_resource($data)) {
+ $data = stream_get_contents($data);
+ }
+
+ $before = $data;
+
+ try {
+ // If the data starts with a [, we can reasonably assume we're dealing
+ // with a jCal object.
+ if ('[' === substr($data, 0, 1)) {
+ $vobj = VObject\Reader::readJson($data);
+
+ // Converting $data back to iCalendar, as that's what we
+ // technically support everywhere.
+ $data = $vobj->serialize();
+ $modified = true;
+ } else {
+ $vobj = VObject\Reader::read($data);
+ }
+ } catch (VObject\ParseException $e) {
+ throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid iCalendar 2.0 data. Parse error: '.$e->getMessage());
+ }
+
+ if ('VCALENDAR' !== $vobj->name) {
+ throw new DAV\Exception\UnsupportedMediaType('This collection can only support iCalendar objects.');
+ }
+
+ $sCCS = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
+
+ // Get the Supported Components for the target calendar
+ list($parentPath) = Uri\split($path);
+ $calendarProperties = $this->server->getProperties($parentPath, [$sCCS]);
+
+ if (isset($calendarProperties[$sCCS])) {
+ $supportedComponents = $calendarProperties[$sCCS]->getValue();
+ } else {
+ $supportedComponents = ['VJOURNAL', 'VTODO', 'VEVENT'];
+ }
+
+ $foundType = null;
+
+ foreach ($vobj->getComponents() as $component) {
+ switch ($component->name) {
+ case 'VTIMEZONE':
+ continue 2;
+ case 'VEVENT':
+ case 'VTODO':
+ case 'VJOURNAL':
+ $foundType = $component->name;
+ break;
+ }
+ }
+
+ if (!$foundType || !in_array($foundType, $supportedComponents)) {
+ throw new Exception\InvalidComponentType('iCalendar objects must at least have a component of type '.implode(', ', $supportedComponents));
+ }
+
+ $options = VObject\Node::PROFILE_CALDAV;
+ $prefer = $this->server->getHTTPPrefer();
+
+ if ('strict' !== $prefer['handling']) {
+ $options |= VObject\Node::REPAIR;
+ }
+
+ $messages = $vobj->validate($options);
+
+ $highestLevel = 0;
+ $warningMessage = null;
+
+ // $messages contains a list of problems with the vcard, along with
+ // their severity.
+ foreach ($messages as $message) {
+ if ($message['level'] > $highestLevel) {
+ // Recording the highest reported error level.
+ $highestLevel = $message['level'];
+ $warningMessage = $message['message'];
+ }
+ switch ($message['level']) {
+ case 1:
+ // Level 1 means that there was a problem, but it was repaired.
+ $modified = true;
+ break;
+ case 2:
+ // Level 2 means a warning, but not critical
+ break;
+ case 3:
+ // Level 3 means a critical error
+ throw new DAV\Exception\UnsupportedMediaType('Validation error in iCalendar: '.$message['message']);
+ }
+ }
+ if ($warningMessage) {
+ $response->setHeader(
+ 'X-Sabre-Ew-Gross',
+ 'iCalendar validation warning: '.$warningMessage
+ );
+ }
+
+ // We use an extra variable to allow event handles to tell us whether
+ // the object was modified or not.
+ //
+ // This helps us determine if we need to re-serialize the object.
+ $subModified = false;
+
+ $this->server->emit(
+ 'calendarObjectChange',
+ [
+ $request,
+ $response,
+ $vobj,
+ $parentPath,
+ &$subModified,
+ $isNew,
+ ]
+ );
+
+ if ($modified || $subModified) {
+ // An event handler told us that it modified the object.
+ $data = $vobj->serialize();
+
+ // Using md5 to figure out if there was an *actual* change.
+ if (!$modified && 0 !== strcmp($data, $before)) {
+ $modified = true;
+ }
+ }
+
+ // Destroy circular references so PHP will garbage collect the object.
+ $vobj->destroy();
+ }
+
+ /**
+ * This method is triggered whenever a subsystem requests the privileges
+ * that are supported on a particular node.
+ */
+ public function getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet)
+ {
+ if ($node instanceof ICalendar) {
+ $supportedPrivilegeSet['{DAV:}read']['aggregates']['{'.self::NS_CALDAV.'}read-free-busy'] = [
+ 'abstract' => false,
+ 'aggregates' => [],
+ ];
+ }
+ }
+
+ /**
+ * This method is used to generate HTML output for the
+ * DAV\Browser\Plugin. This allows us to generate an interface users
+ * can use to create new calendars.
+ *
+ * @param string $output
+ *
+ * @return bool
+ */
+ public function htmlActionsPanel(DAV\INode $node, &$output)
+ {
+ if (!$node instanceof CalendarHome) {
+ return;
+ }
+
+ $output .= '
+
';
+
+ return false;
+ }
+
+ /**
+ * This event is triggered after GET requests.
+ *
+ * This is used to transform data into jCal, if this was requested.
+ */
+ public function httpAfterGet(RequestInterface $request, ResponseInterface $response)
+ {
+ $contentType = $response->getHeader('Content-Type');
+ if (null === $contentType || false === strpos($contentType, 'text/calendar')) {
+ return;
+ }
+
+ $result = HTTP\negotiateContentType(
+ $request->getHeader('Accept'),
+ ['text/calendar', 'application/calendar+json']
+ );
+
+ if ('application/calendar+json' !== $result) {
+ // Do nothing
+ return;
+ }
+
+ // Transforming.
+ $vobj = VObject\Reader::read($response->getBody());
+
+ $jsonBody = json_encode($vobj->jsonSerialize());
+ $response->setBody($jsonBody);
+
+ // Destroy circular references so PHP will garbage collect the object.
+ $vobj->destroy();
+
+ $response->setHeader('Content-Type', 'application/calendar+json');
+ $response->setHeader('Content-Length', strlen($jsonBody));
+ }
+
+ /**
+ * Returns a bunch of meta-data about the plugin.
+ *
+ * Providing this information is optional, and is mainly displayed by the
+ * Browser plugin.
+ *
+ * The description key in the returned array may contain html and will not
+ * be sanitized.
+ *
+ * @return array
+ */
+ public function getPluginInfo()
+ {
+ return [
+ 'name' => $this->getPluginName(),
+ 'description' => 'Adds support for CalDAV (rfc4791)',
+ 'link' => 'http://sabre.io/dav/caldav/',
+ ];
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/Collection.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/Collection.php
new file mode 100644
index 0000000..6d02303
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/Collection.php
@@ -0,0 +1,32 @@
+principalBackend, $principalInfo);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/IProxyRead.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/IProxyRead.php
new file mode 100644
index 0000000..96e6991
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/IProxyRead.php
@@ -0,0 +1,21 @@
+principalInfo = $principalInfo;
+ $this->principalBackend = $principalBackend;
+ }
+
+ /**
+ * Returns this principals name.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return 'calendar-proxy-read';
+ }
+
+ /**
+ * Returns the last modification time.
+ */
+ public function getLastModified()
+ {
+ return null;
+ }
+
+ /**
+ * Deletes the current node.
+ *
+ * @throws DAV\Exception\Forbidden
+ */
+ public function delete()
+ {
+ throw new DAV\Exception\Forbidden('Permission denied to delete node');
+ }
+
+ /**
+ * Renames the node.
+ *
+ * @param string $name The new name
+ *
+ * @throws DAV\Exception\Forbidden
+ */
+ public function setName($name)
+ {
+ throw new DAV\Exception\Forbidden('Permission denied to rename file');
+ }
+
+ /**
+ * Returns a list of alternative urls for a principal.
+ *
+ * This can for example be an email address, or ldap url.
+ *
+ * @return array
+ */
+ public function getAlternateUriSet()
+ {
+ return [];
+ }
+
+ /**
+ * Returns the full principal url.
+ *
+ * @return string
+ */
+ public function getPrincipalUrl()
+ {
+ return $this->principalInfo['uri'].'/'.$this->getName();
+ }
+
+ /**
+ * Returns the list of group members.
+ *
+ * If this principal is a group, this function should return
+ * all member principal uri's for the group.
+ *
+ * @return array
+ */
+ public function getGroupMemberSet()
+ {
+ return $this->principalBackend->getGroupMemberSet($this->getPrincipalUrl());
+ }
+
+ /**
+ * Returns the list of groups this principal is member of.
+ *
+ * If this principal is a member of a (list of) groups, this function
+ * should return a list of principal uri's for it's members.
+ *
+ * @return array
+ */
+ public function getGroupMembership()
+ {
+ return $this->principalBackend->getGroupMembership($this->getPrincipalUrl());
+ }
+
+ /**
+ * Sets a list of group members.
+ *
+ * If this principal is a group, this method sets all the group members.
+ * The list of members is always overwritten, never appended to.
+ *
+ * This method should throw an exception if the members could not be set.
+ */
+ public function setGroupMemberSet(array $principals)
+ {
+ $this->principalBackend->setGroupMemberSet($this->getPrincipalUrl(), $principals);
+ }
+
+ /**
+ * Returns the displayname.
+ *
+ * This should be a human readable name for the principal.
+ * If none is available, return the nodename.
+ *
+ * @return string
+ */
+ public function getDisplayName()
+ {
+ return $this->getName();
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/ProxyWrite.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/ProxyWrite.php
new file mode 100644
index 0000000..2d1ce7c
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/ProxyWrite.php
@@ -0,0 +1,161 @@
+principalInfo = $principalInfo;
+ $this->principalBackend = $principalBackend;
+ }
+
+ /**
+ * Returns this principals name.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return 'calendar-proxy-write';
+ }
+
+ /**
+ * Returns the last modification time.
+ */
+ public function getLastModified()
+ {
+ return null;
+ }
+
+ /**
+ * Deletes the current node.
+ *
+ * @throws DAV\Exception\Forbidden
+ */
+ public function delete()
+ {
+ throw new DAV\Exception\Forbidden('Permission denied to delete node');
+ }
+
+ /**
+ * Renames the node.
+ *
+ * @param string $name The new name
+ *
+ * @throws DAV\Exception\Forbidden
+ */
+ public function setName($name)
+ {
+ throw new DAV\Exception\Forbidden('Permission denied to rename file');
+ }
+
+ /**
+ * Returns a list of alternative urls for a principal.
+ *
+ * This can for example be an email address, or ldap url.
+ *
+ * @return array
+ */
+ public function getAlternateUriSet()
+ {
+ return [];
+ }
+
+ /**
+ * Returns the full principal url.
+ *
+ * @return string
+ */
+ public function getPrincipalUrl()
+ {
+ return $this->principalInfo['uri'].'/'.$this->getName();
+ }
+
+ /**
+ * Returns the list of group members.
+ *
+ * If this principal is a group, this function should return
+ * all member principal uri's for the group.
+ *
+ * @return array
+ */
+ public function getGroupMemberSet()
+ {
+ return $this->principalBackend->getGroupMemberSet($this->getPrincipalUrl());
+ }
+
+ /**
+ * Returns the list of groups this principal is member of.
+ *
+ * If this principal is a member of a (list of) groups, this function
+ * should return a list of principal uri's for it's members.
+ *
+ * @return array
+ */
+ public function getGroupMembership()
+ {
+ return $this->principalBackend->getGroupMembership($this->getPrincipalUrl());
+ }
+
+ /**
+ * Sets a list of group members.
+ *
+ * If this principal is a group, this method sets all the group members.
+ * The list of members is always overwritten, never appended to.
+ *
+ * This method should throw an exception if the members could not be set.
+ */
+ public function setGroupMemberSet(array $principals)
+ {
+ $this->principalBackend->setGroupMemberSet($this->getPrincipalUrl(), $principals);
+ }
+
+ /**
+ * Returns the displayname.
+ *
+ * This should be a human readable name for the principal.
+ * If none is available, return the nodename.
+ *
+ * @return string
+ */
+ public function getDisplayName()
+ {
+ return $this->getName();
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/User.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/User.php
new file mode 100644
index 0000000..88bf4b4
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/User.php
@@ -0,0 +1,136 @@
+principalBackend->getPrincipalByPath($this->getPrincipalURL().'/'.$name);
+ if (!$principal) {
+ throw new DAV\Exception\NotFound('Node with name '.$name.' was not found');
+ }
+ if ('calendar-proxy-read' === $name) {
+ return new ProxyRead($this->principalBackend, $this->principalProperties);
+ }
+
+ if ('calendar-proxy-write' === $name) {
+ return new ProxyWrite($this->principalBackend, $this->principalProperties);
+ }
+
+ throw new DAV\Exception\NotFound('Node with name '.$name.' was not found');
+ }
+
+ /**
+ * Returns an array with all the child nodes.
+ *
+ * @return DAV\INode[]
+ */
+ public function getChildren()
+ {
+ $r = [];
+ if ($this->principalBackend->getPrincipalByPath($this->getPrincipalURL().'/calendar-proxy-read')) {
+ $r[] = new ProxyRead($this->principalBackend, $this->principalProperties);
+ }
+ if ($this->principalBackend->getPrincipalByPath($this->getPrincipalURL().'/calendar-proxy-write')) {
+ $r[] = new ProxyWrite($this->principalBackend, $this->principalProperties);
+ }
+
+ return $r;
+ }
+
+ /**
+ * Returns whether or not the child node exists.
+ *
+ * @param string $name
+ *
+ * @return bool
+ */
+ public function childExists($name)
+ {
+ try {
+ $this->getChild($name);
+
+ return true;
+ } catch (DAV\Exception\NotFound $e) {
+ return false;
+ }
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ public function getACL()
+ {
+ $acl = parent::getACL();
+ $acl[] = [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->principalProperties['uri'].'/calendar-proxy-read',
+ 'protected' => true,
+ ];
+ $acl[] = [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->principalProperties['uri'].'/calendar-proxy-write',
+ 'protected' => true,
+ ];
+
+ return $acl;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/IInbox.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/IInbox.php
new file mode 100644
index 0000000..64a94be
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/IInbox.php
@@ -0,0 +1,17 @@
+senderEmail = $senderEmail;
+ }
+
+ /*
+ * This initializes the plugin.
+ *
+ * This function is called by Sabre\DAV\Server, after
+ * addPlugin is called.
+ *
+ * This method should set up the required event subscriptions.
+ *
+ * @param DAV\Server $server
+ * @return void
+ */
+ public function initialize(DAV\Server $server)
+ {
+ $server->on('schedule', [$this, 'schedule'], 120);
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using \Sabre\DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ public function getPluginName()
+ {
+ return 'imip';
+ }
+
+ /**
+ * Event handler for the 'schedule' event.
+ */
+ public function schedule(ITip\Message $iTipMessage)
+ {
+ // Not sending any emails if the system considers the update
+ // insignificant.
+ if (!$iTipMessage->significantChange) {
+ if (!$iTipMessage->scheduleStatus) {
+ $iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email';
+ }
+
+ return;
+ }
+
+ $summary = $iTipMessage->message->VEVENT->SUMMARY;
+
+ if ('mailto' !== parse_url($iTipMessage->sender, PHP_URL_SCHEME)) {
+ return;
+ }
+
+ if ('mailto' !== parse_url($iTipMessage->recipient, PHP_URL_SCHEME)) {
+ return;
+ }
+
+ $sender = substr($iTipMessage->sender, 7);
+ $recipient = substr($iTipMessage->recipient, 7);
+
+ if ($iTipMessage->senderName) {
+ $sender = $iTipMessage->senderName.' <'.$sender.'>';
+ }
+ if ($iTipMessage->recipientName && $iTipMessage->recipientName != $recipient) {
+ $recipient = $iTipMessage->recipientName.' <'.$recipient.'>';
+ }
+
+ $subject = 'SabreDAV iTIP message';
+ switch (strtoupper($iTipMessage->method)) {
+ case 'REPLY':
+ $subject = 'Re: '.$summary;
+ break;
+ case 'REQUEST':
+ $subject = 'Invitation: '.$summary;
+ break;
+ case 'CANCEL':
+ $subject = 'Cancelled: '.$summary;
+ break;
+ }
+
+ $headers = [
+ 'Reply-To: '.$sender,
+ 'From: '.$iTipMessage->senderName.' <'.$this->senderEmail.'>',
+ 'MIME-Version: 1.0',
+ 'Content-Type: text/calendar; charset=UTF-8; method='.$iTipMessage->method,
+ ];
+ if (DAV\Server::$exposeVersion) {
+ $headers[] = 'X-Sabre-Version: '.DAV\Version::VERSION;
+ }
+ $this->mail(
+ $recipient,
+ $subject,
+ $iTipMessage->message->serialize(),
+ $headers
+ );
+ $iTipMessage->scheduleStatus = '1.1; Scheduling message is sent via iMip';
+ }
+
+ // @codeCoverageIgnoreStart
+ // This is deemed untestable in a reasonable manner
+
+ /**
+ * This function is responsible for sending the actual email.
+ *
+ * @param string $to Recipient email address
+ * @param string $subject Subject of the email
+ * @param string $body iCalendar body
+ * @param array $headers List of headers
+ */
+ protected function mail($to, $subject, $body, array $headers)
+ {
+ mail($to, $subject, $body, implode("\r\n", $headers));
+ }
+
+ // @codeCoverageIgnoreEnd
+
+ /**
+ * Returns a bunch of meta-data about the plugin.
+ *
+ * Providing this information is optional, and is mainly displayed by the
+ * Browser plugin.
+ *
+ * The description key in the returned array may contain html and will not
+ * be sanitized.
+ *
+ * @return array
+ */
+ public function getPluginInfo()
+ {
+ return [
+ 'name' => $this->getPluginName(),
+ 'description' => 'Email delivery (rfc6047) for CalDAV scheduling',
+ 'link' => 'http://sabre.io/dav/scheduling/',
+ ];
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/IOutbox.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/IOutbox.php
new file mode 100644
index 0000000..384b503
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/IOutbox.php
@@ -0,0 +1,17 @@
+caldavBackend = $caldavBackend;
+ $this->principalUri = $principalUri;
+ }
+
+ /**
+ * Returns the name of the node.
+ *
+ * This is used to generate the url.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return 'inbox';
+ }
+
+ /**
+ * Returns an array with all the child nodes.
+ *
+ * @return \Sabre\DAV\INode[]
+ */
+ public function getChildren()
+ {
+ $objs = $this->caldavBackend->getSchedulingObjects($this->principalUri);
+ $children = [];
+ foreach ($objs as $obj) {
+ //$obj['acl'] = $this->getACL();
+ $obj['principaluri'] = $this->principalUri;
+ $children[] = new SchedulingObject($this->caldavBackend, $obj);
+ }
+
+ return $children;
+ }
+
+ /**
+ * Creates a new file in the directory.
+ *
+ * Data will either be supplied as a stream resource, or in certain cases
+ * as a string. Keep in mind that you may have to support either.
+ *
+ * After successful creation of the file, you may choose to return the ETag
+ * of the new file here.
+ *
+ * The returned ETag must be surrounded by double-quotes (The quotes should
+ * be part of the actual string).
+ *
+ * If you cannot accurately determine the ETag, you should not return it.
+ * If you don't store the file exactly as-is (you're transforming it
+ * somehow) you should also not return an ETag.
+ *
+ * This means that if a subsequent GET to this new file does not exactly
+ * return the same contents of what was submitted here, you are strongly
+ * recommended to omit the ETag.
+ *
+ * @param string $name Name of the file
+ * @param resource|string $data Initial payload
+ *
+ * @return string|null
+ */
+ public function createFile($name, $data = null)
+ {
+ $this->caldavBackend->createSchedulingObject($this->principalUri, $name, $data);
+ }
+
+ /**
+ * Returns the owner principal.
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner()
+ {
+ return $this->principalUri;
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ public function getACL()
+ {
+ return [
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => '{DAV:}authenticated',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}write-properties',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}unbind',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}unbind',
+ 'principal' => $this->getOwner().'/calendar-proxy-write',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-deliver',
+ 'principal' => '{DAV:}authenticated',
+ 'protected' => true,
+ ],
+ ];
+ }
+
+ /**
+ * Performs a calendar-query on the contents of this calendar.
+ *
+ * The calendar-query is defined in RFC4791 : CalDAV. Using the
+ * calendar-query it is possible for a client to request a specific set of
+ * object, based on contents of iCalendar properties, date-ranges and
+ * iCalendar component types (VTODO, VEVENT).
+ *
+ * This method should just return a list of (relative) urls that match this
+ * query.
+ *
+ * The list of filters are specified as an array. The exact array is
+ * documented by \Sabre\CalDAV\CalendarQueryParser.
+ *
+ * @return array
+ */
+ public function calendarQuery(array $filters)
+ {
+ $result = [];
+ $validator = new CalDAV\CalendarQueryValidator();
+
+ $objects = $this->caldavBackend->getSchedulingObjects($this->principalUri);
+ foreach ($objects as $object) {
+ $vObject = VObject\Reader::read($object['calendardata']);
+ if ($validator->validate($vObject, $filters)) {
+ $result[] = $object['uri'];
+ }
+
+ // Destroy circular references to PHP will GC the object.
+ $vObject->destroy();
+ }
+
+ return $result;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/Outbox.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/Outbox.php
new file mode 100644
index 0000000..1442c4c
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/Outbox.php
@@ -0,0 +1,119 @@
+principalUri = $principalUri;
+ }
+
+ /**
+ * Returns the name of the node.
+ *
+ * This is used to generate the url.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return 'outbox';
+ }
+
+ /**
+ * Returns an array with all the child nodes.
+ *
+ * @return \Sabre\DAV\INode[]
+ */
+ public function getChildren()
+ {
+ return [];
+ }
+
+ /**
+ * Returns the owner principal.
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner()
+ {
+ return $this->principalUri;
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ public function getACL()
+ {
+ return [
+ [
+ 'privilege' => '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-send',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-send',
+ 'principal' => $this->getOwner().'/calendar-proxy-write',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getOwner().'/calendar-proxy-read',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getOwner().'/calendar-proxy-write',
+ 'protected' => true,
+ ],
+ ];
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/Plugin.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/Plugin.php
new file mode 100644
index 0000000..5bca56d
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/Plugin.php
@@ -0,0 +1,1006 @@
+server = $server;
+ $server->on('method:POST', [$this, 'httpPost']);
+ $server->on('propFind', [$this, 'propFind']);
+ $server->on('propPatch', [$this, 'propPatch']);
+ $server->on('calendarObjectChange', [$this, 'calendarObjectChange']);
+ $server->on('beforeUnbind', [$this, 'beforeUnbind']);
+ $server->on('schedule', [$this, 'scheduleLocalDelivery']);
+ $server->on('getSupportedPrivilegeSet', [$this, 'getSupportedPrivilegeSet']);
+
+ $ns = '{'.self::NS_CALDAV.'}';
+
+ /*
+ * This information ensures that the {DAV:}resourcetype property has
+ * the correct values.
+ */
+ $server->resourceTypeMapping['\\Sabre\\CalDAV\\Schedule\\IOutbox'] = $ns.'schedule-outbox';
+ $server->resourceTypeMapping['\\Sabre\\CalDAV\\Schedule\\IInbox'] = $ns.'schedule-inbox';
+
+ /*
+ * Properties we protect are made read-only by the server.
+ */
+ array_push($server->protectedProperties,
+ $ns.'schedule-inbox-URL',
+ $ns.'schedule-outbox-URL',
+ $ns.'calendar-user-address-set',
+ $ns.'calendar-user-type',
+ $ns.'schedule-default-calendar-URL'
+ );
+ }
+
+ /**
+ * Use this method to tell the server this plugin defines additional
+ * HTTP methods.
+ *
+ * This method is passed a uri. It should only return HTTP methods that are
+ * available for the specified uri.
+ *
+ * @param string $uri
+ *
+ * @return array
+ */
+ public function getHTTPMethods($uri)
+ {
+ try {
+ $node = $this->server->tree->getNodeForPath($uri);
+ } catch (NotFound $e) {
+ return [];
+ }
+
+ if ($node instanceof IOutbox) {
+ return ['POST'];
+ }
+
+ return [];
+ }
+
+ /**
+ * This method handles POST request for the outbox.
+ *
+ * @return bool
+ */
+ public function httpPost(RequestInterface $request, ResponseInterface $response)
+ {
+ // Checking if this is a text/calendar content type
+ $contentType = $request->getHeader('Content-Type');
+ if (!$contentType || 0 !== strpos($contentType, 'text/calendar')) {
+ return;
+ }
+
+ $path = $request->getPath();
+
+ // Checking if we're talking to an outbox
+ try {
+ $node = $this->server->tree->getNodeForPath($path);
+ } catch (NotFound $e) {
+ return;
+ }
+ if (!$node instanceof IOutbox) {
+ return;
+ }
+
+ $this->server->transactionType = 'post-caldav-outbox';
+ $this->outboxRequest($node, $request, $response);
+
+ // Returning false breaks the event chain and tells the server we've
+ // handled the request.
+ return false;
+ }
+
+ /**
+ * This method handler is invoked during fetching of properties.
+ *
+ * We use this event to add calendar-auto-schedule-specific properties.
+ */
+ public function propFind(PropFind $propFind, INode $node)
+ {
+ if ($node instanceof DAVACL\IPrincipal) {
+ $caldavPlugin = $this->server->getPlugin('caldav');
+ $principalUrl = $node->getPrincipalUrl();
+
+ // schedule-outbox-URL property
+ $propFind->handle('{'.self::NS_CALDAV.'}schedule-outbox-URL', function () use ($principalUrl, $caldavPlugin) {
+ $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
+ if (!$calendarHomePath) {
+ return null;
+ }
+ $outboxPath = $calendarHomePath.'/outbox/';
+
+ return new LocalHref($outboxPath);
+ });
+ // schedule-inbox-URL property
+ $propFind->handle('{'.self::NS_CALDAV.'}schedule-inbox-URL', function () use ($principalUrl, $caldavPlugin) {
+ $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
+ if (!$calendarHomePath) {
+ return null;
+ }
+ $inboxPath = $calendarHomePath.'/inbox/';
+
+ return new LocalHref($inboxPath);
+ });
+
+ $propFind->handle('{'.self::NS_CALDAV.'}schedule-default-calendar-URL', function () use ($principalUrl, $caldavPlugin) {
+ // We don't support customizing this property yet, so in the
+ // meantime we just grab the first calendar in the home-set.
+ $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
+
+ if (!$calendarHomePath) {
+ return null;
+ }
+
+ $sccs = '{'.self::NS_CALDAV.'}supported-calendar-component-set';
+
+ $result = $this->server->getPropertiesForPath($calendarHomePath, [
+ '{DAV:}resourcetype',
+ '{DAV:}share-access',
+ $sccs,
+ ], 1);
+
+ foreach ($result as $child) {
+ if (!isset($child[200]['{DAV:}resourcetype']) || !$child[200]['{DAV:}resourcetype']->is('{'.self::NS_CALDAV.'}calendar')) {
+ // Node is either not a calendar
+ continue;
+ }
+ if (isset($child[200]['{DAV:}share-access'])) {
+ $shareAccess = $child[200]['{DAV:}share-access']->getValue();
+ if (Sharing\Plugin::ACCESS_NOTSHARED !== $shareAccess && Sharing\Plugin::ACCESS_SHAREDOWNER !== $shareAccess) {
+ // Node is a shared node, not owned by the relevant
+ // user.
+ continue;
+ }
+ }
+ if (!isset($child[200][$sccs]) || in_array('VEVENT', $child[200][$sccs]->getValue())) {
+ // Either there is no supported-calendar-component-set
+ // (which is fine) or we found one that supports VEVENT.
+ return new LocalHref($child['href']);
+ }
+ }
+ });
+
+ // The server currently reports every principal to be of type
+ // 'INDIVIDUAL'
+ $propFind->handle('{'.self::NS_CALDAV.'}calendar-user-type', function () {
+ return 'INDIVIDUAL';
+ });
+ }
+
+ // Mapping the old property to the new property.
+ $propFind->handle('{http://calendarserver.org/ns/}calendar-availability', function () use ($propFind, $node) {
+ // In case it wasn't clear, the only difference is that we map the
+ // old property to a different namespace.
+ $availProp = '{'.self::NS_CALDAV.'}calendar-availability';
+ $subPropFind = new PropFind(
+ $propFind->getPath(),
+ [$availProp]
+ );
+
+ $this->server->getPropertiesByNode(
+ $subPropFind,
+ $node
+ );
+
+ $propFind->set(
+ '{http://calendarserver.org/ns/}calendar-availability',
+ $subPropFind->get($availProp),
+ $subPropFind->getStatus($availProp)
+ );
+ });
+ }
+
+ /**
+ * This method is called during property updates.
+ *
+ * @param string $path
+ */
+ public function propPatch($path, PropPatch $propPatch)
+ {
+ // Mapping the old property to the new property.
+ $propPatch->handle('{http://calendarserver.org/ns/}calendar-availability', function ($value) use ($path) {
+ $availProp = '{'.self::NS_CALDAV.'}calendar-availability';
+ $subPropPatch = new PropPatch([$availProp => $value]);
+ $this->server->emit('propPatch', [$path, $subPropPatch]);
+ $subPropPatch->commit();
+
+ return $subPropPatch->getResult()[$availProp];
+ });
+ }
+
+ /**
+ * This method is triggered whenever there was a calendar object gets
+ * created or updated.
+ *
+ * @param RequestInterface $request HTTP request
+ * @param ResponseInterface $response HTTP Response
+ * @param VCalendar $vCal Parsed iCalendar object
+ * @param mixed $calendarPath Path to calendar collection
+ * @param mixed $modified the iCalendar object has been touched
+ * @param mixed $isNew Whether this was a new item or we're updating one
+ */
+ public function calendarObjectChange(RequestInterface $request, ResponseInterface $response, VCalendar $vCal, $calendarPath, &$modified, $isNew)
+ {
+ if (!$this->scheduleReply($this->server->httpRequest)) {
+ return;
+ }
+
+ $calendarNode = $this->server->tree->getNodeForPath($calendarPath);
+
+ $addresses = $this->getAddressesForPrincipal(
+ $calendarNode->getOwner()
+ );
+
+ if (!$isNew) {
+ $node = $this->server->tree->getNodeForPath($request->getPath());
+ $oldObj = Reader::read($node->get());
+ } else {
+ $oldObj = null;
+ }
+
+ $this->processICalendarChange($oldObj, $vCal, $addresses, [], $modified);
+
+ if ($oldObj) {
+ // Destroy circular references so PHP will GC the object.
+ $oldObj->destroy();
+ }
+ }
+
+ /**
+ * This method is responsible for delivering the ITip message.
+ */
+ public function deliver(ITip\Message $iTipMessage)
+ {
+ $this->server->emit('schedule', [$iTipMessage]);
+ if (!$iTipMessage->scheduleStatus) {
+ $iTipMessage->scheduleStatus = '5.2;There was no system capable of delivering the scheduling message';
+ }
+ // In case the change was considered 'insignificant', we are going to
+ // remove any error statuses, if any. See ticket #525.
+ list($baseCode) = explode('.', $iTipMessage->scheduleStatus);
+ if (!$iTipMessage->significantChange && in_array($baseCode, ['3', '5'])) {
+ $iTipMessage->scheduleStatus = null;
+ }
+ }
+
+ /**
+ * This method is triggered before a file gets deleted.
+ *
+ * We use this event to make sure that when this happens, attendees get
+ * cancellations, and organizers get 'DECLINED' statuses.
+ *
+ * @param string $path
+ */
+ public function beforeUnbind($path)
+ {
+ // FIXME: We shouldn't trigger this functionality when we're issuing a
+ // MOVE. This is a hack.
+ if ('MOVE' === $this->server->httpRequest->getMethod()) {
+ return;
+ }
+
+ $node = $this->server->tree->getNodeForPath($path);
+
+ if (!$node instanceof ICalendarObject || $node instanceof ISchedulingObject) {
+ return;
+ }
+
+ if (!$this->scheduleReply($this->server->httpRequest)) {
+ return;
+ }
+
+ $addresses = $this->getAddressesForPrincipal(
+ $node->getOwner()
+ );
+
+ $broker = $this->createITipBroker();
+ $messages = $broker->parseEvent(null, $addresses, $node->get());
+
+ foreach ($messages as $message) {
+ $this->deliver($message);
+ }
+ }
+
+ /**
+ * Event handler for the 'schedule' event.
+ *
+ * This handler attempts to look at local accounts to deliver the
+ * scheduling object.
+ */
+ public function scheduleLocalDelivery(ITip\Message $iTipMessage)
+ {
+ $aclPlugin = $this->server->getPlugin('acl');
+
+ // Local delivery is not available if the ACL plugin is not loaded.
+ if (!$aclPlugin) {
+ return;
+ }
+
+ $caldavNS = '{'.self::NS_CALDAV.'}';
+
+ $principalUri = $aclPlugin->getPrincipalByUri($iTipMessage->recipient);
+ if (!$principalUri) {
+ $iTipMessage->scheduleStatus = '3.7;Could not find principal.';
+
+ return;
+ }
+
+ // We found a principal URL, now we need to find its inbox.
+ // Unfortunately we may not have sufficient privileges to find this, so
+ // we are temporarily turning off ACL to let this come through.
+ //
+ // Once we support PHP 5.5, this should be wrapped in a try..finally
+ // block so we can ensure that this privilege gets added again after.
+ $this->server->removeListener('propFind', [$aclPlugin, 'propFind']);
+
+ $result = $this->server->getProperties(
+ $principalUri,
+ [
+ '{DAV:}principal-URL',
+ $caldavNS.'calendar-home-set',
+ $caldavNS.'schedule-inbox-URL',
+ $caldavNS.'schedule-default-calendar-URL',
+ '{http://sabredav.org/ns}email-address',
+ ]
+ );
+
+ // Re-registering the ACL event
+ $this->server->on('propFind', [$aclPlugin, 'propFind'], 20);
+
+ if (!isset($result[$caldavNS.'schedule-inbox-URL'])) {
+ $iTipMessage->scheduleStatus = '5.2;Could not find local inbox';
+
+ return;
+ }
+ if (!isset($result[$caldavNS.'calendar-home-set'])) {
+ $iTipMessage->scheduleStatus = '5.2;Could not locate a calendar-home-set';
+
+ return;
+ }
+ if (!isset($result[$caldavNS.'schedule-default-calendar-URL'])) {
+ $iTipMessage->scheduleStatus = '5.2;Could not find a schedule-default-calendar-URL property';
+
+ return;
+ }
+
+ $calendarPath = $result[$caldavNS.'schedule-default-calendar-URL']->getHref();
+ $homePath = $result[$caldavNS.'calendar-home-set']->getHref();
+ $inboxPath = $result[$caldavNS.'schedule-inbox-URL']->getHref();
+
+ if ('REPLY' === $iTipMessage->method) {
+ $privilege = 'schedule-deliver-reply';
+ } else {
+ $privilege = 'schedule-deliver-invite';
+ }
+
+ if (!$aclPlugin->checkPrivileges($inboxPath, $caldavNS.$privilege, DAVACL\Plugin::R_PARENT, false)) {
+ $iTipMessage->scheduleStatus = '3.8;insufficient privileges: '.$privilege.' is required on the recipient schedule inbox.';
+
+ return;
+ }
+
+ // Next, we're going to find out if the item already exits in one of
+ // the users' calendars.
+ $uid = $iTipMessage->uid;
+
+ $newFileName = 'sabredav-'.\Sabre\DAV\UUIDUtil::getUUID().'.ics';
+
+ $home = $this->server->tree->getNodeForPath($homePath);
+ $inbox = $this->server->tree->getNodeForPath($inboxPath);
+
+ $currentObject = null;
+ $objectNode = null;
+ $oldICalendarData = null;
+ $isNewNode = false;
+
+ $result = $home->getCalendarObjectByUID($uid);
+ if ($result) {
+ // There was an existing object, we need to update probably.
+ $objectPath = $homePath.'/'.$result;
+ $objectNode = $this->server->tree->getNodeForPath($objectPath);
+ $oldICalendarData = $objectNode->get();
+ $currentObject = Reader::read($oldICalendarData);
+ } else {
+ $isNewNode = true;
+ }
+
+ $broker = $this->createITipBroker();
+ $newObject = $broker->processMessage($iTipMessage, $currentObject);
+
+ $inbox->createFile($newFileName, $iTipMessage->message->serialize());
+
+ if (!$newObject) {
+ // We received an iTip message referring to a UID that we don't
+ // have in any calendars yet, and processMessage did not give us a
+ // calendarobject back.
+ //
+ // The implication is that processMessage did not understand the
+ // iTip message.
+ $iTipMessage->scheduleStatus = '5.0;iTip message was not processed by the server, likely because we didn\'t understand it.';
+
+ return;
+ }
+
+ // Note that we are bypassing ACL on purpose by calling this directly.
+ // We may need to look a bit deeper into this later. Supporting ACL
+ // here would be nice.
+ if ($isNewNode) {
+ $calendar = $this->server->tree->getNodeForPath($calendarPath);
+ $calendar->createFile($newFileName, $newObject->serialize());
+ } else {
+ // If the message was a reply, we may have to inform other
+ // attendees of this attendees status. Therefore we're shooting off
+ // another itipMessage.
+ if ('REPLY' === $iTipMessage->method) {
+ $this->processICalendarChange(
+ $oldICalendarData,
+ $newObject,
+ [$iTipMessage->recipient],
+ [$iTipMessage->sender]
+ );
+ }
+ $objectNode->put($newObject->serialize());
+ }
+ $iTipMessage->scheduleStatus = '1.2;Message delivered locally';
+ }
+
+ /**
+ * This method is triggered whenever a subsystem requests the privileges
+ * that are supported on a particular node.
+ *
+ * We need to add a number of privileges for scheduling purposes.
+ */
+ public function getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet)
+ {
+ $ns = '{'.self::NS_CALDAV.'}';
+ if ($node instanceof IOutbox) {
+ $supportedPrivilegeSet[$ns.'schedule-send'] = [
+ 'abstract' => false,
+ 'aggregates' => [
+ $ns.'schedule-send-invite' => [
+ 'abstract' => false,
+ 'aggregates' => [],
+ ],
+ $ns.'schedule-send-reply' => [
+ 'abstract' => false,
+ 'aggregates' => [],
+ ],
+ $ns.'schedule-send-freebusy' => [
+ 'abstract' => false,
+ 'aggregates' => [],
+ ],
+ // Privilege from an earlier scheduling draft, but still
+ // used by some clients.
+ $ns.'schedule-post-vevent' => [
+ 'abstract' => false,
+ 'aggregates' => [],
+ ],
+ ],
+ ];
+ }
+ if ($node instanceof IInbox) {
+ $supportedPrivilegeSet[$ns.'schedule-deliver'] = [
+ 'abstract' => false,
+ 'aggregates' => [
+ $ns.'schedule-deliver-invite' => [
+ 'abstract' => false,
+ 'aggregates' => [],
+ ],
+ $ns.'schedule-deliver-reply' => [
+ 'abstract' => false,
+ 'aggregates' => [],
+ ],
+ $ns.'schedule-query-freebusy' => [
+ 'abstract' => false,
+ 'aggregates' => [],
+ ],
+ ],
+ ];
+ }
+ }
+
+ /**
+ * This method looks at an old iCalendar object, a new iCalendar object and
+ * starts sending scheduling messages based on the changes.
+ *
+ * A list of addresses needs to be specified, so the system knows who made
+ * the update, because the behavior may be different based on if it's an
+ * attendee or an organizer.
+ *
+ * This method may update $newObject to add any status changes.
+ *
+ * @param VCalendar|string|null $oldObject
+ * @param array $ignore any addresses to not send messages to
+ * @param bool $modified a marker to indicate that the original object modified by this process
+ */
+ protected function processICalendarChange($oldObject, VCalendar $newObject, array $addresses, array $ignore = [], &$modified = false)
+ {
+ $broker = $this->createITipBroker();
+ $messages = $broker->parseEvent($newObject, $addresses, $oldObject);
+
+ if ($messages) {
+ $modified = true;
+ }
+
+ foreach ($messages as $message) {
+ if (in_array($message->recipient, $ignore)) {
+ continue;
+ }
+
+ $this->deliver($message);
+
+ if (isset($newObject->VEVENT->ORGANIZER) && ($newObject->VEVENT->ORGANIZER->getNormalizedValue() === $message->recipient)) {
+ if ($message->scheduleStatus) {
+ $newObject->VEVENT->ORGANIZER['SCHEDULE-STATUS'] = $message->getScheduleStatus();
+ }
+ unset($newObject->VEVENT->ORGANIZER['SCHEDULE-FORCE-SEND']);
+ } else {
+ if (isset($newObject->VEVENT->ATTENDEE)) {
+ foreach ($newObject->VEVENT->ATTENDEE as $attendee) {
+ if ($attendee->getNormalizedValue() === $message->recipient) {
+ if ($message->scheduleStatus) {
+ $attendee['SCHEDULE-STATUS'] = $message->getScheduleStatus();
+ }
+ unset($attendee['SCHEDULE-FORCE-SEND']);
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns a list of addresses that are associated with a principal.
+ *
+ * @param string $principal
+ *
+ * @return array
+ */
+ protected function getAddressesForPrincipal($principal)
+ {
+ $CUAS = '{'.self::NS_CALDAV.'}calendar-user-address-set';
+
+ $properties = $this->server->getProperties(
+ $principal,
+ [$CUAS]
+ );
+
+ // If we can't find this information, we'll stop processing
+ if (!isset($properties[$CUAS])) {
+ return [];
+ }
+
+ $addresses = $properties[$CUAS]->getHrefs();
+
+ return $addresses;
+ }
+
+ /**
+ * This method handles POST requests to the schedule-outbox.
+ *
+ * Currently, two types of requests are supported:
+ * * FREEBUSY requests from RFC 6638
+ * * Simple iTIP messages from draft-desruisseaux-caldav-sched-04
+ *
+ * The latter is from an expired early draft of the CalDAV scheduling
+ * extensions, but iCal depends on a feature from that spec, so we
+ * implement it.
+ */
+ public function outboxRequest(IOutbox $outboxNode, RequestInterface $request, ResponseInterface $response)
+ {
+ $outboxPath = $request->getPath();
+
+ // Parsing the request body
+ try {
+ $vObject = VObject\Reader::read($request->getBody());
+ } catch (VObject\ParseException $e) {
+ throw new BadRequest('The request body must be a valid iCalendar object. Parse error: '.$e->getMessage());
+ }
+
+ // The incoming iCalendar object must have a METHOD property, and a
+ // component. The combination of both determines what type of request
+ // this is.
+ $componentType = null;
+ foreach ($vObject->getComponents() as $component) {
+ if ('VTIMEZONE' !== $component->name) {
+ $componentType = $component->name;
+ break;
+ }
+ }
+ if (is_null($componentType)) {
+ throw new BadRequest('We expected at least one VTODO, VJOURNAL, VFREEBUSY or VEVENT component');
+ }
+
+ // Validating the METHOD
+ $method = strtoupper((string) $vObject->METHOD);
+ if (!$method) {
+ throw new BadRequest('A METHOD property must be specified in iTIP messages');
+ }
+
+ // So we support one type of request:
+ //
+ // REQUEST with a VFREEBUSY component
+
+ $acl = $this->server->getPlugin('acl');
+
+ if ('VFREEBUSY' === $componentType && 'REQUEST' === $method) {
+ $acl && $acl->checkPrivileges($outboxPath, '{'.self::NS_CALDAV.'}schedule-send-freebusy');
+ $this->handleFreeBusyRequest($outboxNode, $vObject, $request, $response);
+
+ // Destroy circular references so PHP can GC the object.
+ $vObject->destroy();
+ unset($vObject);
+ } else {
+ throw new NotImplemented('We only support VFREEBUSY (REQUEST) on this endpoint');
+ }
+ }
+
+ /**
+ * This method is responsible for parsing a free-busy query request and
+ * returning its result in $response.
+ */
+ protected function handleFreeBusyRequest(IOutbox $outbox, VObject\Component $vObject, RequestInterface $request, ResponseInterface $response)
+ {
+ $vFreeBusy = $vObject->VFREEBUSY;
+ $organizer = $vFreeBusy->ORGANIZER;
+
+ $organizer = (string) $organizer;
+
+ // Validating if the organizer matches the owner of the inbox.
+ $owner = $outbox->getOwner();
+
+ $caldavNS = '{'.self::NS_CALDAV.'}';
+
+ $uas = $caldavNS.'calendar-user-address-set';
+ $props = $this->server->getProperties($owner, [$uas]);
+
+ if (empty($props[$uas]) || !in_array($organizer, $props[$uas]->getHrefs())) {
+ throw new Forbidden('The organizer in the request did not match any of the addresses for the owner of this inbox');
+ }
+
+ if (!isset($vFreeBusy->ATTENDEE)) {
+ throw new BadRequest('You must at least specify 1 attendee');
+ }
+
+ $attendees = [];
+ foreach ($vFreeBusy->ATTENDEE as $attendee) {
+ $attendees[] = (string) $attendee;
+ }
+
+ if (!isset($vFreeBusy->DTSTART) || !isset($vFreeBusy->DTEND)) {
+ throw new BadRequest('DTSTART and DTEND must both be specified');
+ }
+
+ $startRange = $vFreeBusy->DTSTART->getDateTime();
+ $endRange = $vFreeBusy->DTEND->getDateTime();
+
+ $results = [];
+ foreach ($attendees as $attendee) {
+ $results[] = $this->getFreeBusyForEmail($attendee, $startRange, $endRange, $vObject);
+ }
+
+ $dom = new \DOMDocument('1.0', 'utf-8');
+ $dom->formatOutput = true;
+ $scheduleResponse = $dom->createElement('cal:schedule-response');
+ foreach ($this->server->xml->namespaceMap as $namespace => $prefix) {
+ $scheduleResponse->setAttribute('xmlns:'.$prefix, $namespace);
+ }
+ $dom->appendChild($scheduleResponse);
+
+ foreach ($results as $result) {
+ $xresponse = $dom->createElement('cal:response');
+
+ $recipient = $dom->createElement('cal:recipient');
+ $recipientHref = $dom->createElement('d:href');
+
+ $recipientHref->appendChild($dom->createTextNode($result['href']));
+ $recipient->appendChild($recipientHref);
+ $xresponse->appendChild($recipient);
+
+ $reqStatus = $dom->createElement('cal:request-status');
+ $reqStatus->appendChild($dom->createTextNode($result['request-status']));
+ $xresponse->appendChild($reqStatus);
+
+ if (isset($result['calendar-data'])) {
+ $calendardata = $dom->createElement('cal:calendar-data');
+ $calendardata->appendChild($dom->createTextNode(str_replace("\r\n", "\n", $result['calendar-data']->serialize())));
+ $xresponse->appendChild($calendardata);
+ }
+ $scheduleResponse->appendChild($xresponse);
+ }
+
+ $response->setStatus(200);
+ $response->setHeader('Content-Type', 'application/xml');
+ $response->setBody($dom->saveXML());
+ }
+
+ /**
+ * Returns free-busy information for a specific address. The returned
+ * data is an array containing the following properties:.
+ *
+ * calendar-data : A VFREEBUSY VObject
+ * request-status : an iTip status code.
+ * href: The principal's email address, as requested
+ *
+ * The following request status codes may be returned:
+ * * 2.0;description
+ * * 3.7;description
+ *
+ * @param string $email address
+ *
+ * @return array
+ */
+ protected function getFreeBusyForEmail($email, \DateTimeInterface $start, \DateTimeInterface $end, VObject\Component $request)
+ {
+ $caldavNS = '{'.self::NS_CALDAV.'}';
+
+ $aclPlugin = $this->server->getPlugin('acl');
+ if ('mailto:' === substr($email, 0, 7)) {
+ $email = substr($email, 7);
+ }
+
+ $result = $aclPlugin->principalSearch(
+ ['{http://sabredav.org/ns}email-address' => $email],
+ [
+ '{DAV:}principal-URL',
+ $caldavNS.'calendar-home-set',
+ $caldavNS.'schedule-inbox-URL',
+ '{http://sabredav.org/ns}email-address',
+ ]
+ );
+
+ if (!count($result)) {
+ return [
+ 'request-status' => '3.7;Could not find principal',
+ 'href' => 'mailto:'.$email,
+ ];
+ }
+
+ if (!isset($result[0][200][$caldavNS.'calendar-home-set'])) {
+ return [
+ 'request-status' => '3.7;No calendar-home-set property found',
+ 'href' => 'mailto:'.$email,
+ ];
+ }
+ if (!isset($result[0][200][$caldavNS.'schedule-inbox-URL'])) {
+ return [
+ 'request-status' => '3.7;No schedule-inbox-URL property found',
+ 'href' => 'mailto:'.$email,
+ ];
+ }
+ $homeSet = $result[0][200][$caldavNS.'calendar-home-set']->getHref();
+ $inboxUrl = $result[0][200][$caldavNS.'schedule-inbox-URL']->getHref();
+
+ // Do we have permission?
+ $aclPlugin->checkPrivileges($inboxUrl, $caldavNS.'schedule-query-freebusy');
+
+ // Grabbing the calendar list
+ $objects = [];
+ $calendarTimeZone = new DateTimeZone('UTC');
+
+ foreach ($this->server->tree->getNodeForPath($homeSet)->getChildren() as $node) {
+ if (!$node instanceof ICalendar) {
+ continue;
+ }
+
+ $sct = $caldavNS.'schedule-calendar-transp';
+ $ctz = $caldavNS.'calendar-timezone';
+ $props = $node->getProperties([$sct, $ctz]);
+
+ if (isset($props[$sct]) && ScheduleCalendarTransp::TRANSPARENT == $props[$sct]->getValue()) {
+ // If a calendar is marked as 'transparent', it means we must
+ // ignore it for free-busy purposes.
+ continue;
+ }
+
+ if (isset($props[$ctz])) {
+ $vtimezoneObj = VObject\Reader::read($props[$ctz]);
+ $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
+
+ // Destroy circular references so PHP can garbage collect the object.
+ $vtimezoneObj->destroy();
+ }
+
+ // Getting the list of object uris within the time-range
+ $urls = $node->calendarQuery([
+ 'name' => 'VCALENDAR',
+ 'comp-filters' => [
+ [
+ 'name' => 'VEVENT',
+ 'comp-filters' => [],
+ 'prop-filters' => [],
+ 'is-not-defined' => false,
+ 'time-range' => [
+ 'start' => $start,
+ 'end' => $end,
+ ],
+ ],
+ ],
+ 'prop-filters' => [],
+ 'is-not-defined' => false,
+ 'time-range' => null,
+ ]);
+
+ $calObjects = array_map(function ($url) use ($node) {
+ $obj = $node->getChild($url)->get();
+
+ return $obj;
+ }, $urls);
+
+ $objects = array_merge($objects, $calObjects);
+ }
+
+ $inboxProps = $this->server->getProperties(
+ $inboxUrl,
+ $caldavNS.'calendar-availability'
+ );
+
+ $vcalendar = new VObject\Component\VCalendar();
+ $vcalendar->METHOD = 'REPLY';
+
+ $generator = new VObject\FreeBusyGenerator();
+ $generator->setObjects($objects);
+ $generator->setTimeRange($start, $end);
+ $generator->setBaseObject($vcalendar);
+ $generator->setTimeZone($calendarTimeZone);
+
+ if ($inboxProps) {
+ $generator->setVAvailability(
+ VObject\Reader::read(
+ $inboxProps[$caldavNS.'calendar-availability']
+ )
+ );
+ }
+
+ $result = $generator->getResult();
+
+ $vcalendar->VFREEBUSY->ATTENDEE = 'mailto:'.$email;
+ $vcalendar->VFREEBUSY->UID = (string) $request->VFREEBUSY->UID;
+ $vcalendar->VFREEBUSY->ORGANIZER = clone $request->VFREEBUSY->ORGANIZER;
+
+ return [
+ 'calendar-data' => $result,
+ 'request-status' => '2.0;Success',
+ 'href' => 'mailto:'.$email,
+ ];
+ }
+
+ /**
+ * This method checks the 'Schedule-Reply' header
+ * and returns false if it's 'F', otherwise true.
+ *
+ * @return bool
+ */
+ protected function scheduleReply(RequestInterface $request)
+ {
+ $scheduleReply = $request->getHeader('Schedule-Reply');
+
+ return 'F' !== $scheduleReply;
+ }
+
+ /**
+ * Returns a bunch of meta-data about the plugin.
+ *
+ * Providing this information is optional, and is mainly displayed by the
+ * Browser plugin.
+ *
+ * The description key in the returned array may contain html and will not
+ * be sanitized.
+ *
+ * @return array
+ */
+ public function getPluginInfo()
+ {
+ return [
+ 'name' => $this->getPluginName(),
+ 'description' => 'Adds calendar-auto-schedule, as defined in rfc6638',
+ 'link' => 'http://sabre.io/dav/scheduling/',
+ ];
+ }
+
+ /**
+ * Returns an instance of the iTip\Broker.
+ */
+ protected function createITipBroker(): Broker
+ {
+ return new Broker();
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php
new file mode 100644
index 0000000..b40f28a
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php
@@ -0,0 +1,130 @@
+objectData['calendardata'])) {
+ $this->objectData = $this->caldavBackend->getSchedulingObject($this->objectData['principaluri'], $this->objectData['uri']);
+ }
+
+ return $this->objectData['calendardata'];
+ }
+
+ /**
+ * Updates the ICalendar-formatted object.
+ *
+ * @param string|resource $calendarData
+ *
+ * @return string
+ */
+ public function put($calendarData)
+ {
+ throw new MethodNotAllowed('Updating scheduling objects is not supported');
+ }
+
+ /**
+ * Deletes the scheduling message.
+ */
+ public function delete()
+ {
+ $this->caldavBackend->deleteSchedulingObject($this->objectData['principaluri'], $this->objectData['uri']);
+ }
+
+ /**
+ * Returns the owner principal.
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner()
+ {
+ return $this->objectData['principaluri'];
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ public function getACL()
+ {
+ // An alternative acl may be specified in the object data.
+ //
+
+ if (isset($this->objectData['acl'])) {
+ return $this->objectData['acl'];
+ }
+
+ // The default ACL
+ return [
+ [
+ 'privilege' => '{DAV:}all',
+ 'principal' => '{DAV:}owner',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}all',
+ 'principal' => $this->objectData['principaluri'].'/calendar-proxy-write',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->objectData['principaluri'].'/calendar-proxy-read',
+ 'protected' => true,
+ ],
+ ];
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/SharedCalendar.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/SharedCalendar.php
new file mode 100644
index 0000000..818392f
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/SharedCalendar.php
@@ -0,0 +1,219 @@
+calendarInfo['share-access']) ? $this->calendarInfo['share-access'] : SPlugin::ACCESS_NOTSHARED;
+ }
+
+ /**
+ * This function must return a URI that uniquely identifies the shared
+ * resource. This URI should be identical across instances, and is
+ * also used in several other XML bodies to connect invites to
+ * resources.
+ *
+ * This may simply be a relative reference to the original shared instance,
+ * but it could also be a urn. As long as it's a valid URI and unique.
+ *
+ * @return string
+ */
+ public function getShareResourceUri()
+ {
+ return $this->calendarInfo['share-resource-uri'];
+ }
+
+ /**
+ * Updates the list of sharees.
+ *
+ * Every item must be a Sharee object.
+ *
+ * @param \Sabre\DAV\Xml\Element\Sharee[] $sharees
+ */
+ public function updateInvites(array $sharees)
+ {
+ $this->caldavBackend->updateInvites($this->calendarInfo['id'], $sharees);
+ }
+
+ /**
+ * Returns the list of people whom this resource is shared with.
+ *
+ * Every item in the returned array must be a Sharee object with
+ * at least the following properties set:
+ *
+ * * $href
+ * * $shareAccess
+ * * $inviteStatus
+ *
+ * and optionally:
+ *
+ * * $properties
+ *
+ * @return \Sabre\DAV\Xml\Element\Sharee[]
+ */
+ public function getInvites()
+ {
+ return $this->caldavBackend->getInvites($this->calendarInfo['id']);
+ }
+
+ /**
+ * Marks this calendar as published.
+ *
+ * Publishing a calendar should automatically create a read-only, public,
+ * subscribable calendar.
+ *
+ * @param bool $value
+ */
+ public function setPublishStatus($value)
+ {
+ $this->caldavBackend->setPublishStatus($this->calendarInfo['id'], $value);
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ public function getACL()
+ {
+ $acl = [];
+
+ switch ($this->getShareAccess()) {
+ case SPlugin::ACCESS_NOTSHARED:
+ case SPlugin::ACCESS_SHAREDOWNER:
+ $acl[] = [
+ 'privilege' => '{DAV:}share',
+ 'principal' => $this->calendarInfo['principaluri'],
+ 'protected' => true,
+ ];
+ $acl[] = [
+ 'privilege' => '{DAV:}share',
+ 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write',
+ 'protected' => true,
+ ];
+ // no break intentional!
+ case SPlugin::ACCESS_READWRITE:
+ $acl[] = [
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->calendarInfo['principaluri'],
+ 'protected' => true,
+ ];
+ $acl[] = [
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write',
+ 'protected' => true,
+ ];
+ // no break intentional!
+ case SPlugin::ACCESS_READ:
+ $acl[] = [
+ 'privilege' => '{DAV:}write-properties',
+ 'principal' => $this->calendarInfo['principaluri'],
+ 'protected' => true,
+ ];
+ $acl[] = [
+ 'privilege' => '{DAV:}write-properties',
+ 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write',
+ 'protected' => true,
+ ];
+ $acl[] = [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->calendarInfo['principaluri'],
+ 'protected' => true,
+ ];
+ $acl[] = [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-read',
+ 'protected' => true,
+ ];
+ $acl[] = [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write',
+ 'protected' => true,
+ ];
+ $acl[] = [
+ 'privilege' => '{'.Plugin::NS_CALDAV.'}read-free-busy',
+ 'principal' => '{DAV:}authenticated',
+ 'protected' => true,
+ ];
+ break;
+ }
+
+ return $acl;
+ }
+
+ /**
+ * This method returns the ACL's for calendar objects in this calendar.
+ * The result of this method automatically gets passed to the
+ * calendar-object nodes in the calendar.
+ *
+ * @return array
+ */
+ public function getChildACL()
+ {
+ $acl = [];
+
+ switch ($this->getShareAccess()) {
+ case SPlugin::ACCESS_NOTSHARED:
+ case SPlugin::ACCESS_SHAREDOWNER:
+ case SPlugin::ACCESS_READWRITE:
+ $acl[] = [
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->calendarInfo['principaluri'],
+ 'protected' => true,
+ ];
+ $acl[] = [
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write',
+ 'protected' => true,
+ ];
+ // no break intentional
+ case SPlugin::ACCESS_READ:
+ $acl[] = [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->calendarInfo['principaluri'],
+ 'protected' => true,
+ ];
+ $acl[] = [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write',
+ 'protected' => true,
+ ];
+ $acl[] = [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-read',
+ 'protected' => true,
+ ];
+ break;
+ }
+
+ return $acl;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/SharingPlugin.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/SharingPlugin.php
new file mode 100644
index 0000000..bacfe04
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/SharingPlugin.php
@@ -0,0 +1,346 @@
+server = $server;
+
+ if (is_null($this->server->getPlugin('sharing'))) {
+ throw new \LogicException('The generic "sharing" plugin must be loaded before the caldav sharing plugin. Call $server->addPlugin(new \Sabre\DAV\Sharing\Plugin()); before this one.');
+ }
+
+ array_push(
+ $this->server->protectedProperties,
+ '{'.Plugin::NS_CALENDARSERVER.'}invite',
+ '{'.Plugin::NS_CALENDARSERVER.'}allowed-sharing-modes',
+ '{'.Plugin::NS_CALENDARSERVER.'}shared-url'
+ );
+
+ $this->server->xml->elementMap['{'.Plugin::NS_CALENDARSERVER.'}share'] = 'Sabre\\CalDAV\\Xml\\Request\\Share';
+ $this->server->xml->elementMap['{'.Plugin::NS_CALENDARSERVER.'}invite-reply'] = 'Sabre\\CalDAV\\Xml\\Request\\InviteReply';
+
+ $this->server->on('propFind', [$this, 'propFindEarly']);
+ $this->server->on('propFind', [$this, 'propFindLate'], 150);
+ $this->server->on('propPatch', [$this, 'propPatch'], 40);
+ $this->server->on('method:POST', [$this, 'httpPost']);
+ }
+
+ /**
+ * This event is triggered when properties are requested for a certain
+ * node.
+ *
+ * This allows us to inject any properties early.
+ */
+ public function propFindEarly(DAV\PropFind $propFind, DAV\INode $node)
+ {
+ if ($node instanceof ISharedCalendar) {
+ $propFind->handle('{'.Plugin::NS_CALENDARSERVER.'}invite', function () use ($node) {
+ return new Xml\Property\Invite(
+ $node->getInvites()
+ );
+ });
+ }
+ }
+
+ /**
+ * This method is triggered *after* all properties have been retrieved.
+ * This allows us to inject the correct resourcetype for calendars that
+ * have been shared.
+ */
+ public function propFindLate(DAV\PropFind $propFind, DAV\INode $node)
+ {
+ if ($node instanceof ISharedCalendar) {
+ $shareAccess = $node->getShareAccess();
+ if ($rt = $propFind->get('{DAV:}resourcetype')) {
+ switch ($shareAccess) {
+ case \Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER:
+ $rt->add('{'.Plugin::NS_CALENDARSERVER.'}shared-owner');
+ break;
+ case \Sabre\DAV\Sharing\Plugin::ACCESS_READ:
+ case \Sabre\DAV\Sharing\Plugin::ACCESS_READWRITE:
+ $rt->add('{'.Plugin::NS_CALENDARSERVER.'}shared');
+ break;
+ }
+ }
+ $propFind->handle('{'.Plugin::NS_CALENDARSERVER.'}allowed-sharing-modes', function () {
+ return new Xml\Property\AllowedSharingModes(true, false);
+ });
+ }
+ }
+
+ /**
+ * This method is triggered when a user attempts to update a node's
+ * properties.
+ *
+ * A previous draft of the sharing spec stated that it was possible to use
+ * PROPPATCH to remove 'shared-owner' from the resourcetype, thus unsharing
+ * the calendar.
+ *
+ * Even though this is no longer in the current spec, we keep this around
+ * because OS X 10.7 may still make use of this feature.
+ *
+ * @param string $path
+ */
+ public function propPatch($path, DAV\PropPatch $propPatch)
+ {
+ $node = $this->server->tree->getNodeForPath($path);
+ if (!$node instanceof ISharedCalendar) {
+ return;
+ }
+
+ if (\Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER === $node->getShareAccess() || \Sabre\DAV\Sharing\Plugin::ACCESS_NOTSHARED === $node->getShareAccess()) {
+ $propPatch->handle('{DAV:}resourcetype', function ($value) use ($node) {
+ if ($value->is('{'.Plugin::NS_CALENDARSERVER.'}shared-owner')) {
+ return false;
+ }
+ $shares = $node->getInvites();
+ foreach ($shares as $share) {
+ $share->access = DAV\Sharing\Plugin::ACCESS_NOACCESS;
+ }
+ $node->updateInvites($shares);
+
+ return true;
+ });
+ }
+ }
+
+ /**
+ * We intercept this to handle POST requests on calendars.
+ *
+ * @return bool|null
+ */
+ public function httpPost(RequestInterface $request, ResponseInterface $response)
+ {
+ $path = $request->getPath();
+
+ // Only handling xml
+ $contentType = $request->getHeader('Content-Type');
+ if (null === $contentType) {
+ return;
+ }
+ if (false === strpos($contentType, 'application/xml') && false === strpos($contentType, 'text/xml')) {
+ return;
+ }
+
+ // Making sure the node exists
+ try {
+ $node = $this->server->tree->getNodeForPath($path);
+ } catch (DAV\Exception\NotFound $e) {
+ return;
+ }
+
+ $requestBody = $request->getBodyAsString();
+
+ // If this request handler could not deal with this POST request, it
+ // will return 'null' and other plugins get a chance to handle the
+ // request.
+ //
+ // However, we already requested the full body. This is a problem,
+ // because a body can only be read once. This is why we preemptively
+ // re-populated the request body with the existing data.
+ $request->setBody($requestBody);
+
+ $message = $this->server->xml->parse($requestBody, $request->getUrl(), $documentType);
+
+ switch ($documentType) {
+ // Both the DAV:share-resource and CALENDARSERVER:share requests
+ // behave identically.
+ case '{'.Plugin::NS_CALENDARSERVER.'}share':
+ $sharingPlugin = $this->server->getPlugin('sharing');
+ $sharingPlugin->shareResource($path, $message->sharees);
+
+ $response->setStatus(200);
+ // Adding this because sending a response body may cause issues,
+ // and I wanted some type of indicator the response was handled.
+ $response->setHeader('X-Sabre-Status', 'everything-went-well');
+
+ // Breaking the event chain
+ return false;
+
+ // The invite-reply document is sent when the user replies to an
+ // invitation of a calendar share.
+ case '{'.Plugin::NS_CALENDARSERVER.'}invite-reply':
+ // This only works on the calendar-home-root node.
+ if (!$node instanceof CalendarHome) {
+ return;
+ }
+ $this->server->transactionType = 'post-invite-reply';
+
+ // Getting ACL info
+ $acl = $this->server->getPlugin('acl');
+
+ // If there's no ACL support, we allow everything
+ if ($acl) {
+ $acl->checkPrivileges($path, '{DAV:}write');
+ }
+
+ $url = $node->shareReply(
+ $message->href,
+ $message->status,
+ $message->calendarUri,
+ $message->inReplyTo,
+ $message->summary
+ );
+
+ $response->setStatus(200);
+ // Adding this because sending a response body may cause issues,
+ // and I wanted some type of indicator the response was handled.
+ $response->setHeader('X-Sabre-Status', 'everything-went-well');
+
+ if ($url) {
+ $writer = $this->server->xml->getWriter();
+ $writer->contextUri = $request->getUrl();
+ $writer->openMemory();
+ $writer->startDocument();
+ $writer->startElement('{'.Plugin::NS_CALENDARSERVER.'}shared-as');
+ $writer->write(new LocalHref($url));
+ $writer->endElement();
+ $response->setHeader('Content-Type', 'application/xml');
+ $response->setBody($writer->outputMemory());
+ }
+
+ // Breaking the event chain
+ return false;
+
+ case '{'.Plugin::NS_CALENDARSERVER.'}publish-calendar':
+ // We can only deal with IShareableCalendar objects
+ if (!$node instanceof ISharedCalendar) {
+ return;
+ }
+ $this->server->transactionType = 'post-publish-calendar';
+
+ // Getting ACL info
+ $acl = $this->server->getPlugin('acl');
+
+ // If there's no ACL support, we allow everything
+ if ($acl) {
+ $acl->checkPrivileges($path, '{DAV:}share');
+ }
+
+ $node->setPublishStatus(true);
+
+ // iCloud sends back the 202, so we will too.
+ $response->setStatus(202);
+
+ // Adding this because sending a response body may cause issues,
+ // and I wanted some type of indicator the response was handled.
+ $response->setHeader('X-Sabre-Status', 'everything-went-well');
+
+ // Breaking the event chain
+ return false;
+
+ case '{'.Plugin::NS_CALENDARSERVER.'}unpublish-calendar':
+ // We can only deal with IShareableCalendar objects
+ if (!$node instanceof ISharedCalendar) {
+ return;
+ }
+ $this->server->transactionType = 'post-unpublish-calendar';
+
+ // Getting ACL info
+ $acl = $this->server->getPlugin('acl');
+
+ // If there's no ACL support, we allow everything
+ if ($acl) {
+ $acl->checkPrivileges($path, '{DAV:}share');
+ }
+
+ $node->setPublishStatus(false);
+
+ $response->setStatus(200);
+
+ // Adding this because sending a response body may cause issues,
+ // and I wanted some type of indicator the response was handled.
+ $response->setHeader('X-Sabre-Status', 'everything-went-well');
+
+ // Breaking the event chain
+ return false;
+ }
+ }
+
+ /**
+ * Returns a bunch of meta-data about the plugin.
+ *
+ * Providing this information is optional, and is mainly displayed by the
+ * Browser plugin.
+ *
+ * The description key in the returned array may contain html and will not
+ * be sanitized.
+ *
+ * @return array
+ */
+ public function getPluginInfo()
+ {
+ return [
+ 'name' => $this->getPluginName(),
+ 'description' => 'Adds support for caldav-sharing.',
+ 'link' => 'http://sabre.io/dav/caldav-sharing/',
+ ];
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Subscriptions/ISubscription.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Subscriptions/ISubscription.php
new file mode 100644
index 0000000..e83082c
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Subscriptions/ISubscription.php
@@ -0,0 +1,41 @@
+resourceTypeMapping['Sabre\\CalDAV\\Subscriptions\\ISubscription'] =
+ '{http://calendarserver.org/ns/}subscribed';
+
+ $server->xml->elementMap['{http://calendarserver.org/ns/}source'] =
+ 'Sabre\\DAV\\Xml\\Property\\Href';
+
+ $server->on('propFind', [$this, 'propFind'], 150);
+ }
+
+ /**
+ * This method should return a list of server-features.
+ *
+ * This is for example 'versioning' and is added to the DAV: header
+ * in an OPTIONS response.
+ *
+ * @return array
+ */
+ public function getFeatures()
+ {
+ return ['calendarserver-subscribed'];
+ }
+
+ /**
+ * Triggered after properties have been fetched.
+ */
+ public function propFind(PropFind $propFind, INode $node)
+ {
+ // There's a bunch of properties that must appear as a self-closing
+ // xml-element. This event handler ensures that this will be the case.
+ $props = [
+ '{http://calendarserver.org/ns/}subscribed-strip-alarms',
+ '{http://calendarserver.org/ns/}subscribed-strip-attachments',
+ '{http://calendarserver.org/ns/}subscribed-strip-todos',
+ ];
+
+ foreach ($props as $prop) {
+ if (200 === $propFind->getStatus($prop)) {
+ $propFind->set($prop, '', 200);
+ }
+ }
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using \Sabre\DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ public function getPluginName()
+ {
+ return 'subscriptions';
+ }
+
+ /**
+ * Returns a bunch of meta-data about the plugin.
+ *
+ * Providing this information is optional, and is mainly displayed by the
+ * Browser plugin.
+ *
+ * The description key in the returned array may contain html and will not
+ * be sanitized.
+ *
+ * @return array
+ */
+ public function getPluginInfo()
+ {
+ return [
+ 'name' => $this->getPluginName(),
+ 'description' => 'This plugin allows users to store iCalendar subscriptions in their calendar-home.',
+ 'link' => null,
+ ];
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php
new file mode 100644
index 0000000..8d56e64
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php
@@ -0,0 +1,204 @@
+caldavBackend = $caldavBackend;
+ $this->subscriptionInfo = $subscriptionInfo;
+
+ $required = [
+ 'id',
+ 'uri',
+ 'principaluri',
+ 'source',
+ ];
+
+ foreach ($required as $r) {
+ if (!isset($subscriptionInfo[$r])) {
+ throw new \InvalidArgumentException('The '.$r.' field is required when creating a subscription node');
+ }
+ }
+ }
+
+ /**
+ * Returns the name of the node.
+ *
+ * This is used to generate the url.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->subscriptionInfo['uri'];
+ }
+
+ /**
+ * Returns the last modification time.
+ *
+ * @return int|null
+ */
+ public function getLastModified()
+ {
+ if (isset($this->subscriptionInfo['lastmodified'])) {
+ return $this->subscriptionInfo['lastmodified'];
+ }
+ }
+
+ /**
+ * Deletes the current node.
+ */
+ public function delete()
+ {
+ $this->caldavBackend->deleteSubscription(
+ $this->subscriptionInfo['id']
+ );
+ }
+
+ /**
+ * Returns an array with all the child nodes.
+ *
+ * @return \Sabre\DAV\INode[]
+ */
+ public function getChildren()
+ {
+ return [];
+ }
+
+ /**
+ * Updates properties on this node.
+ *
+ * This method received a PropPatch object, which contains all the
+ * information about the update.
+ *
+ * To update specific properties, call the 'handle' method on this object.
+ * Read the PropPatch documentation for more information.
+ */
+ public function propPatch(PropPatch $propPatch)
+ {
+ return $this->caldavBackend->updateSubscription(
+ $this->subscriptionInfo['id'],
+ $propPatch
+ );
+ }
+
+ /**
+ * Returns a list of properties for this nodes.
+ *
+ * The properties list is a list of propertynames the client requested,
+ * encoded in clark-notation {xmlnamespace}tagname.
+ *
+ * If the array is empty, it means 'all properties' were requested.
+ *
+ * Note that it's fine to liberally give properties back, instead of
+ * conforming to the list of requested properties.
+ * The Server class will filter out the extra.
+ *
+ * @param array $properties
+ *
+ * @return array
+ */
+ public function getProperties($properties)
+ {
+ $r = [];
+
+ foreach ($properties as $prop) {
+ switch ($prop) {
+ case '{http://calendarserver.org/ns/}source':
+ $r[$prop] = new Href($this->subscriptionInfo['source']);
+ break;
+ default:
+ if (array_key_exists($prop, $this->subscriptionInfo)) {
+ $r[$prop] = $this->subscriptionInfo[$prop];
+ }
+ break;
+ }
+ }
+
+ return $r;
+ }
+
+ /**
+ * Returns the owner principal.
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner()
+ {
+ return $this->subscriptionInfo['principaluri'];
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ public function getACL()
+ {
+ return [
+ [
+ 'privilege' => '{DAV:}all',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}all',
+ 'principal' => $this->getOwner().'/calendar-proxy-write',
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getOwner().'/calendar-proxy-read',
+ 'protected' => true,
+ ],
+ ];
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Filter/CalendarData.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Filter/CalendarData.php
new file mode 100644
index 0000000..c9656d8
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Filter/CalendarData.php
@@ -0,0 +1,80 @@
+next();
+ *
+ * $reader->parseInnerTree() will parse the entire sub-tree, and advance to
+ * the next element.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $result = [
+ 'contentType' => $reader->getAttribute('content-type') ?: 'text/calendar',
+ 'version' => $reader->getAttribute('version') ?: '2.0',
+ ];
+
+ $elems = (array) $reader->parseInnerTree();
+ foreach ($elems as $elem) {
+ switch ($elem['name']) {
+ case '{'.Plugin::NS_CALDAV.'}expand':
+ $result['expand'] = [
+ 'start' => isset($elem['attributes']['start']) ? DateTimeParser::parseDateTime($elem['attributes']['start']) : null,
+ 'end' => isset($elem['attributes']['end']) ? DateTimeParser::parseDateTime($elem['attributes']['end']) : null,
+ ];
+
+ if (!$result['expand']['start'] || !$result['expand']['end']) {
+ throw new BadRequest('The "start" and "end" attributes are required when expanding calendar-data');
+ }
+ if ($result['expand']['end'] <= $result['expand']['start']) {
+ throw new BadRequest('The end-date must be larger than the start-date when expanding calendar-data');
+ }
+ break;
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Filter/CompFilter.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Filter/CompFilter.php
new file mode 100644
index 0000000..929000b
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Filter/CompFilter.php
@@ -0,0 +1,94 @@
+next();
+ *
+ * $reader->parseInnerTree() will parse the entire sub-tree, and advance to
+ * the next element.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $result = [
+ 'name' => null,
+ 'is-not-defined' => false,
+ 'comp-filters' => [],
+ 'prop-filters' => [],
+ 'time-range' => false,
+ ];
+
+ $att = $reader->parseAttributes();
+ $result['name'] = $att['name'];
+
+ $elems = $reader->parseInnerTree();
+
+ if (is_array($elems)) {
+ foreach ($elems as $elem) {
+ switch ($elem['name']) {
+ case '{'.Plugin::NS_CALDAV.'}comp-filter':
+ $result['comp-filters'][] = $elem['value'];
+ break;
+ case '{'.Plugin::NS_CALDAV.'}prop-filter':
+ $result['prop-filters'][] = $elem['value'];
+ break;
+ case '{'.Plugin::NS_CALDAV.'}is-not-defined':
+ $result['is-not-defined'] = true;
+ break;
+ case '{'.Plugin::NS_CALDAV.'}time-range':
+ if ('VCALENDAR' === $result['name']) {
+ throw new BadRequest('You cannot add time-range filters on the VCALENDAR component');
+ }
+ $result['time-range'] = [
+ 'start' => isset($elem['attributes']['start']) ? DateTimeParser::parseDateTime($elem['attributes']['start']) : null,
+ 'end' => isset($elem['attributes']['end']) ? DateTimeParser::parseDateTime($elem['attributes']['end']) : null,
+ ];
+ if ($result['time-range']['start'] && $result['time-range']['end'] && $result['time-range']['end'] <= $result['time-range']['start']) {
+ throw new BadRequest('The end-date must be larger than the start-date');
+ }
+ break;
+ }
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Filter/ParamFilter.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Filter/ParamFilter.php
new file mode 100644
index 0000000..1e6dd59
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Filter/ParamFilter.php
@@ -0,0 +1,79 @@
+next();
+ *
+ * $reader->parseInnerTree() will parse the entire sub-tree, and advance to
+ * the next element.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $result = [
+ 'name' => null,
+ 'is-not-defined' => false,
+ 'text-match' => null,
+ ];
+
+ $att = $reader->parseAttributes();
+ $result['name'] = $att['name'];
+
+ $elems = $reader->parseInnerTree();
+
+ if (is_array($elems)) {
+ foreach ($elems as $elem) {
+ switch ($elem['name']) {
+ case '{'.Plugin::NS_CALDAV.'}is-not-defined':
+ $result['is-not-defined'] = true;
+ break;
+ case '{'.Plugin::NS_CALDAV.'}text-match':
+ $result['text-match'] = [
+ 'negate-condition' => isset($elem['attributes']['negate-condition']) && 'yes' === $elem['attributes']['negate-condition'],
+ 'collation' => isset($elem['attributes']['collation']) ? $elem['attributes']['collation'] : 'i;ascii-casemap',
+ 'value' => $elem['value'],
+ ];
+ break;
+ }
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Filter/PropFilter.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Filter/PropFilter.php
new file mode 100644
index 0000000..f1d66cc
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Filter/PropFilter.php
@@ -0,0 +1,95 @@
+next();
+ *
+ * $reader->parseInnerTree() will parse the entire sub-tree, and advance to
+ * the next element.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $result = [
+ 'name' => null,
+ 'is-not-defined' => false,
+ 'param-filters' => [],
+ 'text-match' => null,
+ 'time-range' => [],
+ ];
+
+ $att = $reader->parseAttributes();
+ $result['name'] = $att['name'];
+
+ $elems = $reader->parseInnerTree();
+
+ if (is_array($elems)) {
+ foreach ($elems as $elem) {
+ switch ($elem['name']) {
+ case '{'.Plugin::NS_CALDAV.'}param-filter':
+ $result['param-filters'][] = $elem['value'];
+ break;
+ case '{'.Plugin::NS_CALDAV.'}is-not-defined':
+ $result['is-not-defined'] = true;
+ break;
+ case '{'.Plugin::NS_CALDAV.'}time-range':
+ $result['time-range'] = [
+ 'start' => isset($elem['attributes']['start']) ? DateTimeParser::parseDateTime($elem['attributes']['start']) : null,
+ 'end' => isset($elem['attributes']['end']) ? DateTimeParser::parseDateTime($elem['attributes']['end']) : null,
+ ];
+ if ($result['time-range']['start'] && $result['time-range']['end'] && $result['time-range']['end'] <= $result['time-range']['start']) {
+ throw new BadRequest('The end-date must be larger than the start-date');
+ }
+ break;
+ case '{'.Plugin::NS_CALDAV.'}text-match':
+ $result['text-match'] = [
+ 'negate-condition' => isset($elem['attributes']['negate-condition']) && 'yes' === $elem['attributes']['negate-condition'],
+ 'collation' => isset($elem['attributes']['collation']) ? $elem['attributes']['collation'] : 'i;ascii-casemap',
+ 'value' => $elem['value'],
+ ];
+ break;
+ }
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php
new file mode 100644
index 0000000..2dbb0f4
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php
@@ -0,0 +1,290 @@
+ $value) {
+ if (!property_exists($this, $key)) {
+ throw new \InvalidArgumentException('Unknown option: '.$key);
+ }
+ $this->$key = $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(Writer $writer)
+ {
+ $writer->writeElement('{'.CalDAV\Plugin::NS_CALENDARSERVER.'}invite-notification');
+ }
+
+ /**
+ * This method serializes the entire notification, as it is used in the
+ * response body.
+ */
+ public function xmlSerializeFull(Writer $writer)
+ {
+ $cs = '{'.CalDAV\Plugin::NS_CALENDARSERVER.'}';
+
+ $this->dtStamp->setTimezone(new \DateTimeZone('GMT'));
+ $writer->writeElement($cs.'dtstamp', $this->dtStamp->format('Ymd\\THis\\Z'));
+
+ $writer->startElement($cs.'invite-notification');
+
+ $writer->writeElement($cs.'uid', $this->id);
+ $writer->writeElement('{DAV:}href', $this->href);
+
+ switch ($this->type) {
+ case DAV\Sharing\Plugin::INVITE_ACCEPTED:
+ $writer->writeElement($cs.'invite-accepted');
+ break;
+ case DAV\Sharing\Plugin::INVITE_NORESPONSE:
+ $writer->writeElement($cs.'invite-noresponse');
+ break;
+ }
+
+ $writer->writeElement($cs.'hosturl', [
+ '{DAV:}href' => $writer->contextUri.$this->hostUrl,
+ ]);
+
+ if ($this->summary) {
+ $writer->writeElement($cs.'summary', $this->summary);
+ }
+
+ $writer->startElement($cs.'access');
+ if ($this->readOnly) {
+ $writer->writeElement($cs.'read');
+ } else {
+ $writer->writeElement($cs.'read-write');
+ }
+ $writer->endElement(); // access
+
+ $writer->startElement($cs.'organizer');
+ // If the organizer contains a 'mailto:' part, it means it should be
+ // treated as absolute.
+ if ('mailto:' === strtolower(substr($this->organizer, 0, 7))) {
+ $writer->writeElement('{DAV:}href', $this->organizer);
+ } else {
+ $writer->writeElement('{DAV:}href', $writer->contextUri.$this->organizer);
+ }
+ if ($this->commonName) {
+ $writer->writeElement($cs.'common-name', $this->commonName);
+ }
+ if ($this->firstName) {
+ $writer->writeElement($cs.'first-name', $this->firstName);
+ }
+ if ($this->lastName) {
+ $writer->writeElement($cs.'last-name', $this->lastName);
+ }
+ $writer->endElement(); // organizer
+
+ if ($this->commonName) {
+ $writer->writeElement($cs.'organizer-cn', $this->commonName);
+ }
+ if ($this->firstName) {
+ $writer->writeElement($cs.'organizer-first', $this->firstName);
+ }
+ if ($this->lastName) {
+ $writer->writeElement($cs.'organizer-last', $this->lastName);
+ }
+ if ($this->supportedComponents) {
+ $writer->writeElement('{'.CalDAV\Plugin::NS_CALDAV.'}supported-calendar-component-set', $this->supportedComponents);
+ }
+
+ $writer->endElement(); // invite-notification
+ }
+
+ /**
+ * Returns a unique id for this notification.
+ *
+ * This is just the base url. This should generally be some kind of unique
+ * id.
+ *
+ * @return string
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * Returns the ETag for this notification.
+ *
+ * The ETag must be surrounded by literal double-quotes.
+ *
+ * @return string
+ */
+ public function getETag()
+ {
+ return $this->etag;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php
new file mode 100644
index 0000000..dbdba3b
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php
@@ -0,0 +1,199 @@
+ $value) {
+ if (!property_exists($this, $key)) {
+ throw new \InvalidArgumentException('Unknown option: '.$key);
+ }
+ $this->$key = $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(Writer $writer)
+ {
+ $writer->writeElement('{'.CalDAV\Plugin::NS_CALENDARSERVER.'}invite-reply');
+ }
+
+ /**
+ * This method serializes the entire notification, as it is used in the
+ * response body.
+ */
+ public function xmlSerializeFull(Writer $writer)
+ {
+ $cs = '{'.CalDAV\Plugin::NS_CALENDARSERVER.'}';
+
+ $this->dtStamp->setTimezone(new \DateTimeZone('GMT'));
+ $writer->writeElement($cs.'dtstamp', $this->dtStamp->format('Ymd\\THis\\Z'));
+
+ $writer->startElement($cs.'invite-reply');
+
+ $writer->writeElement($cs.'uid', $this->id);
+ $writer->writeElement($cs.'in-reply-to', $this->inReplyTo);
+ $writer->writeElement('{DAV:}href', $this->href);
+
+ switch ($this->type) {
+ case DAV\Sharing\Plugin::INVITE_ACCEPTED:
+ $writer->writeElement($cs.'invite-accepted');
+ break;
+ case DAV\Sharing\Plugin::INVITE_DECLINED:
+ $writer->writeElement($cs.'invite-declined');
+ break;
+ }
+
+ $writer->writeElement($cs.'hosturl', [
+ '{DAV:}href' => $writer->contextUri.$this->hostUrl,
+ ]);
+
+ if ($this->summary) {
+ $writer->writeElement($cs.'summary', $this->summary);
+ }
+ $writer->endElement(); // invite-reply
+ }
+
+ /**
+ * Returns a unique id for this notification.
+ *
+ * This is just the base url. This should generally be some kind of unique
+ * id.
+ *
+ * @return string
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * Returns the ETag for this notification.
+ *
+ * The ETag must be surrounded by literal double-quotes.
+ *
+ * @return string
+ */
+ public function getETag()
+ {
+ return $this->etag;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Notification/NotificationInterface.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Notification/NotificationInterface.php
new file mode 100644
index 0000000..e1b393f
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Notification/NotificationInterface.php
@@ -0,0 +1,43 @@
+id = $id;
+ $this->type = $type;
+ $this->description = $description;
+ $this->href = $href;
+ $this->etag = $etag;
+ }
+
+ /**
+ * The serialize method is called during xml writing.
+ *
+ * It should use the $writer argument to encode this object into XML.
+ *
+ * Important note: it is not needed to create the parent element. The
+ * parent element is already created, and we only have to worry about
+ * attributes, child elements and text (if any).
+ *
+ * Important note 2: If you are writing any new elements, you are also
+ * responsible for closing them.
+ */
+ public function xmlSerialize(Writer $writer)
+ {
+ switch ($this->type) {
+ case self::TYPE_LOW:
+ $type = 'low';
+ break;
+ case self::TYPE_MEDIUM:
+ $type = 'medium';
+ break;
+ default:
+ case self::TYPE_HIGH:
+ $type = 'high';
+ break;
+ }
+
+ $writer->startElement('{'.Plugin::NS_CALENDARSERVER.'}systemstatus');
+ $writer->writeAttribute('type', $type);
+ $writer->endElement();
+ }
+
+ /**
+ * This method serializes the entire notification, as it is used in the
+ * response body.
+ */
+ public function xmlSerializeFull(Writer $writer)
+ {
+ $cs = '{'.Plugin::NS_CALENDARSERVER.'}';
+ switch ($this->type) {
+ case self::TYPE_LOW:
+ $type = 'low';
+ break;
+ case self::TYPE_MEDIUM:
+ $type = 'medium';
+ break;
+ default:
+ case self::TYPE_HIGH:
+ $type = 'high';
+ break;
+ }
+
+ $writer->startElement($cs.'systemstatus');
+ $writer->writeAttribute('type', $type);
+
+ if ($this->description) {
+ $writer->writeElement($cs.'description', $this->description);
+ }
+ if ($this->href) {
+ $writer->writeElement('{DAV:}href', $this->href);
+ }
+
+ $writer->endElement(); // systemstatus
+ }
+
+ /**
+ * Returns a unique id for this notification.
+ *
+ * This is just the base url. This should generally be some kind of unique
+ * id.
+ *
+ * @return string
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /*
+ * Returns the ETag for this notification.
+ *
+ * The ETag must be surrounded by literal double-quotes.
+ *
+ * @return string
+ */
+ public function getETag()
+ {
+ return $this->etag;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/AllowedSharingModes.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/AllowedSharingModes.php
new file mode 100644
index 0000000..58acb6d
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/AllowedSharingModes.php
@@ -0,0 +1,81 @@
+canBeShared = $canBeShared;
+ $this->canBePublished = $canBePublished;
+ }
+
+ /**
+ * 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)
+ {
+ if ($this->canBeShared) {
+ $writer->writeElement('{'.Plugin::NS_CALENDARSERVER.'}can-be-shared');
+ }
+ if ($this->canBePublished) {
+ $writer->writeElement('{'.Plugin::NS_CALENDARSERVER.'}can-be-published');
+ }
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/EmailAddressSet.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/EmailAddressSet.php
new file mode 100644
index 0000000..84f7ae0
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/EmailAddressSet.php
@@ -0,0 +1,71 @@
+emails = $emails;
+ }
+
+ /**
+ * Returns the email addresses.
+ *
+ * @return array
+ */
+ public function getValue()
+ {
+ return $this->emails;
+ }
+
+ /**
+ * 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)
+ {
+ foreach ($this->emails as $email) {
+ $writer->writeElement('{http://calendarserver.org/ns/}email-address', $email);
+ }
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/Invite.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/Invite.php
new file mode 100644
index 0000000..c389ca8
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/Invite.php
@@ -0,0 +1,120 @@
+sharees = $sharees;
+ }
+
+ /**
+ * Returns the list of users, as it was passed to the constructor.
+ *
+ * @return array
+ */
+ public function getValue()
+ {
+ return $this->sharees;
+ }
+
+ /**
+ * 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)
+ {
+ $cs = '{'.Plugin::NS_CALENDARSERVER.'}';
+
+ foreach ($this->sharees as $sharee) {
+ if (DAV\Sharing\Plugin::ACCESS_SHAREDOWNER === $sharee->access) {
+ $writer->startElement($cs.'organizer');
+ } else {
+ $writer->startElement($cs.'user');
+
+ switch ($sharee->inviteStatus) {
+ case DAV\Sharing\Plugin::INVITE_ACCEPTED:
+ $writer->writeElement($cs.'invite-accepted');
+ break;
+ case DAV\Sharing\Plugin::INVITE_DECLINED:
+ $writer->writeElement($cs.'invite-declined');
+ break;
+ case DAV\Sharing\Plugin::INVITE_NORESPONSE:
+ $writer->writeElement($cs.'invite-noresponse');
+ break;
+ case DAV\Sharing\Plugin::INVITE_INVALID:
+ $writer->writeElement($cs.'invite-invalid');
+ break;
+ }
+
+ $writer->startElement($cs.'access');
+ switch ($sharee->access) {
+ case DAV\Sharing\Plugin::ACCESS_READWRITE:
+ $writer->writeElement($cs.'read-write');
+ break;
+ case DAV\Sharing\Plugin::ACCESS_READ:
+ $writer->writeElement($cs.'read');
+ break;
+ }
+ $writer->endElement(); // access
+ }
+
+ $href = new DAV\Xml\Property\Href($sharee->href);
+ $href->xmlSerialize($writer);
+
+ if (isset($sharee->properties['{DAV:}displayname'])) {
+ $writer->writeElement($cs.'common-name', $sharee->properties['{DAV:}displayname']);
+ }
+ if ($sharee->comment) {
+ $writer->writeElement($cs.'summary', $sharee->comment);
+ }
+ $writer->endElement(); // organizer or user
+ }
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/ScheduleCalendarTransp.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/ScheduleCalendarTransp.php
new file mode 100644
index 0000000..1595220
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/ScheduleCalendarTransp.php
@@ -0,0 +1,124 @@
+value = $value;
+ }
+
+ /**
+ * Returns the current value.
+ *
+ * @return string
+ */
+ public function getValue()
+ {
+ return $this->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(Writer $writer)
+ {
+ switch ($this->value) {
+ case self::TRANSPARENT:
+ $writer->writeElement('{'.Plugin::NS_CALDAV.'}transparent');
+ break;
+ case self::OPAQUE:
+ $writer->writeElement('{'.Plugin::NS_CALDAV.'}opaque');
+ 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.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $elems = Deserializer\enum($reader, Plugin::NS_CALDAV);
+
+ if (in_array('transparent', $elems)) {
+ $value = self::TRANSPARENT;
+ } else {
+ $value = self::OPAQUE;
+ }
+
+ return new self($value);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarComponentSet.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarComponentSet.php
new file mode 100644
index 0000000..d86e7b7
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarComponentSet.php
@@ -0,0 +1,118 @@
+components = $components;
+ }
+
+ /**
+ * Returns the list of supported components.
+ *
+ * @return array
+ */
+ public function getValue()
+ {
+ return $this->components;
+ }
+
+ /**
+ * 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)
+ {
+ foreach ($this->components as $component) {
+ $writer->startElement('{'.Plugin::NS_CALDAV.'}comp');
+ $writer->writeAttributes(['name' => $component]);
+ $writer->endElement();
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $elems = $reader->parseInnerTree();
+
+ $components = [];
+
+ foreach ((array) $elems as $elem) {
+ if ($elem['name'] === '{'.Plugin::NS_CALDAV.'}comp') {
+ $components[] = $elem['attributes']['name'];
+ }
+ }
+
+ if (!$components) {
+ throw new ParseException('supported-calendar-component-set must have at least one CALDAV:comp element');
+ }
+
+ return new self($components);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarData.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarData.php
new file mode 100644
index 0000000..5b08933
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarData.php
@@ -0,0 +1,57 @@
+startElement('{'.Plugin::NS_CALDAV.'}calendar-data');
+ $writer->writeAttributes([
+ 'content-type' => 'text/calendar',
+ 'version' => '2.0',
+ ]);
+ $writer->endElement(); // calendar-data
+ $writer->startElement('{'.Plugin::NS_CALDAV.'}calendar-data');
+ $writer->writeAttributes([
+ 'content-type' => 'application/calendar+json',
+ ]);
+ $writer->endElement(); // calendar-data
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCollationSet.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCollationSet.php
new file mode 100644
index 0000000..c5ffeee
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCollationSet.php
@@ -0,0 +1,54 @@
+writeElement('{'.Plugin::NS_CALDAV.'}supported-collation', $collation);
+ }
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/CalendarMultiGetReport.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/CalendarMultiGetReport.php
new file mode 100644
index 0000000..4771a20
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/CalendarMultiGetReport.php
@@ -0,0 +1,119 @@
+next();
+ *
+ * $reader->parseInnerTree() will parse the entire sub-tree, and advance to
+ * the next element.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $elems = $reader->parseInnerTree([
+ '{urn:ietf:params:xml:ns:caldav}calendar-data' => 'Sabre\\CalDAV\\Xml\\Filter\\CalendarData',
+ '{DAV:}prop' => 'Sabre\\Xml\\Element\\KeyValue',
+ ]);
+
+ $newProps = [
+ 'hrefs' => [],
+ 'properties' => [],
+ ];
+
+ foreach ($elems as $elem) {
+ switch ($elem['name']) {
+ case '{DAV:}prop':
+ $newProps['properties'] = array_keys($elem['value']);
+ if (isset($elem['value']['{'.Plugin::NS_CALDAV.'}calendar-data'])) {
+ $newProps += $elem['value']['{'.Plugin::NS_CALDAV.'}calendar-data'];
+ }
+ break;
+ case '{DAV:}href':
+ $newProps['hrefs'][] = Uri\resolve($reader->contextUri, $elem['value']);
+ break;
+ }
+ }
+
+ $obj = new self();
+ foreach ($newProps as $key => $value) {
+ $obj->$key = $value;
+ }
+
+ return $obj;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/CalendarQueryReport.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/CalendarQueryReport.php
new file mode 100644
index 0000000..5a4df46
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/CalendarQueryReport.php
@@ -0,0 +1,137 @@
+next();
+ *
+ * $reader->parseInnerTree() will parse the entire sub-tree, and advance to
+ * the next element.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $elems = $reader->parseInnerTree([
+ '{urn:ietf:params:xml:ns:caldav}comp-filter' => 'Sabre\\CalDAV\\Xml\\Filter\\CompFilter',
+ '{urn:ietf:params:xml:ns:caldav}prop-filter' => 'Sabre\\CalDAV\\Xml\\Filter\\PropFilter',
+ '{urn:ietf:params:xml:ns:caldav}param-filter' => 'Sabre\\CalDAV\\Xml\\Filter\\ParamFilter',
+ '{urn:ietf:params:xml:ns:caldav}calendar-data' => 'Sabre\\CalDAV\\Xml\\Filter\\CalendarData',
+ '{DAV:}prop' => 'Sabre\\Xml\\Element\\KeyValue',
+ ]);
+
+ $newProps = [
+ 'filters' => null,
+ 'properties' => [],
+ ];
+
+ if (!is_array($elems)) {
+ $elems = [];
+ }
+
+ foreach ($elems as $elem) {
+ switch ($elem['name']) {
+ case '{DAV:}prop':
+ $newProps['properties'] = array_keys($elem['value']);
+ if (isset($elem['value']['{'.Plugin::NS_CALDAV.'}calendar-data'])) {
+ $newProps += $elem['value']['{'.Plugin::NS_CALDAV.'}calendar-data'];
+ }
+ break;
+ case '{'.Plugin::NS_CALDAV.'}filter':
+ foreach ($elem['value'] as $subElem) {
+ if ($subElem['name'] === '{'.Plugin::NS_CALDAV.'}comp-filter') {
+ if (!is_null($newProps['filters'])) {
+ throw new BadRequest('Only one top-level comp-filter may be defined');
+ }
+ $newProps['filters'] = $subElem['value'];
+ }
+ }
+ break;
+ }
+ }
+
+ if (is_null($newProps['filters'])) {
+ throw new BadRequest('The {'.Plugin::NS_CALDAV.'}filter element is required for this request');
+ }
+
+ $obj = new self();
+ foreach ($newProps as $key => $value) {
+ $obj->$key = $value;
+ }
+
+ return $obj;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/FreeBusyQueryReport.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/FreeBusyQueryReport.php
new file mode 100644
index 0000000..17df05a
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/FreeBusyQueryReport.php
@@ -0,0 +1,90 @@
+next();
+ *
+ * $reader->parseInnerTree() will parse the entire sub-tree, and advance to
+ * the next element.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $timeRange = '{'.Plugin::NS_CALDAV.'}time-range';
+
+ $start = null;
+ $end = null;
+
+ foreach ((array) $reader->parseInnerTree([]) as $elem) {
+ if ($elem['name'] !== $timeRange) {
+ continue;
+ }
+
+ $start = empty($elem['attributes']['start']) ?: $elem['attributes']['start'];
+ $end = empty($elem['attributes']['end']) ?: $elem['attributes']['end'];
+ }
+ if (!$start && !$end) {
+ throw new BadRequest('The freebusy report must have a time-range element');
+ }
+ if ($start) {
+ $start = DateTimeParser::parseDateTime($start);
+ }
+ if ($end) {
+ $end = DateTimeParser::parseDateTime($end);
+ }
+ $result = new self();
+ $result->start = $start;
+ $result->end = $end;
+
+ return $result;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php
new file mode 100644
index 0000000..166721e
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php
@@ -0,0 +1,145 @@
+href = $href;
+ $this->calendarUri = $calendarUri;
+ $this->inReplyTo = $inReplyTo;
+ $this->summary = $summary;
+ $this->status = $status;
+ }
+
+ /**
+ * 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.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $elems = KeyValue::xmlDeserialize($reader);
+
+ $href = null;
+ $calendarUri = null;
+ $inReplyTo = null;
+ $summary = null;
+ $status = null;
+
+ foreach ($elems as $name => $value) {
+ switch ($name) {
+ case '{'.Plugin::NS_CALENDARSERVER.'}hosturl':
+ foreach ($value as $bla) {
+ if ('{DAV:}href' === $bla['name']) {
+ $calendarUri = $bla['value'];
+ }
+ }
+ break;
+ case '{'.Plugin::NS_CALENDARSERVER.'}invite-accepted':
+ $status = DAV\Sharing\Plugin::INVITE_ACCEPTED;
+ break;
+ case '{'.Plugin::NS_CALENDARSERVER.'}invite-declined':
+ $status = DAV\Sharing\Plugin::INVITE_DECLINED;
+ break;
+ case '{'.Plugin::NS_CALENDARSERVER.'}in-reply-to':
+ $inReplyTo = $value;
+ break;
+ case '{'.Plugin::NS_CALENDARSERVER.'}summary':
+ $summary = $value;
+ break;
+ case '{DAV:}href':
+ $href = $value;
+ break;
+ }
+ }
+ if (is_null($calendarUri)) {
+ throw new BadRequest('The {http://calendarserver.org/ns/}hosturl/{DAV:}href element must exist');
+ }
+
+ return new self($href, $calendarUri, $inReplyTo, $summary, $status);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/MkCalendar.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/MkCalendar.php
new file mode 100644
index 0000000..b5701e2
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/MkCalendar.php
@@ -0,0 +1,77 @@
+properties;
+ }
+
+ /**
+ * 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.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $self = new self();
+
+ $elementMap = $reader->elementMap;
+ $elementMap['{DAV:}prop'] = 'Sabre\DAV\Xml\Element\Prop';
+ $elementMap['{DAV:}set'] = 'Sabre\Xml\Element\KeyValue';
+ $elems = $reader->parseInnerTree($elementMap);
+
+ foreach ($elems as $elem) {
+ if ('{DAV:}set' === $elem['name']) {
+ $self->properties = array_merge($self->properties, $elem['value']['{DAV:}prop']);
+ }
+ }
+
+ return $self;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/Share.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/Share.php
new file mode 100644
index 0000000..d597b76
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/Share.php
@@ -0,0 +1,107 @@
+sharees = $sharees;
+ }
+
+ /**
+ * 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.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $elems = $reader->parseGetElements([
+ '{'.Plugin::NS_CALENDARSERVER.'}set' => 'Sabre\\Xml\\Element\\KeyValue',
+ '{'.Plugin::NS_CALENDARSERVER.'}remove' => 'Sabre\\Xml\\Element\\KeyValue',
+ ]);
+
+ $sharees = [];
+
+ foreach ($elems as $elem) {
+ switch ($elem['name']) {
+ case '{'.Plugin::NS_CALENDARSERVER.'}set':
+ $sharee = $elem['value'];
+
+ $sumElem = '{'.Plugin::NS_CALENDARSERVER.'}summary';
+ $commonName = '{'.Plugin::NS_CALENDARSERVER.'}common-name';
+
+ $properties = [];
+ if (isset($sharee[$commonName])) {
+ $properties['{DAV:}displayname'] = $sharee[$commonName];
+ }
+
+ $access = array_key_exists('{'.Plugin::NS_CALENDARSERVER.'}read-write', $sharee)
+ ? \Sabre\DAV\Sharing\Plugin::ACCESS_READWRITE
+ : \Sabre\DAV\Sharing\Plugin::ACCESS_READ;
+
+ $sharees[] = new Sharee([
+ 'href' => $sharee['{DAV:}href'],
+ 'properties' => $properties,
+ 'access' => $access,
+ 'comment' => isset($sharee[$sumElem]) ? $sharee[$sumElem] : null,
+ ]);
+ break;
+
+ case '{'.Plugin::NS_CALENDARSERVER.'}remove':
+ $sharees[] = new Sharee([
+ 'href' => $elem['value']['{DAV:}href'],
+ 'access' => \Sabre\DAV\Sharing\Plugin::ACCESS_NOACCESS,
+ ]);
+ break;
+ }
+ }
+
+ return new self($sharees);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/AddressBook.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/AddressBook.php
new file mode 100644
index 0000000..f5744f6
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/AddressBook.php
@@ -0,0 +1,335 @@
+carddavBackend = $carddavBackend;
+ $this->addressBookInfo = $addressBookInfo;
+ }
+
+ /**
+ * Returns the name of the addressbook.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->addressBookInfo['uri'];
+ }
+
+ /**
+ * Returns a card.
+ *
+ * @param string $name
+ *
+ * @return Card
+ */
+ public function getChild($name)
+ {
+ $obj = $this->carddavBackend->getCard($this->addressBookInfo['id'], $name);
+ if (!$obj) {
+ throw new DAV\Exception\NotFound('Card not found');
+ }
+
+ return new Card($this->carddavBackend, $this->addressBookInfo, $obj);
+ }
+
+ /**
+ * Returns the full list of cards.
+ *
+ * @return array
+ */
+ public function getChildren()
+ {
+ $objs = $this->carddavBackend->getCards($this->addressBookInfo['id']);
+ $children = [];
+ foreach ($objs as $obj) {
+ $obj['acl'] = $this->getChildACL();
+ $children[] = new Card($this->carddavBackend, $this->addressBookInfo, $obj);
+ }
+
+ return $children;
+ }
+
+ /**
+ * This method receives a list of paths in it's first argument.
+ * It must return an array with Node objects.
+ *
+ * If any children are not found, you do not have to return them.
+ *
+ * @param string[] $paths
+ *
+ * @return array
+ */
+ public function getMultipleChildren(array $paths)
+ {
+ $objs = $this->carddavBackend->getMultipleCards($this->addressBookInfo['id'], $paths);
+ $children = [];
+ foreach ($objs as $obj) {
+ $obj['acl'] = $this->getChildACL();
+ $children[] = new Card($this->carddavBackend, $this->addressBookInfo, $obj);
+ }
+
+ return $children;
+ }
+
+ /**
+ * Creates a new directory.
+ *
+ * We actually block this, as subdirectories are not allowed in addressbooks.
+ *
+ * @param string $name
+ */
+ public function createDirectory($name)
+ {
+ throw new DAV\Exception\MethodNotAllowed('Creating collections in addressbooks is not allowed');
+ }
+
+ /**
+ * Creates a new file.
+ *
+ * The contents of the new file must be a valid VCARD.
+ *
+ * This method may return an ETag.
+ *
+ * @param string $name
+ * @param resource $data
+ *
+ * @return string|null
+ */
+ public function createFile($name, $data = null)
+ {
+ if (is_resource($data)) {
+ $data = stream_get_contents($data);
+ }
+ // Converting to UTF-8, if needed
+ $data = DAV\StringUtil::ensureUTF8($data);
+
+ return $this->carddavBackend->createCard($this->addressBookInfo['id'], $name, $data);
+ }
+
+ /**
+ * Deletes the entire addressbook.
+ */
+ public function delete()
+ {
+ $this->carddavBackend->deleteAddressBook($this->addressBookInfo['id']);
+ }
+
+ /**
+ * Renames the addressbook.
+ *
+ * @param string $newName
+ */
+ public function setName($newName)
+ {
+ throw new DAV\Exception\MethodNotAllowed('Renaming addressbooks is not yet supported');
+ }
+
+ /**
+ * Returns the last modification date as a unix timestamp.
+ */
+ public function getLastModified()
+ {
+ return null;
+ }
+
+ /**
+ * Updates properties on this node.
+ *
+ * This method received a PropPatch object, which contains all the
+ * information about the update.
+ *
+ * To update specific properties, call the 'handle' method on this object.
+ * Read the PropPatch documentation for more information.
+ */
+ public function propPatch(DAV\PropPatch $propPatch)
+ {
+ return $this->carddavBackend->updateAddressBook($this->addressBookInfo['id'], $propPatch);
+ }
+
+ /**
+ * Returns a list of properties for this nodes.
+ *
+ * The properties list is a list of propertynames the client requested,
+ * encoded in clark-notation {xmlnamespace}tagname
+ *
+ * If the array is empty, it means 'all properties' were requested.
+ *
+ * @param array $properties
+ *
+ * @return array
+ */
+ public function getProperties($properties)
+ {
+ $response = [];
+ foreach ($properties as $propertyName) {
+ if (isset($this->addressBookInfo[$propertyName])) {
+ $response[$propertyName] = $this->addressBookInfo[$propertyName];
+ }
+ }
+
+ return $response;
+ }
+
+ /**
+ * Returns the owner principal.
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner()
+ {
+ return $this->addressBookInfo['principaluri'];
+ }
+
+ /**
+ * This method returns the ACL's for card nodes in this address book.
+ * The result of this method automatically gets passed to the
+ * card nodes in this address book.
+ *
+ * @return array
+ */
+ public function getChildACL()
+ {
+ return [
+ [
+ 'privilege' => '{DAV:}all',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ],
+ ];
+ }
+
+ /**
+ * This method returns the current sync-token for this collection.
+ * This can be any string.
+ *
+ * If null is returned from this function, the plugin assumes there's no
+ * sync information available.
+ *
+ * @return string|null
+ */
+ public function getSyncToken()
+ {
+ if (
+ $this->carddavBackend instanceof Backend\SyncSupport &&
+ isset($this->addressBookInfo['{DAV:}sync-token'])
+ ) {
+ return $this->addressBookInfo['{DAV:}sync-token'];
+ }
+ if (
+ $this->carddavBackend instanceof Backend\SyncSupport &&
+ isset($this->addressBookInfo['{http://sabredav.org/ns}sync-token'])
+ ) {
+ return $this->addressBookInfo['{http://sabredav.org/ns}sync-token'];
+ }
+ }
+
+ /**
+ * The getChanges method returns all the changes that have happened, since
+ * the specified syncToken and the current collection.
+ *
+ * This function should return an array, such as the following:
+ *
+ * [
+ * 'syncToken' => 'The current synctoken',
+ * 'added' => [
+ * 'new.txt',
+ * ],
+ * 'modified' => [
+ * 'modified.txt',
+ * ],
+ * 'deleted' => [
+ * 'foo.php.bak',
+ * 'old.txt'
+ * ]
+ * ];
+ *
+ * The syncToken property should reflect the *current* syncToken of the
+ * collection, as reported getSyncToken(). This is needed here too, to
+ * ensure the operation is atomic.
+ *
+ * If the syncToken is specified as null, this is an initial sync, and all
+ * members should be reported.
+ *
+ * The modified property is an array of nodenames that have changed since
+ * the last token.
+ *
+ * The deleted property is an array with nodenames, that have been deleted
+ * from collection.
+ *
+ * The second argument is basically the 'depth' of the report. If it's 1,
+ * you only have to report changes that happened only directly in immediate
+ * descendants. If it's 2, it should also include changes from the nodes
+ * below the child collections. (grandchildren)
+ *
+ * The third (optional) argument allows a client to specify how many
+ * results should be returned at most. If the limit is not specified, it
+ * should be treated as infinite.
+ *
+ * If the limit (infinite or not) is higher than you're willing to return,
+ * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
+ *
+ * If the syncToken is expired (due to data cleanup) or unknown, you must
+ * return null.
+ *
+ * The limit is 'suggestive'. You are free to ignore it.
+ *
+ * @param string $syncToken
+ * @param int $syncLevel
+ * @param int $limit
+ *
+ * @return array|null
+ */
+ public function getChanges($syncToken, $syncLevel, $limit = null)
+ {
+ if (!$this->carddavBackend instanceof Backend\SyncSupport) {
+ return null;
+ }
+
+ return $this->carddavBackend->getChangesForAddressBook(
+ $this->addressBookInfo['id'],
+ $syncToken,
+ $syncLevel,
+ $limit
+ );
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/AddressBookHome.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/AddressBookHome.php
new file mode 100644
index 0000000..d7365fb
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/AddressBookHome.php
@@ -0,0 +1,178 @@
+carddavBackend = $carddavBackend;
+ $this->principalUri = $principalUri;
+ }
+
+ /**
+ * Returns the name of this object.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ list(, $name) = Uri\split($this->principalUri);
+
+ return $name;
+ }
+
+ /**
+ * Updates the name of this object.
+ *
+ * @param string $name
+ */
+ public function setName($name)
+ {
+ throw new DAV\Exception\MethodNotAllowed();
+ }
+
+ /**
+ * Deletes this object.
+ */
+ public function delete()
+ {
+ throw new DAV\Exception\MethodNotAllowed();
+ }
+
+ /**
+ * Returns the last modification date.
+ *
+ * @return int
+ */
+ public function getLastModified()
+ {
+ return null;
+ }
+
+ /**
+ * Creates a new file under this object.
+ *
+ * This is currently not allowed
+ *
+ * @param string $name
+ * @param resource $data
+ */
+ public function createFile($name, $data = null)
+ {
+ throw new DAV\Exception\MethodNotAllowed('Creating new files in this collection is not supported');
+ }
+
+ /**
+ * Creates a new directory under this object.
+ *
+ * This is currently not allowed.
+ *
+ * @param string $filename
+ */
+ public function createDirectory($filename)
+ {
+ throw new DAV\Exception\MethodNotAllowed('Creating new collections in this collection is not supported');
+ }
+
+ /**
+ * Returns a single addressbook, by name.
+ *
+ * @param string $name
+ *
+ * @todo needs optimizing
+ *
+ * @return AddressBook
+ */
+ public function getChild($name)
+ {
+ foreach ($this->getChildren() as $child) {
+ if ($name == $child->getName()) {
+ return $child;
+ }
+ }
+ throw new DAV\Exception\NotFound('Addressbook with name \''.$name.'\' could not be found');
+ }
+
+ /**
+ * Returns a list of addressbooks.
+ *
+ * @return array
+ */
+ public function getChildren()
+ {
+ $addressbooks = $this->carddavBackend->getAddressBooksForUser($this->principalUri);
+ $objs = [];
+ foreach ($addressbooks as $addressbook) {
+ $objs[] = new AddressBook($this->carddavBackend, $addressbook);
+ }
+
+ return $objs;
+ }
+
+ /**
+ * Creates a new address book.
+ *
+ * @param string $name
+ *
+ * @throws DAV\Exception\InvalidResourceType
+ */
+ public function createExtendedCollection($name, MkCol $mkCol)
+ {
+ if (!$mkCol->hasResourceType('{'.Plugin::NS_CARDDAV.'}addressbook')) {
+ throw new DAV\Exception\InvalidResourceType('Unknown resourceType for this collection');
+ }
+ $properties = $mkCol->getRemainingValues();
+ $mkCol->setRemainingResultCode(201);
+ $this->carddavBackend->createAddressBook($this->principalUri, $name, $properties);
+ }
+
+ /**
+ * Returns the owner principal.
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner()
+ {
+ return $this->principalUri;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/AddressBookRoot.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/AddressBookRoot.php
new file mode 100644
index 0000000..ee1721a
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/AddressBookRoot.php
@@ -0,0 +1,75 @@
+carddavBackend = $carddavBackend;
+ parent::__construct($principalBackend, $principalPrefix);
+ }
+
+ /**
+ * Returns the name of the node.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return Plugin::ADDRESSBOOK_ROOT;
+ }
+
+ /**
+ * This method returns a node for a principal.
+ *
+ * The passed array contains principal information, and is guaranteed to
+ * at least contain a uri item. Other properties may or may not be
+ * supplied by the authentication backend.
+ *
+ * @return \Sabre\DAV\INode
+ */
+ public function getChildForPrincipal(array $principal)
+ {
+ return new AddressBookHome($this->carddavBackend, $principal['uri']);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/Backend/AbstractBackend.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/Backend/AbstractBackend.php
new file mode 100644
index 0000000..a900c62
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/Backend/AbstractBackend.php
@@ -0,0 +1,38 @@
+getCard($addressBookId, $uri);
+ }, $uris);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/Backend/BackendInterface.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/Backend/BackendInterface.php
new file mode 100644
index 0000000..f9955ac
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/Backend/BackendInterface.php
@@ -0,0 +1,194 @@
+pdo = $pdo;
+ }
+
+ /**
+ * Returns the list of addressbooks for a specific user.
+ *
+ * @param string $principalUri
+ *
+ * @return array
+ */
+ public function getAddressBooksForUser($principalUri)
+ {
+ $stmt = $this->pdo->prepare('SELECT id, uri, displayname, principaluri, description, synctoken FROM '.$this->addressBooksTableName.' WHERE principaluri = ?');
+ $stmt->execute([$principalUri]);
+
+ $addressBooks = [];
+
+ foreach ($stmt->fetchAll() as $row) {
+ $addressBooks[] = [
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ 'principaluri' => $row['principaluri'],
+ '{DAV:}displayname' => $row['displayname'],
+ '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description' => $row['description'],
+ '{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
+ '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
+ ];
+ }
+
+ return $addressBooks;
+ }
+
+ /**
+ * Updates properties for an address book.
+ *
+ * The list of mutations is stored in a Sabre\DAV\PropPatch object.
+ * To do the actual updates, you must tell this object which properties
+ * you're going to process with the handle() method.
+ *
+ * Calling the handle method is like telling the PropPatch object "I
+ * promise I can handle updating this property".
+ *
+ * Read the PropPatch documentation for more info and examples.
+ *
+ * @param string $addressBookId
+ */
+ public function updateAddressBook($addressBookId, PropPatch $propPatch)
+ {
+ $supportedProperties = [
+ '{DAV:}displayname',
+ '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description',
+ ];
+
+ $propPatch->handle($supportedProperties, function ($mutations) use ($addressBookId) {
+ $updates = [];
+ foreach ($mutations as $property => $newValue) {
+ switch ($property) {
+ case '{DAV:}displayname':
+ $updates['displayname'] = $newValue;
+ break;
+ case '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description':
+ $updates['description'] = $newValue;
+ break;
+ }
+ }
+ $query = 'UPDATE '.$this->addressBooksTableName.' SET ';
+ $first = true;
+ foreach ($updates as $key => $value) {
+ if ($first) {
+ $first = false;
+ } else {
+ $query .= ', ';
+ }
+ $query .= ' '.$key.' = :'.$key.' ';
+ }
+ $query .= ' WHERE id = :addressbookid';
+
+ $stmt = $this->pdo->prepare($query);
+ $updates['addressbookid'] = $addressBookId;
+
+ $stmt->execute($updates);
+
+ $this->addChange($addressBookId, '', 2);
+
+ return true;
+ });
+ }
+
+ /**
+ * Creates a new address book.
+ *
+ * @param string $principalUri
+ * @param string $url just the 'basename' of the url
+ *
+ * @return int Last insert id
+ */
+ public function createAddressBook($principalUri, $url, array $properties)
+ {
+ $values = [
+ 'displayname' => null,
+ 'description' => null,
+ 'principaluri' => $principalUri,
+ 'uri' => $url,
+ ];
+
+ foreach ($properties as $property => $newValue) {
+ switch ($property) {
+ case '{DAV:}displayname':
+ $values['displayname'] = $newValue;
+ break;
+ case '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description':
+ $values['description'] = $newValue;
+ break;
+ default:
+ throw new DAV\Exception\BadRequest('Unknown property: '.$property);
+ }
+ }
+
+ $query = 'INSERT INTO '.$this->addressBooksTableName.' (uri, displayname, description, principaluri, synctoken) VALUES (:uri, :displayname, :description, :principaluri, 1)';
+ $stmt = $this->pdo->prepare($query);
+ $stmt->execute($values);
+
+ return $this->pdo->lastInsertId(
+ $this->addressBooksTableName.'_id_seq'
+ );
+ }
+
+ /**
+ * Deletes an entire addressbook and all its contents.
+ *
+ * @param int $addressBookId
+ */
+ public function deleteAddressBook($addressBookId)
+ {
+ $stmt = $this->pdo->prepare('DELETE FROM '.$this->cardsTableName.' WHERE addressbookid = ?');
+ $stmt->execute([$addressBookId]);
+
+ $stmt = $this->pdo->prepare('DELETE FROM '.$this->addressBooksTableName.' WHERE id = ?');
+ $stmt->execute([$addressBookId]);
+
+ $stmt = $this->pdo->prepare('DELETE FROM '.$this->addressBookChangesTableName.' WHERE addressbookid = ?');
+ $stmt->execute([$addressBookId]);
+ }
+
+ /**
+ * Returns all cards for a specific addressbook id.
+ *
+ * This method should return the following properties for each card:
+ * * carddata - raw vcard data
+ * * uri - Some unique url
+ * * lastmodified - A unix timestamp
+ *
+ * It's recommended to also return the following properties:
+ * * etag - A unique etag. This must change every time the card changes.
+ * * size - The size of the card in bytes.
+ *
+ * If these last two properties are provided, less time will be spent
+ * calculating them. If they are specified, you can also omit carddata.
+ * This may speed up certain requests, especially with large cards.
+ *
+ * @param mixed $addressbookId
+ *
+ * @return array
+ */
+ public function getCards($addressbookId)
+ {
+ $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, size FROM '.$this->cardsTableName.' WHERE addressbookid = ?');
+ $stmt->execute([$addressbookId]);
+
+ $result = [];
+ while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $row['etag'] = '"'.$row['etag'].'"';
+ $row['lastmodified'] = (int) $row['lastmodified'];
+ $result[] = $row;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns a specific card.
+ *
+ * The same set of properties must be returned as with getCards. The only
+ * exception is that 'carddata' is absolutely required.
+ *
+ * If the card does not exist, you must return false.
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ *
+ * @return array
+ */
+ public function getCard($addressBookId, $cardUri)
+ {
+ $stmt = $this->pdo->prepare('SELECT id, carddata, uri, lastmodified, etag, size FROM '.$this->cardsTableName.' WHERE addressbookid = ? AND uri = ? LIMIT 1');
+ $stmt->execute([$addressBookId, $cardUri]);
+
+ $result = $stmt->fetch(\PDO::FETCH_ASSOC);
+
+ if (!$result) {
+ return false;
+ }
+
+ $result['etag'] = '"'.$result['etag'].'"';
+ $result['lastmodified'] = (int) $result['lastmodified'];
+
+ return $result;
+ }
+
+ /**
+ * Returns a list of cards.
+ *
+ * This method should work identical to getCard, but instead return all the
+ * cards in the list as an array.
+ *
+ * If the backend supports this, it may allow for some speed-ups.
+ *
+ * @param mixed $addressBookId
+ *
+ * @return array
+ */
+ public function getMultipleCards($addressBookId, array $uris)
+ {
+ $query = 'SELECT id, uri, lastmodified, etag, size, carddata FROM '.$this->cardsTableName.' WHERE addressbookid = ? AND uri IN (';
+ // Inserting a whole bunch of question marks
+ $query .= implode(',', array_fill(0, count($uris), '?'));
+ $query .= ')';
+
+ $stmt = $this->pdo->prepare($query);
+ $stmt->execute(array_merge([$addressBookId], $uris));
+ $result = [];
+ while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $row['etag'] = '"'.$row['etag'].'"';
+ $row['lastmodified'] = (int) $row['lastmodified'];
+ $result[] = $row;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Creates a new card.
+ *
+ * The addressbook id will be passed as the first argument. This is the
+ * same id as it is returned from the getAddressBooksForUser method.
+ *
+ * The cardUri is a base uri, and doesn't include the full path. The
+ * cardData argument is the vcard body, and is passed as a string.
+ *
+ * It is possible to return an ETag from this method. This ETag is for the
+ * newly created resource, and must be enclosed with double quotes (that
+ * is, the string itself must contain the double quotes).
+ *
+ * You should only return the ETag if you store the carddata as-is. If a
+ * subsequent GET request on the same card does not have the same body,
+ * byte-by-byte and you did return an ETag here, clients tend to get
+ * confused.
+ *
+ * If you don't return an ETag, you can just return null.
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ * @param string $cardData
+ *
+ * @return string|null
+ */
+ public function createCard($addressBookId, $cardUri, $cardData)
+ {
+ $stmt = $this->pdo->prepare('INSERT INTO '.$this->cardsTableName.' (carddata, uri, lastmodified, addressbookid, size, etag) VALUES (?, ?, ?, ?, ?, ?)');
+
+ $etag = md5($cardData);
+
+ $stmt->execute([
+ $cardData,
+ $cardUri,
+ time(),
+ $addressBookId,
+ strlen($cardData),
+ $etag,
+ ]);
+
+ $this->addChange($addressBookId, $cardUri, 1);
+
+ return '"'.$etag.'"';
+ }
+
+ /**
+ * Updates a card.
+ *
+ * The addressbook id will be passed as the first argument. This is the
+ * same id as it is returned from the getAddressBooksForUser method.
+ *
+ * The cardUri is a base uri, and doesn't include the full path. The
+ * cardData argument is the vcard body, and is passed as a string.
+ *
+ * It is possible to return an ETag from this method. This ETag should
+ * match that of the updated resource, and must be enclosed with double
+ * quotes (that is: the string itself must contain the actual quotes).
+ *
+ * You should only return the ETag if you store the carddata as-is. If a
+ * subsequent GET request on the same card does not have the same body,
+ * byte-by-byte and you did return an ETag here, clients tend to get
+ * confused.
+ *
+ * If you don't return an ETag, you can just return null.
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ * @param string $cardData
+ *
+ * @return string|null
+ */
+ public function updateCard($addressBookId, $cardUri, $cardData)
+ {
+ $stmt = $this->pdo->prepare('UPDATE '.$this->cardsTableName.' SET carddata = ?, lastmodified = ?, size = ?, etag = ? WHERE uri = ? AND addressbookid =?');
+
+ $etag = md5($cardData);
+ $stmt->execute([
+ $cardData,
+ time(),
+ strlen($cardData),
+ $etag,
+ $cardUri,
+ $addressBookId,
+ ]);
+
+ $this->addChange($addressBookId, $cardUri, 2);
+
+ return '"'.$etag.'"';
+ }
+
+ /**
+ * Deletes a card.
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ *
+ * @return bool
+ */
+ public function deleteCard($addressBookId, $cardUri)
+ {
+ $stmt = $this->pdo->prepare('DELETE FROM '.$this->cardsTableName.' WHERE addressbookid = ? AND uri = ?');
+ $stmt->execute([$addressBookId, $cardUri]);
+
+ $this->addChange($addressBookId, $cardUri, 3);
+
+ return 1 === $stmt->rowCount();
+ }
+
+ /**
+ * The getChanges method returns all the changes that have happened, since
+ * the specified syncToken in the specified address book.
+ *
+ * This function should return an array, such as the following:
+ *
+ * [
+ * 'syncToken' => 'The current synctoken',
+ * 'added' => [
+ * 'new.txt',
+ * ],
+ * 'modified' => [
+ * 'updated.txt',
+ * ],
+ * 'deleted' => [
+ * 'foo.php.bak',
+ * 'old.txt'
+ * ]
+ * ];
+ *
+ * The returned syncToken property should reflect the *current* syncToken
+ * of the addressbook, as reported in the {http://sabredav.org/ns}sync-token
+ * property. This is needed here too, to ensure the operation is atomic.
+ *
+ * If the $syncToken argument is specified as null, this is an initial
+ * sync, and all members should be reported.
+ *
+ * The modified property is an array of nodenames that have changed since
+ * the last token.
+ *
+ * The deleted property is an array with nodenames, that have been deleted
+ * from collection.
+ *
+ * The $syncLevel argument is basically the 'depth' of the report. If it's
+ * 1, you only have to report changes that happened only directly in
+ * immediate descendants. If it's 2, it should also include changes from
+ * the nodes below the child collections. (grandchildren)
+ *
+ * The $limit argument allows a client to specify how many results should
+ * be returned at most. If the limit is not specified, it should be treated
+ * as infinite.
+ *
+ * If the limit (infinite or not) is higher than you're willing to return,
+ * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
+ *
+ * If the syncToken is expired (due to data cleanup) or unknown, you must
+ * return null.
+ *
+ * The limit is 'suggestive'. You are free to ignore it.
+ *
+ * @param string $addressBookId
+ * @param string $syncToken
+ * @param int $syncLevel
+ * @param int $limit
+ *
+ * @return array|null
+ */
+ public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null)
+ {
+ // Current synctoken
+ $stmt = $this->pdo->prepare('SELECT synctoken FROM '.$this->addressBooksTableName.' WHERE id = ?');
+ $stmt->execute([$addressBookId]);
+ $currentToken = $stmt->fetchColumn(0);
+
+ if (is_null($currentToken)) {
+ return null;
+ }
+
+ $result = [
+ 'syncToken' => $currentToken,
+ 'added' => [],
+ 'modified' => [],
+ 'deleted' => [],
+ ];
+
+ if ($syncToken) {
+ $query = 'SELECT uri, operation FROM '.$this->addressBookChangesTableName.' WHERE synctoken >= ? AND synctoken < ? AND addressbookid = ? ORDER BY synctoken';
+ if ($limit > 0) {
+ $query .= ' LIMIT '.(int) $limit;
+ }
+
+ // Fetching all changes
+ $stmt = $this->pdo->prepare($query);
+ $stmt->execute([$syncToken, $currentToken, $addressBookId]);
+
+ $changes = [];
+
+ // This loop ensures that any duplicates are overwritten, only the
+ // last change on a node is relevant.
+ while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $changes[$row['uri']] = $row['operation'];
+ }
+
+ foreach ($changes as $uri => $operation) {
+ switch ($operation) {
+ case 1:
+ $result['added'][] = $uri;
+ break;
+ case 2:
+ $result['modified'][] = $uri;
+ break;
+ case 3:
+ $result['deleted'][] = $uri;
+ break;
+ }
+ }
+ } else {
+ // No synctoken supplied, this is the initial sync.
+ $query = 'SELECT uri FROM '.$this->cardsTableName.' WHERE addressbookid = ?';
+ $stmt = $this->pdo->prepare($query);
+ $stmt->execute([$addressBookId]);
+
+ $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Adds a change record to the addressbookchanges table.
+ *
+ * @param mixed $addressBookId
+ * @param string $objectUri
+ * @param int $operation 1 = add, 2 = modify, 3 = delete
+ */
+ protected function addChange($addressBookId, $objectUri, $operation)
+ {
+ $stmt = $this->pdo->prepare('INSERT INTO '.$this->addressBookChangesTableName.' (uri, synctoken, addressbookid, operation) SELECT ?, synctoken, ?, ? FROM '.$this->addressBooksTableName.' WHERE id = ?');
+ $stmt->execute([
+ $objectUri,
+ $addressBookId,
+ $operation,
+ $addressBookId,
+ ]);
+ $stmt = $this->pdo->prepare('UPDATE '.$this->addressBooksTableName.' SET synctoken = synctoken + 1 WHERE id = ?');
+ $stmt->execute([
+ $addressBookId,
+ ]);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/Backend/SyncSupport.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/Backend/SyncSupport.php
new file mode 100644
index 0000000..6aaad14
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/Backend/SyncSupport.php
@@ -0,0 +1,83 @@
+ 'The current synctoken',
+ * 'added' => [
+ * 'new.txt',
+ * ],
+ * 'modified' => [
+ * 'modified.txt',
+ * ],
+ * 'deleted' => [
+ * 'foo.php.bak',
+ * 'old.txt'
+ * ]
+ * ];
+ *
+ * The returned syncToken property should reflect the *current* syncToken
+ * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
+ * property. This is needed here too, to ensure the operation is atomic.
+ *
+ * If the $syncToken argument is specified as null, this is an initial
+ * sync, and all members should be reported.
+ *
+ * The modified property is an array of nodenames that have changed since
+ * the last token.
+ *
+ * The deleted property is an array with nodenames, that have been deleted
+ * from collection.
+ *
+ * The $syncLevel argument is basically the 'depth' of the report. If it's
+ * 1, you only have to report changes that happened only directly in
+ * immediate descendants. If it's 2, it should also include changes from
+ * the nodes below the child collections. (grandchildren)
+ *
+ * The $limit argument allows a client to specify how many results should
+ * be returned at most. If the limit is not specified, it should be treated
+ * as infinite.
+ *
+ * If the limit (infinite or not) is higher than you're willing to return,
+ * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
+ *
+ * If the syncToken is expired (due to data cleanup) or unknown, you must
+ * return null.
+ *
+ * The limit is 'suggestive'. You are free to ignore it.
+ *
+ * @param string $addressBookId
+ * @param string $syncToken
+ * @param int $syncLevel
+ * @param int $limit
+ *
+ * @return array|null
+ */
+ public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null);
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/Card.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/Card.php
new file mode 100644
index 0000000..c9cd2bb
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/Card.php
@@ -0,0 +1,202 @@
+carddavBackend = $carddavBackend;
+ $this->addressBookInfo = $addressBookInfo;
+ $this->cardData = $cardData;
+ }
+
+ /**
+ * Returns the uri for this object.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->cardData['uri'];
+ }
+
+ /**
+ * Returns the VCard-formatted object.
+ *
+ * @return string
+ */
+ public function get()
+ {
+ // Pre-populating 'carddata' is optional. If we don't yet have it
+ // already, we fetch it from the backend.
+ if (!isset($this->cardData['carddata'])) {
+ $this->cardData = $this->carddavBackend->getCard($this->addressBookInfo['id'], $this->cardData['uri']);
+ }
+
+ return $this->cardData['carddata'];
+ }
+
+ /**
+ * Updates the VCard-formatted object.
+ *
+ * @param string $cardData
+ *
+ * @return string|null
+ */
+ public function put($cardData)
+ {
+ if (is_resource($cardData)) {
+ $cardData = stream_get_contents($cardData);
+ }
+
+ // Converting to UTF-8, if needed
+ $cardData = DAV\StringUtil::ensureUTF8($cardData);
+
+ $etag = $this->carddavBackend->updateCard($this->addressBookInfo['id'], $this->cardData['uri'], $cardData);
+ $this->cardData['carddata'] = $cardData;
+ $this->cardData['etag'] = $etag;
+
+ return $etag;
+ }
+
+ /**
+ * Deletes the card.
+ */
+ public function delete()
+ {
+ $this->carddavBackend->deleteCard($this->addressBookInfo['id'], $this->cardData['uri']);
+ }
+
+ /**
+ * Returns the mime content-type.
+ *
+ * @return string
+ */
+ public function getContentType()
+ {
+ return 'text/vcard; charset=utf-8';
+ }
+
+ /**
+ * Returns an ETag for this object.
+ *
+ * @return string
+ */
+ public function getETag()
+ {
+ if (isset($this->cardData['etag'])) {
+ return $this->cardData['etag'];
+ } else {
+ $data = $this->get();
+ if (is_string($data)) {
+ return '"'.md5($data).'"';
+ } else {
+ // We refuse to calculate the md5 if it's a stream.
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Returns the last modification date as a unix timestamp.
+ *
+ * @return int
+ */
+ public function getLastModified()
+ {
+ return isset($this->cardData['lastmodified']) ? $this->cardData['lastmodified'] : null;
+ }
+
+ /**
+ * Returns the size of this object in bytes.
+ *
+ * @return int
+ */
+ public function getSize()
+ {
+ if (array_key_exists('size', $this->cardData)) {
+ return $this->cardData['size'];
+ } else {
+ return strlen($this->get());
+ }
+ }
+
+ /**
+ * Returns the owner principal.
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner()
+ {
+ return $this->addressBookInfo['principaluri'];
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ public function getACL()
+ {
+ // An alternative acl may be specified through the cardData array.
+ if (isset($this->cardData['acl'])) {
+ return $this->cardData['acl'];
+ }
+
+ return [
+ [
+ 'privilege' => '{DAV:}all',
+ 'principal' => $this->addressBookInfo['principaluri'],
+ 'protected' => true,
+ ],
+ ];
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/IAddressBook.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/IAddressBook.php
new file mode 100644
index 0000000..3f489f4
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/IAddressBook.php
@@ -0,0 +1,20 @@
+on('propFind', [$this, 'propFindEarly']);
+ $server->on('propFind', [$this, 'propFindLate'], 150);
+ $server->on('report', [$this, 'report']);
+ $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']);
+ $server->on('beforeWriteContent', [$this, 'beforeWriteContent']);
+ $server->on('beforeCreateFile', [$this, 'beforeCreateFile']);
+ $server->on('afterMethod:GET', [$this, 'httpAfterGet']);
+
+ $server->xml->namespaceMap[self::NS_CARDDAV] = 'card';
+
+ $server->xml->elementMap['{'.self::NS_CARDDAV.'}addressbook-query'] = 'Sabre\\CardDAV\\Xml\\Request\\AddressBookQueryReport';
+ $server->xml->elementMap['{'.self::NS_CARDDAV.'}addressbook-multiget'] = 'Sabre\\CardDAV\\Xml\\Request\\AddressBookMultiGetReport';
+
+ /* Mapping Interfaces to {DAV:}resourcetype values */
+ $server->resourceTypeMapping['Sabre\\CardDAV\\IAddressBook'] = '{'.self::NS_CARDDAV.'}addressbook';
+ $server->resourceTypeMapping['Sabre\\CardDAV\\IDirectory'] = '{'.self::NS_CARDDAV.'}directory';
+
+ /* Adding properties that may never be changed */
+ $server->protectedProperties[] = '{'.self::NS_CARDDAV.'}supported-address-data';
+ $server->protectedProperties[] = '{'.self::NS_CARDDAV.'}max-resource-size';
+ $server->protectedProperties[] = '{'.self::NS_CARDDAV.'}addressbook-home-set';
+ $server->protectedProperties[] = '{'.self::NS_CARDDAV.'}supported-collation-set';
+
+ $server->xml->elementMap['{http://calendarserver.org/ns/}me-card'] = 'Sabre\\DAV\\Xml\\Property\\Href';
+
+ $this->server = $server;
+ }
+
+ /**
+ * Returns a list of supported features.
+ *
+ * This is used in the DAV: header in the OPTIONS and PROPFIND requests.
+ *
+ * @return array
+ */
+ public function getFeatures()
+ {
+ return ['addressbook'];
+ }
+
+ /**
+ * Returns a list of reports this plugin supports.
+ *
+ * This will be used in the {DAV:}supported-report-set property.
+ * Note that you still need to subscribe to the 'report' event to actually
+ * implement them
+ *
+ * @param string $uri
+ *
+ * @return array
+ */
+ public function getSupportedReportSet($uri)
+ {
+ $node = $this->server->tree->getNodeForPath($uri);
+ if ($node instanceof IAddressBook || $node instanceof ICard) {
+ return [
+ '{'.self::NS_CARDDAV.'}addressbook-multiget',
+ '{'.self::NS_CARDDAV.'}addressbook-query',
+ ];
+ }
+
+ return [];
+ }
+
+ /**
+ * Adds all CardDAV-specific properties.
+ */
+ public function propFindEarly(DAV\PropFind $propFind, DAV\INode $node)
+ {
+ $ns = '{'.self::NS_CARDDAV.'}';
+
+ if ($node instanceof IAddressBook) {
+ $propFind->handle($ns.'max-resource-size', $this->maxResourceSize);
+ $propFind->handle($ns.'supported-address-data', function () {
+ return new Xml\Property\SupportedAddressData();
+ });
+ $propFind->handle($ns.'supported-collation-set', function () {
+ return new Xml\Property\SupportedCollationSet();
+ });
+ }
+ if ($node instanceof DAVACL\IPrincipal) {
+ $path = $propFind->getPath();
+
+ $propFind->handle('{'.self::NS_CARDDAV.'}addressbook-home-set', function () use ($path) {
+ return new LocalHref($this->getAddressBookHomeForPrincipal($path).'/');
+ });
+
+ if ($this->directories) {
+ $propFind->handle('{'.self::NS_CARDDAV.'}directory-gateway', function () {
+ return new LocalHref($this->directories);
+ });
+ }
+ }
+
+ if ($node instanceof ICard) {
+ // The address-data property is not supposed to be a 'real'
+ // property, but in large chunks of the spec it does act as such.
+ // Therefore we simply expose it as a property.
+ $propFind->handle('{'.self::NS_CARDDAV.'}address-data', function () use ($node) {
+ $val = $node->get();
+ if (is_resource($val)) {
+ $val = stream_get_contents($val);
+ }
+
+ return $val;
+ });
+ }
+ }
+
+ /**
+ * This functions handles REPORT requests specific to CardDAV.
+ *
+ * @param string $reportName
+ * @param \DOMNode $dom
+ * @param mixed $path
+ *
+ * @return bool
+ */
+ public function report($reportName, $dom, $path)
+ {
+ switch ($reportName) {
+ case '{'.self::NS_CARDDAV.'}addressbook-multiget':
+ $this->server->transactionType = 'report-addressbook-multiget';
+ $this->addressbookMultiGetReport($dom);
+
+ return false;
+ case '{'.self::NS_CARDDAV.'}addressbook-query':
+ $this->server->transactionType = 'report-addressbook-query';
+ $this->addressBookQueryReport($dom);
+
+ return false;
+ default:
+ return;
+ }
+ }
+
+ /**
+ * Returns the addressbook home for a given principal.
+ *
+ * @param string $principal
+ *
+ * @return string
+ */
+ protected function getAddressbookHomeForPrincipal($principal)
+ {
+ list(, $principalId) = Uri\split($principal);
+
+ return self::ADDRESSBOOK_ROOT.'/'.$principalId;
+ }
+
+ /**
+ * This function handles the addressbook-multiget REPORT.
+ *
+ * This report is used by the client to fetch the content of a series
+ * of urls. Effectively avoiding a lot of redundant requests.
+ *
+ * @param Xml\Request\AddressBookMultiGetReport $report
+ */
+ public function addressbookMultiGetReport($report)
+ {
+ $contentType = $report->contentType;
+ $version = $report->version;
+ if ($version) {
+ $contentType .= '; version='.$version;
+ }
+
+ $vcardType = $this->negotiateVCard(
+ $contentType
+ );
+
+ $propertyList = [];
+ $paths = array_map(
+ [$this->server, 'calculateUri'],
+ $report->hrefs
+ );
+ foreach ($this->server->getPropertiesForMultiplePaths($paths, $report->properties) as $props) {
+ if (isset($props['200']['{'.self::NS_CARDDAV.'}address-data'])) {
+ $props['200']['{'.self::NS_CARDDAV.'}address-data'] = $this->convertVCard(
+ $props[200]['{'.self::NS_CARDDAV.'}address-data'],
+ $vcardType
+ );
+ }
+ $propertyList[] = $props;
+ }
+
+ $prefer = $this->server->getHTTPPrefer();
+
+ $this->server->httpResponse->setStatus(207);
+ $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
+ $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer');
+ $this->server->httpResponse->setBody($this->server->generateMultiStatus($propertyList, 'minimal' === $prefer['return']));
+ }
+
+ /**
+ * This method is triggered before a file gets updated with new content.
+ *
+ * This plugin uses this method to ensure that Card nodes receive valid
+ * vcard data.
+ *
+ * @param string $path
+ * @param resource $data
+ * @param bool $modified should be set to true, if this event handler
+ * changed &$data
+ */
+ public function beforeWriteContent($path, DAV\IFile $node, &$data, &$modified)
+ {
+ if (!$node instanceof ICard) {
+ return;
+ }
+
+ $this->validateVCard($data, $modified);
+ }
+
+ /**
+ * This method is triggered before a new file is created.
+ *
+ * This plugin uses this method to ensure that Card nodes receive valid
+ * vcard data.
+ *
+ * @param string $path
+ * @param resource $data
+ * @param bool $modified should be set to true, if this event handler
+ * changed &$data
+ */
+ public function beforeCreateFile($path, &$data, DAV\ICollection $parentNode, &$modified)
+ {
+ if (!$parentNode instanceof IAddressBook) {
+ return;
+ }
+
+ $this->validateVCard($data, $modified);
+ }
+
+ /**
+ * Checks if the submitted iCalendar data is in fact, valid.
+ *
+ * An exception is thrown if it's not.
+ *
+ * @param resource|string $data
+ * @param bool $modified should be set to true, if this event handler
+ * changed &$data
+ */
+ protected function validateVCard(&$data, &$modified)
+ {
+ // If it's a stream, we convert it to a string first.
+ if (is_resource($data)) {
+ $data = stream_get_contents($data);
+ }
+
+ $before = $data;
+
+ try {
+ // If the data starts with a [, we can reasonably assume we're dealing
+ // with a jCal object.
+ if ('[' === substr($data, 0, 1)) {
+ $vobj = VObject\Reader::readJson($data);
+
+ // Converting $data back to iCalendar, as that's what we
+ // technically support everywhere.
+ $data = $vobj->serialize();
+ $modified = true;
+ } else {
+ $vobj = VObject\Reader::read($data);
+ }
+ } catch (VObject\ParseException $e) {
+ throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid vCard or jCard data. Parse error: '.$e->getMessage());
+ }
+
+ if ('VCARD' !== $vobj->name) {
+ throw new DAV\Exception\UnsupportedMediaType('This collection can only support vcard objects.');
+ }
+
+ $options = VObject\Node::PROFILE_CARDDAV;
+ $prefer = $this->server->getHTTPPrefer();
+
+ if ('strict' !== $prefer['handling']) {
+ $options |= VObject\Node::REPAIR;
+ }
+
+ $messages = $vobj->validate($options);
+
+ $highestLevel = 0;
+ $warningMessage = null;
+
+ // $messages contains a list of problems with the vcard, along with
+ // their severity.
+ foreach ($messages as $message) {
+ if ($message['level'] > $highestLevel) {
+ // Recording the highest reported error level.
+ $highestLevel = $message['level'];
+ $warningMessage = $message['message'];
+ }
+
+ switch ($message['level']) {
+ case 1:
+ // Level 1 means that there was a problem, but it was repaired.
+ $modified = true;
+ break;
+ case 2:
+ // Level 2 means a warning, but not critical
+ break;
+ case 3:
+ // Level 3 means a critical error
+ throw new DAV\Exception\UnsupportedMediaType('Validation error in vCard: '.$message['message']);
+ }
+ }
+ if ($warningMessage) {
+ $this->server->httpResponse->setHeader(
+ 'X-Sabre-Ew-Gross',
+ 'vCard validation warning: '.$warningMessage
+ );
+
+ // Re-serializing object.
+ $data = $vobj->serialize();
+ if (!$modified && 0 !== strcmp($data, $before)) {
+ // This ensures that the system does not send an ETag back.
+ $modified = true;
+ }
+ }
+
+ // Destroy circular references to PHP will GC the object.
+ $vobj->destroy();
+ }
+
+ /**
+ * This function handles the addressbook-query REPORT.
+ *
+ * This report is used by the client to filter an addressbook based on a
+ * complex query.
+ *
+ * @param Xml\Request\AddressBookQueryReport $report
+ */
+ protected function addressbookQueryReport($report)
+ {
+ $depth = $this->server->getHTTPDepth(0);
+
+ if (0 == $depth) {
+ $candidateNodes = [
+ $this->server->tree->getNodeForPath($this->server->getRequestUri()),
+ ];
+ if (!$candidateNodes[0] instanceof ICard) {
+ throw new ReportNotSupported('The addressbook-query report is not supported on this url with Depth: 0');
+ }
+ } else {
+ $candidateNodes = $this->server->tree->getChildren($this->server->getRequestUri());
+ }
+
+ $contentType = $report->contentType;
+ if ($report->version) {
+ $contentType .= '; version='.$report->version;
+ }
+
+ $vcardType = $this->negotiateVCard(
+ $contentType
+ );
+
+ $validNodes = [];
+ foreach ($candidateNodes as $node) {
+ if (!$node instanceof ICard) {
+ continue;
+ }
+
+ $blob = $node->get();
+ if (is_resource($blob)) {
+ $blob = stream_get_contents($blob);
+ }
+
+ if (!$this->validateFilters($blob, $report->filters, $report->test)) {
+ continue;
+ }
+
+ $validNodes[] = $node;
+
+ if ($report->limit && $report->limit <= count($validNodes)) {
+ // We hit the maximum number of items, we can stop now.
+ break;
+ }
+ }
+
+ $result = [];
+ foreach ($validNodes as $validNode) {
+ if (0 == $depth) {
+ $href = $this->server->getRequestUri();
+ } else {
+ $href = $this->server->getRequestUri().'/'.$validNode->getName();
+ }
+
+ list($props) = $this->server->getPropertiesForPath($href, $report->properties, 0);
+
+ if (isset($props[200]['{'.self::NS_CARDDAV.'}address-data'])) {
+ $props[200]['{'.self::NS_CARDDAV.'}address-data'] = $this->convertVCard(
+ $props[200]['{'.self::NS_CARDDAV.'}address-data'],
+ $vcardType,
+ $report->addressDataProperties
+ );
+ }
+ $result[] = $props;
+ }
+
+ $prefer = $this->server->getHTTPPrefer();
+
+ $this->server->httpResponse->setStatus(207);
+ $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
+ $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer');
+ $this->server->httpResponse->setBody($this->server->generateMultiStatus($result, 'minimal' === $prefer['return']));
+ }
+
+ /**
+ * Validates if a vcard makes it throught a list of filters.
+ *
+ * @param string $vcardData
+ * @param string $test anyof or allof (which means OR or AND)
+ *
+ * @return bool
+ */
+ public function validateFilters($vcardData, array $filters, $test)
+ {
+ if (!$filters) {
+ return true;
+ }
+ $vcard = VObject\Reader::read($vcardData);
+
+ foreach ($filters as $filter) {
+ $isDefined = isset($vcard->{$filter['name']});
+ if ($filter['is-not-defined']) {
+ if ($isDefined) {
+ $success = false;
+ } else {
+ $success = true;
+ }
+ } elseif ((!$filter['param-filters'] && !$filter['text-matches']) || !$isDefined) {
+ // We only need to check for existence
+ $success = $isDefined;
+ } else {
+ $vProperties = $vcard->select($filter['name']);
+
+ $results = [];
+ if ($filter['param-filters']) {
+ $results[] = $this->validateParamFilters($vProperties, $filter['param-filters'], $filter['test']);
+ }
+ if ($filter['text-matches']) {
+ $texts = [];
+ foreach ($vProperties as $vProperty) {
+ $texts[] = $vProperty->getValue();
+ }
+
+ $results[] = $this->validateTextMatches($texts, $filter['text-matches'], $filter['test']);
+ }
+
+ if (1 === count($results)) {
+ $success = $results[0];
+ } else {
+ if ('anyof' === $filter['test']) {
+ $success = $results[0] || $results[1];
+ } else {
+ $success = $results[0] && $results[1];
+ }
+ }
+ } // else
+
+ // There are two conditions where we can already determine whether
+ // or not this filter succeeds.
+ if ('anyof' === $test && $success) {
+ // Destroy circular references to PHP will GC the object.
+ $vcard->destroy();
+
+ return true;
+ }
+ if ('allof' === $test && !$success) {
+ // Destroy circular references to PHP will GC the object.
+ $vcard->destroy();
+
+ return false;
+ }
+ } // foreach
+
+ // Destroy circular references to PHP will GC the object.
+ $vcard->destroy();
+
+ // If we got all the way here, it means we haven't been able to
+ // determine early if the test failed or not.
+ //
+ // This implies for 'anyof' that the test failed, and for 'allof' that
+ // we succeeded. Sounds weird, but makes sense.
+ return 'allof' === $test;
+ }
+
+ /**
+ * Validates if a param-filter can be applied to a specific property.
+ *
+ * @todo currently we're only validating the first parameter of the passed
+ * property. Any subsequence parameters with the same name are
+ * ignored.
+ *
+ * @param string $test
+ *
+ * @return bool
+ */
+ protected function validateParamFilters(array $vProperties, array $filters, $test)
+ {
+ foreach ($filters as $filter) {
+ $isDefined = false;
+ foreach ($vProperties as $vProperty) {
+ $isDefined = isset($vProperty[$filter['name']]);
+ if ($isDefined) {
+ break;
+ }
+ }
+
+ if ($filter['is-not-defined']) {
+ if ($isDefined) {
+ $success = false;
+ } else {
+ $success = true;
+ }
+
+ // If there's no text-match, we can just check for existence
+ } elseif (!$filter['text-match'] || !$isDefined) {
+ $success = $isDefined;
+ } else {
+ $success = false;
+ foreach ($vProperties as $vProperty) {
+ // If we got all the way here, we'll need to validate the
+ // text-match filter.
+ if (isset($vProperty[$filter['name']])) {
+ $success = DAV\StringUtil::textMatch(
+ $vProperty[$filter['name']]->getValue(),
+ $filter['text-match']['value'],
+ $filter['text-match']['collation'],
+ $filter['text-match']['match-type']
+ );
+ if ($filter['text-match']['negate-condition']) {
+ $success = !$success;
+ }
+ }
+ if ($success) {
+ break;
+ }
+ }
+ } // else
+
+ // There are two conditions where we can already determine whether
+ // or not this filter succeeds.
+ if ('anyof' === $test && $success) {
+ return true;
+ }
+ if ('allof' === $test && !$success) {
+ return false;
+ }
+ }
+
+ // If we got all the way here, it means we haven't been able to
+ // determine early if the test failed or not.
+ //
+ // This implies for 'anyof' that the test failed, and for 'allof' that
+ // we succeeded. Sounds weird, but makes sense.
+ return 'allof' === $test;
+ }
+
+ /**
+ * Validates if a text-filter can be applied to a specific property.
+ *
+ * @param string $test
+ *
+ * @return bool
+ */
+ protected function validateTextMatches(array $texts, array $filters, $test)
+ {
+ foreach ($filters as $filter) {
+ $success = false;
+ foreach ($texts as $haystack) {
+ $success = DAV\StringUtil::textMatch($haystack, $filter['value'], $filter['collation'], $filter['match-type']);
+ if ($filter['negate-condition']) {
+ $success = !$success;
+ }
+
+ // Breaking on the first match
+ if ($success) {
+ break;
+ }
+ }
+
+ if ($success && 'anyof' === $test) {
+ return true;
+ }
+
+ if (!$success && 'allof' == $test) {
+ return false;
+ }
+ }
+
+ // If we got all the way here, it means we haven't been able to
+ // determine early if the test failed or not.
+ //
+ // This implies for 'anyof' that the test failed, and for 'allof' that
+ // we succeeded. Sounds weird, but makes sense.
+ return 'allof' === $test;
+ }
+
+ /**
+ * This event is triggered when fetching properties.
+ *
+ * This event is scheduled late in the process, after most work for
+ * propfind has been done.
+ */
+ public function propFindLate(DAV\PropFind $propFind, DAV\INode $node)
+ {
+ // If the request was made using the SOGO connector, we must rewrite
+ // the content-type property. By default SabreDAV will send back
+ // text/x-vcard; charset=utf-8, but for SOGO we must strip that last
+ // part.
+ if (false === strpos((string) $this->server->httpRequest->getHeader('User-Agent'), 'Thunderbird')) {
+ return;
+ }
+ $contentType = $propFind->get('{DAV:}getcontenttype');
+ if (null !== $contentType) {
+ list($part) = explode(';', $contentType);
+ if ('text/x-vcard' === $part || 'text/vcard' === $part) {
+ $propFind->set('{DAV:}getcontenttype', 'text/x-vcard');
+ }
+ }
+ }
+
+ /**
+ * This method is used to generate HTML output for the
+ * Sabre\DAV\Browser\Plugin. This allows us to generate an interface users
+ * can use to create new addressbooks.
+ *
+ * @param string $output
+ *
+ * @return bool
+ */
+ public function htmlActionsPanel(DAV\INode $node, &$output)
+ {
+ if (!$node instanceof AddressBookHome) {
+ return;
+ }
+
+ $output .= '
+
';
+
+ return false;
+ }
+
+ /**
+ * This event is triggered after GET requests.
+ *
+ * This is used to transform data into jCal, if this was requested.
+ */
+ public function httpAfterGet(RequestInterface $request, ResponseInterface $response)
+ {
+ $contentType = $response->getHeader('Content-Type');
+ if (null === $contentType || false === strpos($contentType, 'text/vcard')) {
+ return;
+ }
+
+ $target = $this->negotiateVCard($request->getHeader('Accept'), $mimeType);
+
+ $newBody = $this->convertVCard(
+ $response->getBody(),
+ $target
+ );
+
+ $response->setBody($newBody);
+ $response->setHeader('Content-Type', $mimeType.'; charset=utf-8');
+ $response->setHeader('Content-Length', strlen($newBody));
+ }
+
+ /**
+ * This helper function performs the content-type negotiation for vcards.
+ *
+ * It will return one of the following strings:
+ * 1. vcard3
+ * 2. vcard4
+ * 3. jcard
+ *
+ * It defaults to vcard3.
+ *
+ * @param string $input
+ * @param string $mimeType
+ *
+ * @return string
+ */
+ protected function negotiateVCard($input, &$mimeType = null)
+ {
+ $result = HTTP\negotiateContentType(
+ $input,
+ [
+ // Most often used mime-type. Version 3
+ 'text/x-vcard',
+ // The correct standard mime-type. Defaults to version 3 as
+ // well.
+ 'text/vcard',
+ // vCard 4
+ 'text/vcard; version=4.0',
+ // vCard 3
+ 'text/vcard; version=3.0',
+ // jCard
+ 'application/vcard+json',
+ ]
+ );
+
+ $mimeType = $result;
+ switch ($result) {
+ default:
+ case 'text/x-vcard':
+ case 'text/vcard':
+ case 'text/vcard; version=3.0':
+ $mimeType = 'text/vcard';
+
+ return 'vcard3';
+ case 'text/vcard; version=4.0':
+ return 'vcard4';
+ case 'application/vcard+json':
+ return 'jcard';
+
+ // @codeCoverageIgnoreStart
+ }
+ // @codeCoverageIgnoreEnd
+ }
+
+ /**
+ * Converts a vcard blob to a different version, or jcard.
+ *
+ * @param string|resource $data
+ * @param string $target
+ * @param array $propertiesFilter
+ *
+ * @return string
+ */
+ protected function convertVCard($data, $target, ?array $propertiesFilter = null)
+ {
+ if (is_resource($data)) {
+ $data = stream_get_contents($data);
+ }
+ $input = VObject\Reader::read($data);
+ if (!empty($propertiesFilter)) {
+ $propertiesFilter = array_merge(['UID', 'VERSION', 'FN'], $propertiesFilter);
+ $keys = array_unique(array_map(function ($child) {
+ return $child->name;
+ }, $input->children()));
+ $keys = array_diff($keys, $propertiesFilter);
+ foreach ($keys as $key) {
+ unset($input->$key);
+ }
+ $data = $input->serialize();
+ }
+ $output = null;
+ try {
+ switch ($target) {
+ default:
+ case 'vcard3':
+ if (VObject\Document::VCARD30 === $input->getDocumentType()) {
+ // Do nothing
+ return $data;
+ }
+ $output = $input->convert(VObject\Document::VCARD30);
+
+ return $output->serialize();
+ case 'vcard4':
+ if (VObject\Document::VCARD40 === $input->getDocumentType()) {
+ // Do nothing
+ return $data;
+ }
+ $output = $input->convert(VObject\Document::VCARD40);
+
+ return $output->serialize();
+ case 'jcard':
+ $output = $input->convert(VObject\Document::VCARD40);
+
+ return json_encode($output);
+ }
+ } finally {
+ // Destroy circular references to PHP will GC the object.
+ $input->destroy();
+ if (!is_null($output)) {
+ $output->destroy();
+ }
+ }
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ public function getPluginName()
+ {
+ return 'carddav';
+ }
+
+ /**
+ * Returns a bunch of meta-data about the plugin.
+ *
+ * Providing this information is optional, and is mainly displayed by the
+ * Browser plugin.
+ *
+ * The description key in the returned array may contain html and will not
+ * be sanitized.
+ *
+ * @return array
+ */
+ public function getPluginInfo()
+ {
+ return [
+ 'name' => $this->getPluginName(),
+ 'description' => 'Adds support for CardDAV (rfc6352)',
+ 'link' => 'http://sabre.io/dav/carddav/',
+ ];
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/VCFExportPlugin.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/VCFExportPlugin.php
new file mode 100644
index 0000000..431391e
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/VCFExportPlugin.php
@@ -0,0 +1,165 @@
+server = $server;
+ $this->server->on('method:GET', [$this, 'httpGet'], 90);
+ $server->on('browserButtonActions', function ($path, $node, &$actions) {
+ if ($node instanceof IAddressBook) {
+ $actions .= '';
+ }
+ });
+ }
+
+ /**
+ * Intercepts GET requests on addressbook urls ending with ?export.
+ *
+ * @return bool
+ */
+ public function httpGet(RequestInterface $request, ResponseInterface $response)
+ {
+ $queryParams = $request->getQueryParameters();
+ if (!array_key_exists('export', $queryParams)) {
+ return;
+ }
+
+ $path = $request->getPath();
+
+ $node = $this->server->tree->getNodeForPath($path);
+
+ if (!($node instanceof IAddressBook)) {
+ return;
+ }
+
+ $this->server->transactionType = 'get-addressbook-export';
+
+ // Checking ACL, if available.
+ if ($aclPlugin = $this->server->getPlugin('acl')) {
+ $aclPlugin->checkPrivileges($path, '{DAV:}read');
+ }
+
+ $nodes = $this->server->getPropertiesForPath($path, [
+ '{'.Plugin::NS_CARDDAV.'}address-data',
+ ], 1);
+
+ $format = 'text/directory';
+
+ $output = null;
+ $filenameExtension = null;
+
+ switch ($format) {
+ case 'text/directory':
+ $output = $this->generateVCF($nodes);
+ $filenameExtension = '.vcf';
+ break;
+ }
+
+ $filename = preg_replace(
+ '/[^a-zA-Z0-9-_ ]/um',
+ '',
+ $node->getName()
+ );
+ $filename .= '-'.date('Y-m-d').$filenameExtension;
+
+ $response->setHeader('Content-Disposition', 'attachment; filename="'.$filename.'"');
+ $response->setHeader('Content-Type', $format);
+
+ $response->setStatus(200);
+ $response->setBody($output);
+
+ // Returning false to break the event chain
+ return false;
+ }
+
+ /**
+ * Merges all vcard objects, and builds one big vcf export.
+ *
+ * @return string
+ */
+ public function generateVCF(array $nodes)
+ {
+ $output = '';
+
+ foreach ($nodes as $node) {
+ if (!isset($node[200]['{'.Plugin::NS_CARDDAV.'}address-data'])) {
+ continue;
+ }
+ $nodeData = $node[200]['{'.Plugin::NS_CARDDAV.'}address-data'];
+
+ // Parsing this node so VObject can clean up the output.
+ $vcard = VObject\Reader::read($nodeData);
+ $output .= $vcard->serialize();
+
+ // Destroy circular references to PHP will GC the object.
+ $vcard->destroy();
+ }
+
+ return $output;
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using \Sabre\DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ public function getPluginName()
+ {
+ return 'vcf-export';
+ }
+
+ /**
+ * Returns a bunch of meta-data about the plugin.
+ *
+ * Providing this information is optional, and is mainly displayed by the
+ * Browser plugin.
+ *
+ * The description key in the returned array may contain html and will not
+ * be sanitized.
+ *
+ * @return array
+ */
+ public function getPluginInfo()
+ {
+ return [
+ 'name' => $this->getPluginName(),
+ 'description' => 'Adds the ability to export CardDAV addressbooks as a single vCard file.',
+ 'link' => 'http://sabre.io/dav/vcf-export-plugin/',
+ ];
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Filter/AddressData.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Filter/AddressData.php
new file mode 100644
index 0000000..b60fceb
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Filter/AddressData.php
@@ -0,0 +1,66 @@
+next();
+ *
+ * $reader->parseInnerTree() will parse the entire sub-tree, and advance to
+ * the next element.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $result = [
+ 'contentType' => $reader->getAttribute('content-type') ?: 'text/vcard',
+ 'version' => $reader->getAttribute('version') ?: '3.0',
+ ];
+
+ $elems = (array) $reader->parseInnerTree();
+ $elems = array_filter($elems, function ($element) {
+ return '{urn:ietf:params:xml:ns:carddav}prop' === $element['name'] &&
+ isset($element['attributes']['name']);
+ });
+ $result['addressDataProperties'] = array_map(function ($element) {
+ return $element['attributes']['name'];
+ }, $elems);
+
+ return $result;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Filter/ParamFilter.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Filter/ParamFilter.php
new file mode 100644
index 0000000..0a7ec06
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Filter/ParamFilter.php
@@ -0,0 +1,86 @@
+next();
+ *
+ * $reader->parseInnerTree() will parse the entire sub-tree, and advance to
+ * the next element.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $result = [
+ 'name' => null,
+ 'is-not-defined' => false,
+ 'text-match' => null,
+ ];
+
+ $att = $reader->parseAttributes();
+ $result['name'] = $att['name'];
+
+ $elems = $reader->parseInnerTree();
+
+ if (is_array($elems)) {
+ foreach ($elems as $elem) {
+ switch ($elem['name']) {
+ case '{'.Plugin::NS_CARDDAV.'}is-not-defined':
+ $result['is-not-defined'] = true;
+ break;
+ case '{'.Plugin::NS_CARDDAV.'}text-match':
+ $matchType = isset($elem['attributes']['match-type']) ? $elem['attributes']['match-type'] : 'contains';
+
+ if (!in_array($matchType, ['contains', 'equals', 'starts-with', 'ends-with'])) {
+ throw new BadRequest('Unknown match-type: '.$matchType);
+ }
+ $result['text-match'] = [
+ 'negate-condition' => isset($elem['attributes']['negate-condition']) && 'yes' === $elem['attributes']['negate-condition'],
+ 'collation' => isset($elem['attributes']['collation']) ? $elem['attributes']['collation'] : 'i;unicode-casemap',
+ 'value' => $elem['value'],
+ 'match-type' => $matchType,
+ ];
+ break;
+ }
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Filter/PropFilter.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Filter/PropFilter.php
new file mode 100644
index 0000000..5dedac8
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Filter/PropFilter.php
@@ -0,0 +1,95 @@
+next();
+ *
+ * $reader->parseInnerTree() will parse the entire sub-tree, and advance to
+ * the next element.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $result = [
+ 'name' => null,
+ 'test' => 'anyof',
+ 'is-not-defined' => false,
+ 'param-filters' => [],
+ 'text-matches' => [],
+ ];
+
+ $att = $reader->parseAttributes();
+ $result['name'] = $att['name'];
+
+ if (isset($att['test']) && 'allof' === $att['test']) {
+ $result['test'] = 'allof';
+ }
+
+ $elems = $reader->parseInnerTree();
+
+ if (is_array($elems)) {
+ foreach ($elems as $elem) {
+ switch ($elem['name']) {
+ case '{'.Plugin::NS_CARDDAV.'}param-filter':
+ $result['param-filters'][] = $elem['value'];
+ break;
+ case '{'.Plugin::NS_CARDDAV.'}is-not-defined':
+ $result['is-not-defined'] = true;
+ break;
+ case '{'.Plugin::NS_CARDDAV.'}text-match':
+ $matchType = isset($elem['attributes']['match-type']) ? $elem['attributes']['match-type'] : 'contains';
+
+ if (!in_array($matchType, ['contains', 'equals', 'starts-with', 'ends-with'])) {
+ throw new BadRequest('Unknown match-type: '.$matchType);
+ }
+ $result['text-matches'][] = [
+ 'negate-condition' => isset($elem['attributes']['negate-condition']) && 'yes' === $elem['attributes']['negate-condition'],
+ 'collation' => isset($elem['attributes']['collation']) ? $elem['attributes']['collation'] : 'i;unicode-casemap',
+ 'value' => $elem['value'],
+ 'match-type' => $matchType,
+ ];
+ break;
+ }
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Property/SupportedAddressData.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Property/SupportedAddressData.php
new file mode 100644
index 0000000..536c5a1
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Property/SupportedAddressData.php
@@ -0,0 +1,77 @@
+ 'text/vcard', 'version' => '3.0'],
+ ['contentType' => 'text/vcard', 'version' => '4.0'],
+ ['contentType' => 'application/vcard+json', 'version' => '4.0'],
+ ];
+ }
+
+ $this->supportedData = $supportedData;
+ }
+
+ /**
+ * 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)
+ {
+ foreach ($this->supportedData as $supported) {
+ $writer->startElement('{'.Plugin::NS_CARDDAV.'}address-data-type');
+ $writer->writeAttributes([
+ 'content-type' => $supported['contentType'],
+ 'version' => $supported['version'],
+ ]);
+ $writer->endElement(); // address-data-type
+ }
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Property/SupportedCollationSet.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Property/SupportedCollationSet.php
new file mode 100644
index 0000000..b19eddd
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Property/SupportedCollationSet.php
@@ -0,0 +1,44 @@
+writeElement('{urn:ietf:params:xml:ns:carddav}supported-collation', $coll);
+ }
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Request/AddressBookMultiGetReport.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Request/AddressBookMultiGetReport.php
new file mode 100644
index 0000000..491f969
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Request/AddressBookMultiGetReport.php
@@ -0,0 +1,116 @@
+next();
+ *
+ * $reader->parseInnerTree() will parse the entire sub-tree, and advance to
+ * the next element.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $elems = $reader->parseInnerTree([
+ '{urn:ietf:params:xml:ns:carddav}address-data' => 'Sabre\\CardDAV\\Xml\\Filter\\AddressData',
+ '{DAV:}prop' => 'Sabre\\Xml\\Element\\KeyValue',
+ ]);
+
+ $newProps = [
+ 'hrefs' => [],
+ 'properties' => [],
+ ];
+
+ foreach ($elems as $elem) {
+ switch ($elem['name']) {
+ case '{DAV:}prop':
+ $newProps['properties'] = array_keys($elem['value']);
+ if (isset($elem['value']['{'.Plugin::NS_CARDDAV.'}address-data'])) {
+ $newProps += $elem['value']['{'.Plugin::NS_CARDDAV.'}address-data'];
+ }
+ break;
+ case '{DAV:}href':
+ $newProps['hrefs'][] = Uri\resolve($reader->contextUri, $elem['value']);
+ break;
+ }
+ }
+
+ $obj = new self();
+ foreach ($newProps as $key => $value) {
+ $obj->$key = $value;
+ }
+
+ return $obj;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Request/AddressBookQueryReport.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Request/AddressBookQueryReport.php
new file mode 100644
index 0000000..02402f6
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Request/AddressBookQueryReport.php
@@ -0,0 +1,193 @@
+next();
+ *
+ * $reader->parseInnerTree() will parse the entire sub-tree, and advance to
+ * the next element.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $elems = (array) $reader->parseInnerTree([
+ '{urn:ietf:params:xml:ns:carddav}prop-filter' => 'Sabre\\CardDAV\\Xml\\Filter\\PropFilter',
+ '{urn:ietf:params:xml:ns:carddav}param-filter' => 'Sabre\\CardDAV\\Xml\\Filter\\ParamFilter',
+ '{urn:ietf:params:xml:ns:carddav}address-data' => 'Sabre\\CardDAV\\Xml\\Filter\\AddressData',
+ '{DAV:}prop' => 'Sabre\\Xml\\Element\\KeyValue',
+ ]);
+
+ $newProps = [
+ 'filters' => null,
+ 'properties' => [],
+ 'test' => 'anyof',
+ 'limit' => null,
+ ];
+
+ if (!is_array($elems)) {
+ $elems = [];
+ }
+
+ foreach ($elems as $elem) {
+ switch ($elem['name']) {
+ case '{DAV:}prop':
+ $newProps['properties'] = array_keys($elem['value']);
+ if (isset($elem['value']['{'.Plugin::NS_CARDDAV.'}address-data'])) {
+ $newProps += $elem['value']['{'.Plugin::NS_CARDDAV.'}address-data'];
+ }
+ break;
+ case '{'.Plugin::NS_CARDDAV.'}filter':
+ if (!is_null($newProps['filters'])) {
+ throw new BadRequest('You can only include 1 {'.Plugin::NS_CARDDAV.'}filter element');
+ }
+ if (isset($elem['attributes']['test'])) {
+ $newProps['test'] = $elem['attributes']['test'];
+ if ('allof' !== $newProps['test'] && 'anyof' !== $newProps['test']) {
+ throw new BadRequest('The "test" attribute must be one of "allof" or "anyof"');
+ }
+ }
+
+ $newProps['filters'] = [];
+ foreach ((array) $elem['value'] as $subElem) {
+ if ($subElem['name'] === '{'.Plugin::NS_CARDDAV.'}prop-filter') {
+ $newProps['filters'][] = $subElem['value'];
+ }
+ }
+ break;
+ case '{'.Plugin::NS_CARDDAV.'}limit':
+ foreach ($elem['value'] as $child) {
+ if ($child['name'] === '{'.Plugin::NS_CARDDAV.'}nresults') {
+ $newProps['limit'] = (int) $child['value'];
+ }
+ }
+ break;
+ }
+ }
+
+ if (is_null($newProps['filters'])) {
+ /*
+ * We are supposed to throw this error, but KDE sometimes does not
+ * include the filter element, and we need to treat it as if no
+ * filters are supplied
+ */
+ //throw new BadRequest('The {' . Plugin::NS_CARDDAV . '}filter element is required for this request');
+ $newProps['filters'] = [];
+ }
+
+ $obj = new self();
+ foreach ($newProps as $key => $value) {
+ $obj->$key = $value;
+ }
+
+ return $obj;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractBasic.php b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractBasic.php
new file mode 100644
index 0000000..3132333
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractBasic.php
@@ -0,0 +1,136 @@
+realm = $realm;
+ }
+
+ /**
+ * When this method is called, the backend must check if authentication was
+ * successful.
+ *
+ * The returned value must be one of the following
+ *
+ * [true, "principals/username"]
+ * [false, "reason for failure"]
+ *
+ * If authentication was successful, it's expected that the authentication
+ * backend returns a so-called principal url.
+ *
+ * Examples of a principal url:
+ *
+ * principals/admin
+ * principals/user1
+ * principals/users/joe
+ * principals/uid/123457
+ *
+ * If you don't use WebDAV ACL (RFC3744) we recommend that you simply
+ * return a string such as:
+ *
+ * principals/users/[username]
+ *
+ * @return array
+ */
+ public function check(RequestInterface $request, ResponseInterface $response)
+ {
+ $auth = new HTTP\Auth\Basic(
+ $this->realm,
+ $request,
+ $response
+ );
+
+ $userpass = $auth->getCredentials();
+ if (!$userpass) {
+ return [false, "No 'Authorization: Basic' header found. Either the client didn't send one, or the server is misconfigured"];
+ }
+ if (!$this->validateUserPass($userpass[0], $userpass[1])) {
+ return [false, 'Username or password was incorrect'];
+ }
+
+ return [true, $this->principalPrefix.$userpass[0]];
+ }
+
+ /**
+ * This method is called when a user could not be authenticated, and
+ * authentication was required for the current request.
+ *
+ * This gives you the opportunity to set authentication headers. The 401
+ * status code will already be set.
+ *
+ * In this case of Basic Auth, this would for example mean that the
+ * following header needs to be set:
+ *
+ * $response->addHeader('WWW-Authenticate', 'Basic realm=SabreDAV');
+ *
+ * Keep in mind that in the case of multiple authentication backends, other
+ * WWW-Authenticate headers may already have been set, and you'll want to
+ * append your own WWW-Authenticate header instead of overwriting the
+ * existing one.
+ */
+ public function challenge(RequestInterface $request, ResponseInterface $response)
+ {
+ $auth = new HTTP\Auth\Basic(
+ $this->realm,
+ $request,
+ $response
+ );
+ $auth->requireLogin();
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractBearer.php b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractBearer.php
new file mode 100644
index 0000000..b681747
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractBearer.php
@@ -0,0 +1,130 @@
+realm = $realm;
+ }
+
+ /**
+ * When this method is called, the backend must check if authentication was
+ * successful.
+ *
+ * The returned value must be one of the following
+ *
+ * [true, "principals/username"]
+ * [false, "reason for failure"]
+ *
+ * If authentication was successful, it's expected that the authentication
+ * backend returns a so-called principal url.
+ *
+ * Examples of a principal url:
+ *
+ * principals/admin
+ * principals/user1
+ * principals/users/joe
+ * principals/uid/123457
+ *
+ * If you don't use WebDAV ACL (RFC3744) we recommend that you simply
+ * return a string such as:
+ *
+ * principals/users/[username]
+ *
+ * @return array
+ */
+ public function check(RequestInterface $request, ResponseInterface $response)
+ {
+ $auth = new HTTP\Auth\Bearer(
+ $this->realm,
+ $request,
+ $response
+ );
+
+ $bearerToken = $auth->getToken($request);
+ if (!$bearerToken) {
+ return [false, "No 'Authorization: Bearer' header found. Either the client didn't send one, or the server is mis-configured"];
+ }
+ $principalUrl = $this->validateBearerToken($bearerToken);
+ if (!$principalUrl) {
+ return [false, 'Bearer token was incorrect'];
+ }
+
+ return [true, $principalUrl];
+ }
+
+ /**
+ * This method is called when a user could not be authenticated, and
+ * authentication was required for the current request.
+ *
+ * This gives you the opportunity to set authentication headers. The 401
+ * status code will already be set.
+ *
+ * In this case of Bearer Auth, this would for example mean that the
+ * following header needs to be set:
+ *
+ * $response->addHeader('WWW-Authenticate', 'Bearer realm=SabreDAV');
+ *
+ * Keep in mind that in the case of multiple authentication backends, other
+ * WWW-Authenticate headers may already have been set, and you'll want to
+ * append your own WWW-Authenticate header instead of overwriting the
+ * existing one.
+ */
+ public function challenge(RequestInterface $request, ResponseInterface $response)
+ {
+ $auth = new HTTP\Auth\Bearer(
+ $this->realm,
+ $request,
+ $response
+ );
+ $auth->requireLogin();
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractDigest.php b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractDigest.php
new file mode 100644
index 0000000..297655d
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractDigest.php
@@ -0,0 +1,160 @@
+realm = $realm;
+ }
+
+ /**
+ * Returns a users digest hash based on the username and realm.
+ *
+ * If the user was not known, null must be returned.
+ *
+ * @param string $realm
+ * @param string $username
+ *
+ * @return string|null
+ */
+ abstract public function getDigestHash($realm, $username);
+
+ /**
+ * When this method is called, the backend must check if authentication was
+ * successful.
+ *
+ * The returned value must be one of the following
+ *
+ * [true, "principals/username"]
+ * [false, "reason for failure"]
+ *
+ * If authentication was successful, it's expected that the authentication
+ * backend returns a so-called principal url.
+ *
+ * Examples of a principal url:
+ *
+ * principals/admin
+ * principals/user1
+ * principals/users/joe
+ * principals/uid/123457
+ *
+ * If you don't use WebDAV ACL (RFC3744) we recommend that you simply
+ * return a string such as:
+ *
+ * principals/users/[username]
+ *
+ * @return array
+ */
+ public function check(RequestInterface $request, ResponseInterface $response)
+ {
+ $digest = new HTTP\Auth\Digest(
+ $this->realm,
+ $request,
+ $response
+ );
+ $digest->init();
+
+ $username = $digest->getUsername();
+
+ // No username was given
+ if (!$username) {
+ return [false, "No 'Authorization: Digest' header found. Either the client didn't send one, or the server is misconfigured"];
+ }
+
+ $hash = $this->getDigestHash($this->realm, $username);
+ // If this was false, the user account didn't exist
+ if (false === $hash || is_null($hash)) {
+ return [false, 'Username or password was incorrect'];
+ }
+ if (!is_string($hash)) {
+ throw new DAV\Exception('The returned value from getDigestHash must be a string or null');
+ }
+
+ // If this was false, the password or part of the hash was incorrect.
+ if (!$digest->validateA1($hash)) {
+ return [false, 'Username or password was incorrect'];
+ }
+
+ return [true, $this->principalPrefix.$username];
+ }
+
+ /**
+ * This method is called when a user could not be authenticated, and
+ * authentication was required for the current request.
+ *
+ * This gives you the opportunity to set authentication headers. The 401
+ * status code will already be set.
+ *
+ * In this case of Basic Auth, this would for example mean that the
+ * following header needs to be set:
+ *
+ * $response->addHeader('WWW-Authenticate', 'Basic realm=SabreDAV');
+ *
+ * Keep in mind that in the case of multiple authentication backends, other
+ * WWW-Authenticate headers may already have been set, and you'll want to
+ * append your own WWW-Authenticate header instead of overwriting the
+ * existing one.
+ */
+ public function challenge(RequestInterface $request, ResponseInterface $response)
+ {
+ $auth = new HTTP\Auth\Digest(
+ $this->realm,
+ $request,
+ $response
+ );
+ $auth->init();
+
+ $oldStatus = $response->getStatus() ?: 200;
+ $auth->requireLogin();
+
+ // Preventing the digest utility from modifying the http status code,
+ // this should be handled by the main plugin.
+ $response->setStatus($oldStatus);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/Apache.php b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/Apache.php
new file mode 100644
index 0000000..ebf67ca
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/Apache.php
@@ -0,0 +1,93 @@
+getRawServerValue('REMOTE_USER');
+ if (is_null($remoteUser)) {
+ $remoteUser = $request->getRawServerValue('REDIRECT_REMOTE_USER');
+ }
+ if (is_null($remoteUser)) {
+ $remoteUser = $request->getRawServerValue('PHP_AUTH_USER');
+ }
+ if (is_null($remoteUser)) {
+ return [false, 'No REMOTE_USER, REDIRECT_REMOTE_USER, or PHP_AUTH_USER property was found in the PHP $_SERVER super-global. This likely means your server is not configured correctly'];
+ }
+
+ return [true, $this->principalPrefix.$remoteUser];
+ }
+
+ /**
+ * This method is called when a user could not be authenticated, and
+ * authentication was required for the current request.
+ *
+ * This gives you the opportunity to set authentication headers. The 401
+ * status code will already be set.
+ *
+ * In this case of Basic Auth, this would for example mean that the
+ * following header needs to be set:
+ *
+ * $response->addHeader('WWW-Authenticate', 'Basic realm=SabreDAV');
+ *
+ * Keep in mind that in the case of multiple authentication backends, other
+ * WWW-Authenticate headers may already have been set, and you'll want to
+ * append your own WWW-Authenticate header instead of overwriting the
+ * existing one.
+ */
+ public function challenge(RequestInterface $request, ResponseInterface $response)
+ {
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/BackendInterface.php b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/BackendInterface.php
new file mode 100644
index 0000000..133eac9
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/BackendInterface.php
@@ -0,0 +1,65 @@
+addHeader('WWW-Authenticate', 'Basic realm=SabreDAV');
+ *
+ * Keep in mind that in the case of multiple authentication backends, other
+ * WWW-Authenticate headers may already have been set, and you'll want to
+ * append your own WWW-Authenticate header instead of overwriting the
+ * existing one.
+ */
+ public function challenge(RequestInterface $request, ResponseInterface $response);
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/BasicCallBack.php b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/BasicCallBack.php
new file mode 100644
index 0000000..5a8bb98
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/BasicCallBack.php
@@ -0,0 +1,56 @@
+callBack = $callBack;
+ }
+
+ /**
+ * Validates a username and password.
+ *
+ * This method should return true or false depending on if login
+ * succeeded.
+ *
+ * @param string $username
+ * @param string $password
+ *
+ * @return bool
+ */
+ protected function validateUserPass($username, $password)
+ {
+ $cb = $this->callBack;
+
+ return $cb($username, $password);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/File.php b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/File.php
new file mode 100644
index 0000000..ea2d396
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/File.php
@@ -0,0 +1,74 @@
+loadFile($filename);
+ }
+ }
+
+ /**
+ * Loads an htdigest-formatted file. This method can be called multiple times if
+ * more than 1 file is used.
+ *
+ * @param string $filename
+ */
+ public function loadFile($filename)
+ {
+ foreach (file($filename, FILE_IGNORE_NEW_LINES) as $line) {
+ if (2 !== substr_count($line, ':')) {
+ throw new DAV\Exception('Malformed htdigest file. Every line should contain 2 colons');
+ }
+ list($username, $realm, $A1) = explode(':', $line);
+
+ if (!preg_match('/^[a-zA-Z0-9]{32}$/', $A1)) {
+ throw new DAV\Exception('Malformed htdigest file. Invalid md5 hash');
+ }
+ $this->users[$realm.':'.$username] = $A1;
+ }
+ }
+
+ /**
+ * Returns a users' information.
+ *
+ * @param string $realm
+ * @param string $username
+ *
+ * @return string
+ */
+ public function getDigestHash($realm, $username)
+ {
+ return isset($this->users[$realm.':'.$username]) ? $this->users[$realm.':'.$username] : false;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/IMAP.php b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/IMAP.php
new file mode 100644
index 0000000..3a18311
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/IMAP.php
@@ -0,0 +1,82 @@
+mailbox = $mailbox;
+ }
+
+ /**
+ * Connects to an IMAP server and tries to authenticate.
+ *
+ * @param string $username
+ * @param string $password
+ *
+ * @return bool
+ */
+ protected function imapOpen($username, $password)
+ {
+ $success = false;
+
+ try {
+ $imap = imap_open($this->mailbox, $username, $password, OP_HALFOPEN | OP_READONLY, 1);
+ if ($imap) {
+ $success = true;
+ }
+ } catch (\ErrorException $e) {
+ error_log($e->getMessage());
+ }
+
+ $errors = imap_errors();
+ if ($errors) {
+ foreach ($errors as $error) {
+ error_log($error);
+ }
+ }
+
+ if (isset($imap) && $imap) {
+ imap_close($imap);
+ }
+
+ return $success;
+ }
+
+ /**
+ * Validates a username and password by trying to authenticate against IMAP.
+ *
+ * @param string $username
+ * @param string $password
+ *
+ * @return bool
+ */
+ protected function validateUserPass($username, $password)
+ {
+ return $this->imapOpen($username, $password);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/PDO.php b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/PDO.php
new file mode 100644
index 0000000..9a06912
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/PDO.php
@@ -0,0 +1,55 @@
+pdo = $pdo;
+ }
+
+ /**
+ * Returns the digest hash for a user.
+ *
+ * @param string $realm
+ * @param string $username
+ *
+ * @return string|null
+ */
+ public function getDigestHash($realm, $username)
+ {
+ $stmt = $this->pdo->prepare('SELECT digesta1 FROM '.$this->tableName.' WHERE username = ?');
+ $stmt->execute([$username]);
+
+ return $stmt->fetchColumn() ?: null;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/PDOBasicAuth.php b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/PDOBasicAuth.php
new file mode 100644
index 0000000..d142cbf
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/PDOBasicAuth.php
@@ -0,0 +1,114 @@
+pdo = $pdo;
+ if (isset($options['tableName'])) {
+ $this->tableName = $options['tableName'];
+ } else {
+ $this->tableName = 'users';
+ }
+ if (isset($options['digestColumn'])) {
+ $this->digestColumn = $options['digestColumn'];
+ } else {
+ $this->digestColumn = 'digest';
+ }
+ if (isset($options['uuidColumn'])) {
+ $this->uuidColumn = $options['uuidColumn'];
+ } else {
+ $this->uuidColumn = 'username';
+ }
+ if (isset($options['digestPrefix'])) {
+ $this->digestPrefix = $options['digestPrefix'];
+ }
+ }
+
+ /**
+ * Validates a username and password.
+ *
+ * This method should return true or false depending on if login
+ * succeeded.
+ *
+ * @param string $username
+ * @param string $password
+ *
+ * @return bool
+ */
+ public function validateUserPass($username, $password)
+ {
+ $stmt = $this->pdo->prepare('SELECT '.$this->digestColumn.' FROM '.$this->tableName.' WHERE '.$this->uuidColumn.' = ?');
+ $stmt->execute([$username]);
+ $result = $stmt->fetchAll();
+
+ if (!count($result)) {
+ return false;
+ } else {
+ $digest = $result[0][$this->digestColumn];
+
+ if (isset($this->digestPrefix)) {
+ $digest = substr($digest, strlen($this->digestPrefix));
+ }
+
+ if (password_verify($password, $digest)) {
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Plugin.php b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Plugin.php
new file mode 100644
index 0000000..47fbe20
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Plugin.php
@@ -0,0 +1,255 @@
+addBackend($authBackend);
+ }
+ }
+
+ /**
+ * Adds an authentication backend to the plugin.
+ */
+ public function addBackend(Backend\BackendInterface $authBackend)
+ {
+ $this->backends[] = $authBackend;
+ }
+
+ /**
+ * Initializes the plugin. This function is automatically called by the server.
+ */
+ public function initialize(Server $server)
+ {
+ $server->on('beforeMethod:*', [$this, 'beforeMethod'], 10);
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ public function getPluginName()
+ {
+ return 'auth';
+ }
+
+ /**
+ * Returns the currently logged-in principal.
+ *
+ * This will return a string such as:
+ *
+ * principals/username
+ * principals/users/username
+ *
+ * This method will return null if nobody is logged in.
+ *
+ * @return string|null
+ */
+ public function getCurrentPrincipal()
+ {
+ return $this->currentPrincipal;
+ }
+
+ /**
+ * This method is called before any HTTP method and forces users to be authenticated.
+ */
+ public function beforeMethod(RequestInterface $request, ResponseInterface $response)
+ {
+ if ($this->currentPrincipal) {
+ // We already have authentication information. This means that the
+ // event has already fired earlier, and is now likely fired for a
+ // sub-request.
+ //
+ // We don't want to authenticate users twice, so we simply don't do
+ // anything here. See Issue #700 for additional reasoning.
+ //
+ // This is not a perfect solution, but will be fixed once the
+ // "currently authenticated principal" is information that's not
+ // not associated with the plugin, but rather per-request.
+ //
+ // See issue #580 for more information about that.
+ return;
+ }
+
+ $authResult = $this->check($request, $response);
+
+ if ($authResult[0]) {
+ // Auth was successful
+ $this->currentPrincipal = $authResult[1];
+ $this->loginFailedReasons = null;
+
+ return;
+ }
+
+ // If we got here, it means that no authentication backend was
+ // successful in authenticating the user.
+ $this->currentPrincipal = null;
+ $this->loginFailedReasons = $authResult[1];
+
+ if ($this->autoRequireLogin) {
+ $this->challenge($request, $response);
+ throw new NotAuthenticated(implode(', ', $authResult[1]));
+ }
+ }
+
+ /**
+ * Checks authentication credentials, and logs the user in if possible.
+ *
+ * This method returns an array. The first item in the array is a boolean
+ * indicating if login was successful.
+ *
+ * If login was successful, the second item in the array will contain the
+ * current principal url/path of the logged in user.
+ *
+ * If login was not successful, the second item in the array will contain a
+ * an array with strings. The strings are a list of reasons why login was
+ * unsuccessful. For every auth backend there will be one reason, so usually
+ * there's just one.
+ *
+ * @return array
+ */
+ public function check(RequestInterface $request, ResponseInterface $response)
+ {
+ if (!$this->backends) {
+ throw new \Sabre\DAV\Exception('No authentication backends were configured on this server.');
+ }
+ $reasons = [];
+ foreach ($this->backends as $backend) {
+ $result = $backend->check(
+ $request,
+ $response
+ );
+
+ if (!is_array($result) || 2 !== count($result) || !is_bool($result[0]) || !is_string($result[1])) {
+ throw new \Sabre\DAV\Exception('The authentication backend did not return a correct value from the check() method.');
+ }
+
+ if ($result[0]) {
+ $this->currentPrincipal = $result[1];
+ // Exit early
+ return [true, $result[1]];
+ }
+ $reasons[] = $result[1];
+ }
+
+ return [false, $reasons];
+ }
+
+ /**
+ * This method sends authentication challenges to the user.
+ *
+ * This method will for example cause a HTTP Basic backend to set a
+ * WWW-Authorization header, indicating to the client that it should
+ * authenticate.
+ */
+ public function challenge(RequestInterface $request, ResponseInterface $response)
+ {
+ foreach ($this->backends as $backend) {
+ $backend->challenge($request, $response);
+ }
+ }
+
+ /**
+ * List of reasons why login failed for the last login operation.
+ *
+ * @var string[]|null
+ */
+ protected $loginFailedReasons;
+
+ /**
+ * Returns a list of reasons why login was unsuccessful.
+ *
+ * This method will return the login failed reasons for the last login
+ * operation. One for each auth backend.
+ *
+ * This method returns null if the last authentication attempt was
+ * successful, or if there was no authentication attempt yet.
+ *
+ * @return string[]|null
+ */
+ public function getLoginFailedReasons()
+ {
+ return $this->loginFailedReasons;
+ }
+
+ /**
+ * Returns a bunch of meta-data about the plugin.
+ *
+ * Providing this information is optional, and is mainly displayed by the
+ * Browser plugin.
+ *
+ * The description key in the returned array may contain html and will not
+ * be sanitized.
+ *
+ * @return array
+ */
+ public function getPluginInfo()
+ {
+ return [
+ 'name' => $this->getPluginName(),
+ 'description' => 'Generic authentication plugin',
+ 'link' => 'http://sabre.io/dav/authentication/',
+ ];
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Browser/GuessContentType.php b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/GuessContentType.php
new file mode 100644
index 0000000..5cda0b8
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/GuessContentType.php
@@ -0,0 +1,93 @@
+ 'image/jpeg',
+ 'gif' => 'image/gif',
+ 'png' => 'image/png',
+
+ // groupware
+ 'ics' => 'text/calendar',
+ 'vcf' => 'text/vcard',
+
+ // text
+ 'txt' => 'text/plain',
+ ];
+
+ /**
+ * Initializes the plugin.
+ */
+ public function initialize(DAV\Server $server)
+ {
+ // Using a relatively low priority (200) to allow other extensions
+ // to set the content-type first.
+ $server->on('propFind', [$this, 'propFind'], 200);
+ }
+
+ /**
+ * Our PROPFIND handler.
+ *
+ * Here we set a contenttype, if the node didn't already have one.
+ */
+ public function propFind(PropFind $propFind, INode $node)
+ {
+ $propFind->handle('{DAV:}getcontenttype', function () use ($propFind) {
+ list(, $fileName) = Uri\split($propFind->getPath());
+
+ return $this->getContentType($fileName);
+ });
+ }
+
+ /**
+ * Simple method to return the contenttype.
+ *
+ * @param string $fileName
+ *
+ * @return string
+ */
+ protected function getContentType($fileName)
+ {
+ if (null !== $fileName) {
+ // Just grabbing the extension
+ $extension = strtolower(substr($fileName, strrpos($fileName, '.') + 1));
+ if (isset($this->extensionMap[$extension])) {
+ return $this->extensionMap[$extension];
+ }
+ }
+
+ return 'application/octet-stream';
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Browser/HtmlOutput.php b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/HtmlOutput.php
new file mode 100644
index 0000000..be5a284
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/HtmlOutput.php
@@ -0,0 +1,34 @@
+baseUri = $baseUri;
+ $this->namespaceMap = $namespaceMap;
+ }
+
+ /**
+ * Generates a 'full' url based on a relative one.
+ *
+ * For relative urls, the base of the application is taken as the reference
+ * url, not the 'current url of the current request'.
+ *
+ * Absolute urls are left alone.
+ *
+ * @param string $path
+ *
+ * @return string
+ */
+ public function fullUrl($path)
+ {
+ return Uri\resolve($this->baseUri, $path);
+ }
+
+ /**
+ * Escape string for HTML output.
+ *
+ * @param scalar $input
+ *
+ * @return string
+ */
+ public function h($input)
+ {
+ return htmlspecialchars((string) $input, ENT_COMPAT, 'UTF-8');
+ }
+
+ /**
+ * Generates a full -tag.
+ *
+ * Url is automatically expanded. If label is not specified, we re-use the
+ * url.
+ *
+ * @param string $url
+ * @param string $label
+ *
+ * @return string
+ */
+ public function link($url, $label = null)
+ {
+ $url = $this->h($this->fullUrl($url));
+
+ return ''.($label ? $this->h($label) : $url).'';
+ }
+
+ /**
+ * This method takes an xml element in clark-notation, and turns it into a
+ * shortened version with a prefix, if it was a known namespace.
+ *
+ * @param string $element
+ *
+ * @return string
+ */
+ public function xmlName($element)
+ {
+ list($ns, $localName) = XmlService::parseClarkNotation($element);
+ if (isset($this->namespaceMap[$ns])) {
+ $propName = $this->namespaceMap[$ns].':'.$localName;
+ } else {
+ $propName = $element;
+ }
+
+ return ''.$this->h($propName).'';
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Browser/MapGetToPropFind.php b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/MapGetToPropFind.php
new file mode 100644
index 0000000..0bbe70c
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/MapGetToPropFind.php
@@ -0,0 +1,58 @@
+server = $server;
+ $this->server->on('method:GET', [$this, 'httpGet'], 90);
+ }
+
+ /**
+ * This method intercepts GET requests to non-files, and changes it into an HTTP PROPFIND request.
+ *
+ * @return bool
+ */
+ public function httpGet(RequestInterface $request, ResponseInterface $response)
+ {
+ $node = $this->server->tree->getNodeForPath($request->getPath());
+ if ($node instanceof DAV\IFile) {
+ return;
+ }
+
+ $subRequest = clone $request;
+ $subRequest->setMethod('PROPFIND');
+
+ $this->server->invokeMethod($subRequest, $response);
+
+ return false;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Browser/Plugin.php b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/Plugin.php
new file mode 100644
index 0000000..a8a6f43
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/Plugin.php
@@ -0,0 +1,789 @@
+enablePost = $enablePost;
+ }
+
+ /**
+ * Initializes the plugin and subscribes to events.
+ */
+ public function initialize(DAV\Server $server)
+ {
+ $this->server = $server;
+ $this->server->on('method:GET', [$this, 'httpGetEarly'], 90);
+ $this->server->on('method:GET', [$this, 'httpGet'], 200);
+ $this->server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel'], 200);
+ if ($this->enablePost) {
+ $this->server->on('method:POST', [$this, 'httpPOST']);
+ }
+ }
+
+ /**
+ * This method intercepts GET requests that have ?sabreAction=info
+ * appended to the URL.
+ */
+ public function httpGetEarly(RequestInterface $request, ResponseInterface $response)
+ {
+ $params = $request->getQueryParameters();
+ if (isset($params['sabreAction']) && 'info' === $params['sabreAction']) {
+ return $this->httpGet($request, $response);
+ }
+ }
+
+ /**
+ * This method intercepts GET requests to collections and returns the html.
+ *
+ * @return bool
+ */
+ public function httpGet(RequestInterface $request, ResponseInterface $response)
+ {
+ // We're not using straight-up $_GET, because we want everything to be
+ // unit testable.
+ $getVars = $request->getQueryParameters();
+
+ // CSP headers
+ $response->setHeader('Content-Security-Policy', "default-src 'none'; img-src 'self'; style-src 'self'; font-src 'self';");
+
+ $sabreAction = isset($getVars['sabreAction']) ? $getVars['sabreAction'] : null;
+
+ switch ($sabreAction) {
+ case 'asset':
+ // Asset handling, such as images
+ $this->serveAsset(isset($getVars['assetName']) ? $getVars['assetName'] : null);
+
+ return false;
+ default:
+ case 'info':
+ try {
+ $this->server->tree->getNodeForPath($request->getPath());
+ } catch (DAV\Exception\NotFound $e) {
+ // We're simply stopping when the file isn't found to not interfere
+ // with other plugins.
+ return;
+ }
+
+ $response->setStatus(200);
+ $response->setHeader('Content-Type', 'text/html; charset=utf-8');
+
+ $response->setBody(
+ $this->generateDirectoryIndex($request->getPath())
+ );
+
+ return false;
+
+ case 'plugins':
+ $response->setStatus(200);
+ $response->setHeader('Content-Type', 'text/html; charset=utf-8');
+
+ $response->setBody(
+ $this->generatePluginListing()
+ );
+
+ return false;
+ }
+ }
+
+ /**
+ * Handles POST requests for tree operations.
+ *
+ * @return bool
+ */
+ public function httpPOST(RequestInterface $request, ResponseInterface $response)
+ {
+ $contentType = $request->getHeader('Content-Type');
+ if (!\is_string($contentType)) {
+ return;
+ }
+ list($contentType) = explode(';', $contentType);
+ if ('application/x-www-form-urlencoded' !== $contentType &&
+ 'multipart/form-data' !== $contentType) {
+ return;
+ }
+ $postVars = $request->getPostData();
+
+ if (!isset($postVars['sabreAction'])) {
+ return;
+ }
+
+ $uri = $request->getPath();
+
+ if ($this->server->emit('onBrowserPostAction', [$uri, $postVars['sabreAction'], $postVars])) {
+ switch ($postVars['sabreAction']) {
+ case 'mkcol':
+ if (isset($postVars['name']) && trim($postVars['name'])) {
+ // Using basename() because we won't allow slashes
+ list(, $folderName) = Uri\split(trim($postVars['name']));
+
+ if (isset($postVars['resourceType'])) {
+ $resourceType = explode(',', $postVars['resourceType']);
+ } else {
+ $resourceType = ['{DAV:}collection'];
+ }
+
+ $properties = [];
+ foreach ($postVars as $varName => $varValue) {
+ // Any _POST variable in clark notation is treated
+ // like a property.
+ if ('{' === $varName[0]) {
+ // PHP will convert any dots to underscores.
+ // This leaves us with no way to differentiate
+ // the two.
+ // Therefore we replace the string *DOT* with a
+ // real dot. * is not allowed in uris so we
+ // should be good.
+ $varName = str_replace('*DOT*', '.', $varName);
+ $properties[$varName] = $varValue;
+ }
+ }
+
+ $mkCol = new MkCol(
+ $resourceType,
+ $properties
+ );
+ $this->server->createCollection($uri.'/'.$folderName, $mkCol);
+ }
+ break;
+
+ // @codeCoverageIgnoreStart
+ case 'put':
+ if ($_FILES) {
+ $file = current($_FILES);
+ } else {
+ break;
+ }
+
+ list(, $newName) = Uri\split(trim($file['name']));
+ if (isset($postVars['name']) && trim($postVars['name'])) {
+ $newName = trim($postVars['name']);
+ }
+
+ // Making sure we only have a 'basename' component
+ list(, $newName) = Uri\split($newName);
+
+ if (is_uploaded_file($file['tmp_name'])) {
+ $this->server->createFile($uri.'/'.$newName, fopen($file['tmp_name'], 'r'));
+ }
+ break;
+ // @codeCoverageIgnoreEnd
+ }
+ }
+ $response->setHeader('Location', $request->getUrl());
+ $response->setStatus(302);
+
+ return false;
+ }
+
+ /**
+ * Escapes a string for html.
+ *
+ * @param string $value
+ *
+ * @return string
+ */
+ public function escapeHTML($value)
+ {
+ return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
+ }
+
+ /**
+ * Generates the html directory index for a given url.
+ *
+ * @param string $path
+ *
+ * @return string
+ */
+ public function generateDirectoryIndex($path)
+ {
+ $html = $this->generateHeader($path ? $path : '/', $path);
+
+ $node = $this->server->tree->getNodeForPath($path);
+ if ($node instanceof DAV\ICollection) {
+ $html .= "
+
+
+ ';
+
+ return $html;
+ }
+
+ /**
+ * Generates the page footer.
+ *
+ * Returns html.
+ *
+ * @return string
+ */
+ public function generateFooter()
+ {
+ $version = '';
+ if (DAV\Server::$exposeVersion) {
+ $version = DAV\Version::VERSION;
+ }
+ $year = date('Y');
+
+ return <<Generated by SabreDAV $version (c)2007-$year http://sabre.io/
+
+
+HTML;
+ }
+
+ /**
+ * This method is used to generate the 'actions panel' output for
+ * collections.
+ *
+ * This specifically generates the interfaces for creating new files, and
+ * creating new directories.
+ *
+ * @param mixed $output
+ * @param string $path
+ */
+ public function htmlActionsPanel(DAV\INode $node, &$output, $path)
+ {
+ if (!$node instanceof DAV\ICollection) {
+ return;
+ }
+
+ // We also know fairly certain that if an object is a non-extended
+ // SimpleCollection, we won't need to show the panel either.
+ if ('Sabre\\DAV\\SimpleCollection' === get_class($node)) {
+ return;
+ }
+
+ $output .= <<
+
Create new folder
+
+
+
+
+
+HTML;
+ }
+
+ /**
+ * This method takes a path/name of an asset and turns it into url
+ * suitable for http access.
+ *
+ * @param string $assetName
+ *
+ * @return string
+ */
+ protected function getAssetUrl($assetName)
+ {
+ return $this->server->getBaseUri().'?sabreAction=asset&assetName='.urlencode($assetName);
+ }
+
+ /**
+ * This method returns a local pathname to an asset.
+ *
+ * @param string $assetName
+ *
+ * @throws DAV\Exception\NotFound
+ *
+ * @return string
+ */
+ protected function getLocalAssetPath($assetName)
+ {
+ $assetDir = __DIR__.'/assets/';
+ $path = $assetDir.$assetName;
+
+ // Making sure people aren't trying to escape from the base path.
+ $path = str_replace('\\', '/', $path);
+ if (false !== strpos($path, '/../') || '/..' === strrchr($path, '/')) {
+ throw new DAV\Exception\NotFound('Path does not exist, or escaping from the base path was detected');
+ }
+ $realPath = realpath($path);
+ if ($realPath && 0 === strpos($realPath, realpath($assetDir)) && file_exists($path)) {
+ return $path;
+ }
+ throw new DAV\Exception\NotFound('Path does not exist, or escaping from the base path was detected');
+ }
+
+ /**
+ * This method reads an asset from disk and generates a full http response.
+ *
+ * @param string $assetName
+ */
+ protected function serveAsset($assetName)
+ {
+ $assetPath = $this->getLocalAssetPath($assetName);
+
+ // Rudimentary mime type detection
+ $mime = 'application/octet-stream';
+ $map = [
+ 'ico' => 'image/vnd.microsoft.icon',
+ 'png' => 'image/png',
+ 'css' => 'text/css',
+ ];
+
+ $ext = substr($assetName, strrpos($assetName, '.') + 1);
+ if (isset($map[$ext])) {
+ $mime = $map[$ext];
+ }
+
+ $this->server->httpResponse->setHeader('Content-Type', $mime);
+ $this->server->httpResponse->setHeader('Content-Length', filesize($assetPath));
+ $this->server->httpResponse->setHeader('Cache-Control', 'public, max-age=1209600');
+ $this->server->httpResponse->setStatus(200);
+ $this->server->httpResponse->setBody(fopen($assetPath, 'r'));
+ }
+
+ /**
+ * Sort helper function: compares two directory entries based on type and
+ * display name. Collections sort above other types.
+ *
+ * @param array $a
+ * @param array $b
+ *
+ * @return int
+ */
+ protected function compareNodes($a, $b)
+ {
+ $typeA = (isset($a['{DAV:}resourcetype']))
+ ? (in_array('{DAV:}collection', $a['{DAV:}resourcetype']->getValue()))
+ : false;
+
+ $typeB = (isset($b['{DAV:}resourcetype']))
+ ? (in_array('{DAV:}collection', $b['{DAV:}resourcetype']->getValue()))
+ : false;
+
+ // If same type, sort alphabetically by filename:
+ if ($typeA === $typeB) {
+ return strnatcasecmp($a['displayPath'], $b['displayPath']);
+ }
+
+ return ($typeA < $typeB) ? 1 : -1;
+ }
+
+ /**
+ * Maps a resource type to a human-readable string and icon.
+ *
+ * @param DAV\INode $node
+ *
+ * @return array
+ */
+ private function mapResourceType(array $resourceTypes, $node)
+ {
+ if (!$resourceTypes) {
+ if ($node instanceof DAV\IFile) {
+ return [
+ 'string' => 'File',
+ 'icon' => 'file',
+ ];
+ } else {
+ return [
+ 'string' => 'Unknown',
+ 'icon' => 'cog',
+ ];
+ }
+ }
+
+ $types = [
+ '{http://calendarserver.org/ns/}calendar-proxy-write' => [
+ 'string' => 'Proxy-Write',
+ 'icon' => 'people',
+ ],
+ '{http://calendarserver.org/ns/}calendar-proxy-read' => [
+ 'string' => 'Proxy-Read',
+ 'icon' => 'people',
+ ],
+ '{urn:ietf:params:xml:ns:caldav}schedule-outbox' => [
+ 'string' => 'Outbox',
+ 'icon' => 'inbox',
+ ],
+ '{urn:ietf:params:xml:ns:caldav}schedule-inbox' => [
+ 'string' => 'Inbox',
+ 'icon' => 'inbox',
+ ],
+ '{urn:ietf:params:xml:ns:caldav}calendar' => [
+ 'string' => 'Calendar',
+ 'icon' => 'calendar',
+ ],
+ '{http://calendarserver.org/ns/}shared-owner' => [
+ 'string' => 'Shared',
+ 'icon' => 'calendar',
+ ],
+ '{http://calendarserver.org/ns/}subscribed' => [
+ 'string' => 'Subscription',
+ 'icon' => 'calendar',
+ ],
+ '{urn:ietf:params:xml:ns:carddav}directory' => [
+ 'string' => 'Directory',
+ 'icon' => 'globe',
+ ],
+ '{urn:ietf:params:xml:ns:carddav}addressbook' => [
+ 'string' => 'Address book',
+ 'icon' => 'book',
+ ],
+ '{DAV:}principal' => [
+ 'string' => 'Principal',
+ 'icon' => 'person',
+ ],
+ '{DAV:}collection' => [
+ 'string' => 'Collection',
+ 'icon' => 'folder',
+ ],
+ ];
+
+ $info = [
+ 'string' => [],
+ 'icon' => 'cog',
+ ];
+ foreach ($resourceTypes as $k => $resourceType) {
+ if (isset($types[$resourceType])) {
+ $info['string'][] = $types[$resourceType]['string'];
+ } else {
+ $info['string'][] = $resourceType;
+ }
+ }
+ foreach ($types as $key => $resourceInfo) {
+ if (in_array($key, $resourceTypes)) {
+ $info['icon'] = $resourceInfo['icon'];
+ break;
+ }
+ }
+ $info['string'] = implode(', ', $info['string']);
+
+ return $info;
+ }
+
+ /**
+ * Draws a table row for a property.
+ *
+ * @param string $name
+ * @param mixed $value
+ *
+ * @return string
+ */
+ private function drawPropertyRow($name, $value)
+ {
+ $html = new HtmlOutputHelper(
+ $this->server->getBaseUri(),
+ $this->server->xml->namespaceMap
+ );
+
+ return '
'.$html->xmlName($name).'
'.$this->drawPropertyValue($html, $value).'
';
+ }
+
+ /**
+ * Draws a table row for a property.
+ *
+ * @param HtmlOutputHelper $html
+ * @param mixed $value
+ *
+ * @return string
+ */
+ private function drawPropertyValue($html, $value)
+ {
+ if (is_scalar($value)) {
+ return $html->h($value);
+ } elseif ($value instanceof HtmlOutput) {
+ return $value->toHtml($html);
+ } elseif ($value instanceof \Sabre\Xml\XmlSerializable) {
+ // There's no default html output for this property, we're going
+ // to output the actual xml serialization instead.
+ $xml = $this->server->xml->write('{DAV:}root', $value, $this->server->getBaseUri());
+ // removing first and last line, as they contain our root
+ // element.
+ $xml = explode("\n", $xml);
+ $xml = array_slice($xml, 2, -2);
+
+ return '
'.$html->h(implode("\n", $xml)).'
';
+ } else {
+ return 'unknown';
+ }
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins;
+ * using \Sabre\DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ public function getPluginName()
+ {
+ return 'browser';
+ }
+
+ /**
+ * Returns a bunch of meta-data about the plugin.
+ *
+ * Providing this information is optional, and is mainly displayed by the
+ * Browser plugin.
+ *
+ * The description key in the returned array may contain html and will not
+ * be sanitized.
+ *
+ * @return array
+ */
+ public function getPluginInfo()
+ {
+ return [
+ 'name' => $this->getPluginName(),
+ 'description' => 'Generates HTML indexes and debug information for your sabre/dav server',
+ 'link' => 'http://sabre.io/dav/browser-plugin/',
+ ];
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Browser/PropFindAll.php b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/PropFindAll.php
new file mode 100644
index 0000000..34702bd
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/PropFindAll.php
@@ -0,0 +1,128 @@
+handle('{DAV:}displayname', function() {
+ * return 'hello';
+ * });
+ *
+ * Note that handle will only work the first time. If null is returned, the
+ * value is ignored.
+ *
+ * It's also possible to not pass a callback, but immediately pass a value
+ *
+ * @param string $propertyName
+ * @param mixed $valueOrCallBack
+ */
+ public function handle($propertyName, $valueOrCallBack)
+ {
+ if (is_callable($valueOrCallBack)) {
+ $value = $valueOrCallBack();
+ } else {
+ $value = $valueOrCallBack;
+ }
+ if (!is_null($value)) {
+ $this->result[$propertyName] = [200, $value];
+ }
+ }
+
+ /**
+ * Sets the value of the property.
+ *
+ * If status is not supplied, the status will default to 200 for non-null
+ * properties, and 404 for null properties.
+ *
+ * @param string $propertyName
+ * @param mixed $value
+ * @param int $status
+ */
+ public function set($propertyName, $value, $status = null)
+ {
+ if (is_null($status)) {
+ $status = is_null($value) ? 404 : 200;
+ }
+ $this->result[$propertyName] = [$status, $value];
+ }
+
+ /**
+ * Returns the current value for a property.
+ *
+ * @param string $propertyName
+ *
+ * @return mixed
+ */
+ public function get($propertyName)
+ {
+ return isset($this->result[$propertyName]) ? $this->result[$propertyName][1] : null;
+ }
+
+ /**
+ * Returns the current status code for a property name.
+ *
+ * If the property does not appear in the list of requested properties,
+ * null will be returned.
+ *
+ * @param string $propertyName
+ *
+ * @return int|null
+ */
+ public function getStatus($propertyName)
+ {
+ return isset($this->result[$propertyName]) ? $this->result[$propertyName][0] : 404;
+ }
+
+ /**
+ * Returns all propertynames that have a 404 status, and thus don't have a
+ * value yet.
+ *
+ * @return array
+ */
+ public function get404Properties()
+ {
+ $result = [];
+ foreach ($this->result as $propertyName => $stuff) {
+ if (404 === $stuff[0]) {
+ $result[] = $propertyName;
+ }
+ }
+ // If there's nothing in this list, we're adding one fictional item.
+ if (!$result) {
+ $result[] = '{http://sabredav.org/ns}idk';
+ }
+
+ return $result;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/favicon.ico b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/favicon.ico
new file mode 100644
index 0000000..2b2c10a
Binary files /dev/null and b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/favicon.ico differ
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/ICON-LICENSE b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/ICON-LICENSE
new file mode 100644
index 0000000..2199f4a
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/ICON-LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Waybury
+
+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.
\ No newline at end of file
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.css b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.css
new file mode 100644
index 0000000..e748674
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.css
@@ -0,0 +1,510 @@
+@font-face {
+ font-family: 'Icons';
+ src: url('?sabreAction=asset&assetName=openiconic/open-iconic.eot');
+ src: url('?sabreAction=asset&assetName=openiconic/open-iconic.eot?#iconic-sm') format('embedded-opentype'), url('?sabreAction=asset&assetName=openiconic/open-iconic.woff') format('woff'), url('?sabreAction=asset&assetName=openiconic/open-iconic.ttf') format('truetype'), url('?sabreAction=asset&assetName=openiconic/open-iconic.otf') format('opentype'), url('?sabreAction=asset&assetName=openiconic/open-iconic.svg#iconic-sm') format('svg');
+ font-weight: normal;
+ font-style: normal;
+}
+
+.oi[data-glyph].oi-text-replace {
+ font-size: 0;
+ line-height: 0;
+}
+
+.oi[data-glyph].oi-text-replace:before {
+ width: 1em;
+ text-align: center;
+}
+
+.oi[data-glyph]:before {
+ font-family: 'Icons';
+ display: inline-block;
+ speak: none;
+ line-height: 1;
+ vertical-align: baseline;
+ font-weight: normal;
+ font-style: normal;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.oi[data-glyph]:empty:before {
+ width: 1em;
+ text-align: center;
+ box-sizing: content-box;
+}
+
+.oi[data-glyph].oi-align-left:before {
+ text-align: left;
+}
+
+.oi[data-glyph].oi-align-right:before {
+ text-align: right;
+}
+
+.oi[data-glyph].oi-align-center:before {
+ text-align: center;
+}
+
+.oi[data-glyph].oi-flip-horizontal:before {
+ -webkit-transform: scale(-1, 1);
+ -ms-transform: scale(-1, 1);
+ transform: scale(-1, 1);
+}
+.oi[data-glyph].oi-flip-vertical:before {
+ -webkit-transform: scale(1, -1);
+ -ms-transform: scale(-1, 1);
+ transform: scale(1, -1);
+}
+.oi[data-glyph].oi-flip-horizontal-vertical:before {
+ -webkit-transform: scale(-1, -1);
+ -ms-transform: scale(-1, 1);
+ transform: scale(-1, -1);
+}
+
+
+.oi[data-glyph=account-login]:before { content:'\e000'; }
+
+.oi[data-glyph=account-logout]:before { content:'\e001'; }
+
+.oi[data-glyph=action-redo]:before { content:'\e002'; }
+
+.oi[data-glyph=action-undo]:before { content:'\e003'; }
+
+.oi[data-glyph=align-center]:before { content:'\e004'; }
+
+.oi[data-glyph=align-left]:before { content:'\e005'; }
+
+.oi[data-glyph=align-right]:before { content:'\e006'; }
+
+.oi[data-glyph=aperture]:before { content:'\e007'; }
+
+.oi[data-glyph=arrow-bottom]:before { content:'\e008'; }
+
+.oi[data-glyph=arrow-circle-bottom]:before { content:'\e009'; }
+
+.oi[data-glyph=arrow-circle-left]:before { content:'\e00a'; }
+
+.oi[data-glyph=arrow-circle-right]:before { content:'\e00b'; }
+
+.oi[data-glyph=arrow-circle-top]:before { content:'\e00c'; }
+
+.oi[data-glyph=arrow-left]:before { content:'\e00d'; }
+
+.oi[data-glyph=arrow-right]:before { content:'\e00e'; }
+
+.oi[data-glyph=arrow-thick-bottom]:before { content:'\e00f'; }
+
+.oi[data-glyph=arrow-thick-left]:before { content:'\e010'; }
+
+.oi[data-glyph=arrow-thick-right]:before { content:'\e011'; }
+
+.oi[data-glyph=arrow-thick-top]:before { content:'\e012'; }
+
+.oi[data-glyph=arrow-top]:before { content:'\e013'; }
+
+.oi[data-glyph=audio-spectrum]:before { content:'\e014'; }
+
+.oi[data-glyph=audio]:before { content:'\e015'; }
+
+.oi[data-glyph=badge]:before { content:'\e016'; }
+
+.oi[data-glyph=ban]:before { content:'\e017'; }
+
+.oi[data-glyph=bar-chart]:before { content:'\e018'; }
+
+.oi[data-glyph=basket]:before { content:'\e019'; }
+
+.oi[data-glyph=battery-empty]:before { content:'\e01a'; }
+
+.oi[data-glyph=battery-full]:before { content:'\e01b'; }
+
+.oi[data-glyph=beaker]:before { content:'\e01c'; }
+
+.oi[data-glyph=bell]:before { content:'\e01d'; }
+
+.oi[data-glyph=bluetooth]:before { content:'\e01e'; }
+
+.oi[data-glyph=bold]:before { content:'\e01f'; }
+
+.oi[data-glyph=bolt]:before { content:'\e020'; }
+
+.oi[data-glyph=book]:before { content:'\e021'; }
+
+.oi[data-glyph=bookmark]:before { content:'\e022'; }
+
+.oi[data-glyph=box]:before { content:'\e023'; }
+
+.oi[data-glyph=briefcase]:before { content:'\e024'; }
+
+.oi[data-glyph=british-pound]:before { content:'\e025'; }
+
+.oi[data-glyph=browser]:before { content:'\e026'; }
+
+.oi[data-glyph=brush]:before { content:'\e027'; }
+
+.oi[data-glyph=bug]:before { content:'\e028'; }
+
+.oi[data-glyph=bullhorn]:before { content:'\e029'; }
+
+.oi[data-glyph=calculator]:before { content:'\e02a'; }
+
+.oi[data-glyph=calendar]:before { content:'\e02b'; }
+
+.oi[data-glyph=camera-slr]:before { content:'\e02c'; }
+
+.oi[data-glyph=caret-bottom]:before { content:'\e02d'; }
+
+.oi[data-glyph=caret-left]:before { content:'\e02e'; }
+
+.oi[data-glyph=caret-right]:before { content:'\e02f'; }
+
+.oi[data-glyph=caret-top]:before { content:'\e030'; }
+
+.oi[data-glyph=cart]:before { content:'\e031'; }
+
+.oi[data-glyph=chat]:before { content:'\e032'; }
+
+.oi[data-glyph=check]:before { content:'\e033'; }
+
+.oi[data-glyph=chevron-bottom]:before { content:'\e034'; }
+
+.oi[data-glyph=chevron-left]:before { content:'\e035'; }
+
+.oi[data-glyph=chevron-right]:before { content:'\e036'; }
+
+.oi[data-glyph=chevron-top]:before { content:'\e037'; }
+
+.oi[data-glyph=circle-check]:before { content:'\e038'; }
+
+.oi[data-glyph=circle-x]:before { content:'\e039'; }
+
+.oi[data-glyph=clipboard]:before { content:'\e03a'; }
+
+.oi[data-glyph=clock]:before { content:'\e03b'; }
+
+.oi[data-glyph=cloud-download]:before { content:'\e03c'; }
+
+.oi[data-glyph=cloud-upload]:before { content:'\e03d'; }
+
+.oi[data-glyph=cloud]:before { content:'\e03e'; }
+
+.oi[data-glyph=cloudy]:before { content:'\e03f'; }
+
+.oi[data-glyph=code]:before { content:'\e040'; }
+
+.oi[data-glyph=cog]:before { content:'\e041'; }
+
+.oi[data-glyph=collapse-down]:before { content:'\e042'; }
+
+.oi[data-glyph=collapse-left]:before { content:'\e043'; }
+
+.oi[data-glyph=collapse-right]:before { content:'\e044'; }
+
+.oi[data-glyph=collapse-up]:before { content:'\e045'; }
+
+.oi[data-glyph=command]:before { content:'\e046'; }
+
+.oi[data-glyph=comment-square]:before { content:'\e047'; }
+
+.oi[data-glyph=compass]:before { content:'\e048'; }
+
+.oi[data-glyph=contrast]:before { content:'\e049'; }
+
+.oi[data-glyph=copywriting]:before { content:'\e04a'; }
+
+.oi[data-glyph=credit-card]:before { content:'\e04b'; }
+
+.oi[data-glyph=crop]:before { content:'\e04c'; }
+
+.oi[data-glyph=dashboard]:before { content:'\e04d'; }
+
+.oi[data-glyph=data-transfer-download]:before { content:'\e04e'; }
+
+.oi[data-glyph=data-transfer-upload]:before { content:'\e04f'; }
+
+.oi[data-glyph=delete]:before { content:'\e050'; }
+
+.oi[data-glyph=dial]:before { content:'\e051'; }
+
+.oi[data-glyph=document]:before { content:'\e052'; }
+
+.oi[data-glyph=dollar]:before { content:'\e053'; }
+
+.oi[data-glyph=double-quote-sans-left]:before { content:'\e054'; }
+
+.oi[data-glyph=double-quote-sans-right]:before { content:'\e055'; }
+
+.oi[data-glyph=double-quote-serif-left]:before { content:'\e056'; }
+
+.oi[data-glyph=double-quote-serif-right]:before { content:'\e057'; }
+
+.oi[data-glyph=droplet]:before { content:'\e058'; }
+
+.oi[data-glyph=eject]:before { content:'\e059'; }
+
+.oi[data-glyph=elevator]:before { content:'\e05a'; }
+
+.oi[data-glyph=ellipses]:before { content:'\e05b'; }
+
+.oi[data-glyph=envelope-closed]:before { content:'\e05c'; }
+
+.oi[data-glyph=envelope-open]:before { content:'\e05d'; }
+
+.oi[data-glyph=euro]:before { content:'\e05e'; }
+
+.oi[data-glyph=excerpt]:before { content:'\e05f'; }
+
+.oi[data-glyph=expand-down]:before { content:'\e060'; }
+
+.oi[data-glyph=expand-left]:before { content:'\e061'; }
+
+.oi[data-glyph=expand-right]:before { content:'\e062'; }
+
+.oi[data-glyph=expand-up]:before { content:'\e063'; }
+
+.oi[data-glyph=external-link]:before { content:'\e064'; }
+
+.oi[data-glyph=eye]:before { content:'\e065'; }
+
+.oi[data-glyph=eyedropper]:before { content:'\e066'; }
+
+.oi[data-glyph=file]:before { content:'\e067'; }
+
+.oi[data-glyph=fire]:before { content:'\e068'; }
+
+.oi[data-glyph=flag]:before { content:'\e069'; }
+
+.oi[data-glyph=flash]:before { content:'\e06a'; }
+
+.oi[data-glyph=folder]:before { content:'\e06b'; }
+
+.oi[data-glyph=fork]:before { content:'\e06c'; }
+
+.oi[data-glyph=fullscreen-enter]:before { content:'\e06d'; }
+
+.oi[data-glyph=fullscreen-exit]:before { content:'\e06e'; }
+
+.oi[data-glyph=globe]:before { content:'\e06f'; }
+
+.oi[data-glyph=graph]:before { content:'\e070'; }
+
+.oi[data-glyph=grid-four-up]:before { content:'\e071'; }
+
+.oi[data-glyph=grid-three-up]:before { content:'\e072'; }
+
+.oi[data-glyph=grid-two-up]:before { content:'\e073'; }
+
+.oi[data-glyph=hard-drive]:before { content:'\e074'; }
+
+.oi[data-glyph=header]:before { content:'\e075'; }
+
+.oi[data-glyph=headphones]:before { content:'\e076'; }
+
+.oi[data-glyph=heart]:before { content:'\e077'; }
+
+.oi[data-glyph=home]:before { content:'\e078'; }
+
+.oi[data-glyph=image]:before { content:'\e079'; }
+
+.oi[data-glyph=inbox]:before { content:'\e07a'; }
+
+.oi[data-glyph=infinity]:before { content:'\e07b'; }
+
+.oi[data-glyph=info]:before { content:'\e07c'; }
+
+.oi[data-glyph=italic]:before { content:'\e07d'; }
+
+.oi[data-glyph=justify-center]:before { content:'\e07e'; }
+
+.oi[data-glyph=justify-left]:before { content:'\e07f'; }
+
+.oi[data-glyph=justify-right]:before { content:'\e080'; }
+
+.oi[data-glyph=key]:before { content:'\e081'; }
+
+.oi[data-glyph=laptop]:before { content:'\e082'; }
+
+.oi[data-glyph=layers]:before { content:'\e083'; }
+
+.oi[data-glyph=lightbulb]:before { content:'\e084'; }
+
+.oi[data-glyph=link-broken]:before { content:'\e085'; }
+
+.oi[data-glyph=link-intact]:before { content:'\e086'; }
+
+.oi[data-glyph=list-rich]:before { content:'\e087'; }
+
+.oi[data-glyph=list]:before { content:'\e088'; }
+
+.oi[data-glyph=location]:before { content:'\e089'; }
+
+.oi[data-glyph=lock-locked]:before { content:'\e08a'; }
+
+.oi[data-glyph=lock-unlocked]:before { content:'\e08b'; }
+
+.oi[data-glyph=loop-circular]:before { content:'\e08c'; }
+
+.oi[data-glyph=loop-square]:before { content:'\e08d'; }
+
+.oi[data-glyph=loop]:before { content:'\e08e'; }
+
+.oi[data-glyph=magnifying-glass]:before { content:'\e08f'; }
+
+.oi[data-glyph=map-marker]:before { content:'\e090'; }
+
+.oi[data-glyph=map]:before { content:'\e091'; }
+
+.oi[data-glyph=media-pause]:before { content:'\e092'; }
+
+.oi[data-glyph=media-play]:before { content:'\e093'; }
+
+.oi[data-glyph=media-record]:before { content:'\e094'; }
+
+.oi[data-glyph=media-skip-backward]:before { content:'\e095'; }
+
+.oi[data-glyph=media-skip-forward]:before { content:'\e096'; }
+
+.oi[data-glyph=media-step-backward]:before { content:'\e097'; }
+
+.oi[data-glyph=media-step-forward]:before { content:'\e098'; }
+
+.oi[data-glyph=media-stop]:before { content:'\e099'; }
+
+.oi[data-glyph=medical-cross]:before { content:'\e09a'; }
+
+.oi[data-glyph=menu]:before { content:'\e09b'; }
+
+.oi[data-glyph=microphone]:before { content:'\e09c'; }
+
+.oi[data-glyph=minus]:before { content:'\e09d'; }
+
+.oi[data-glyph=monitor]:before { content:'\e09e'; }
+
+.oi[data-glyph=moon]:before { content:'\e09f'; }
+
+.oi[data-glyph=move]:before { content:'\e0a0'; }
+
+.oi[data-glyph=musical-note]:before { content:'\e0a1'; }
+
+.oi[data-glyph=paperclip]:before { content:'\e0a2'; }
+
+.oi[data-glyph=pencil]:before { content:'\e0a3'; }
+
+.oi[data-glyph=people]:before { content:'\e0a4'; }
+
+.oi[data-glyph=person]:before { content:'\e0a5'; }
+
+.oi[data-glyph=phone]:before { content:'\e0a6'; }
+
+.oi[data-glyph=pie-chart]:before { content:'\e0a7'; }
+
+.oi[data-glyph=pin]:before { content:'\e0a8'; }
+
+.oi[data-glyph=play-circle]:before { content:'\e0a9'; }
+
+.oi[data-glyph=plus]:before { content:'\e0aa'; }
+
+.oi[data-glyph=power-standby]:before { content:'\e0ab'; }
+
+.oi[data-glyph=print]:before { content:'\e0ac'; }
+
+.oi[data-glyph=project]:before { content:'\e0ad'; }
+
+.oi[data-glyph=pulse]:before { content:'\e0ae'; }
+
+.oi[data-glyph=puzzle-piece]:before { content:'\e0af'; }
+
+.oi[data-glyph=question-mark]:before { content:'\e0b0'; }
+
+.oi[data-glyph=rain]:before { content:'\e0b1'; }
+
+.oi[data-glyph=random]:before { content:'\e0b2'; }
+
+.oi[data-glyph=reload]:before { content:'\e0b3'; }
+
+.oi[data-glyph=resize-both]:before { content:'\e0b4'; }
+
+.oi[data-glyph=resize-height]:before { content:'\e0b5'; }
+
+.oi[data-glyph=resize-width]:before { content:'\e0b6'; }
+
+.oi[data-glyph=rss-alt]:before { content:'\e0b7'; }
+
+.oi[data-glyph=rss]:before { content:'\e0b8'; }
+
+.oi[data-glyph=script]:before { content:'\e0b9'; }
+
+.oi[data-glyph=share-boxed]:before { content:'\e0ba'; }
+
+.oi[data-glyph=share]:before { content:'\e0bb'; }
+
+.oi[data-glyph=shield]:before { content:'\e0bc'; }
+
+.oi[data-glyph=signal]:before { content:'\e0bd'; }
+
+.oi[data-glyph=signpost]:before { content:'\e0be'; }
+
+.oi[data-glyph=sort-ascending]:before { content:'\e0bf'; }
+
+.oi[data-glyph=sort-descending]:before { content:'\e0c0'; }
+
+.oi[data-glyph=spreadsheet]:before { content:'\e0c1'; }
+
+.oi[data-glyph=star]:before { content:'\e0c2'; }
+
+.oi[data-glyph=sun]:before { content:'\e0c3'; }
+
+.oi[data-glyph=tablet]:before { content:'\e0c4'; }
+
+.oi[data-glyph=tag]:before { content:'\e0c5'; }
+
+.oi[data-glyph=tags]:before { content:'\e0c6'; }
+
+.oi[data-glyph=target]:before { content:'\e0c7'; }
+
+.oi[data-glyph=task]:before { content:'\e0c8'; }
+
+.oi[data-glyph=terminal]:before { content:'\e0c9'; }
+
+.oi[data-glyph=text]:before { content:'\e0ca'; }
+
+.oi[data-glyph=thumb-down]:before { content:'\e0cb'; }
+
+.oi[data-glyph=thumb-up]:before { content:'\e0cc'; }
+
+.oi[data-glyph=timer]:before { content:'\e0cd'; }
+
+.oi[data-glyph=transfer]:before { content:'\e0ce'; }
+
+.oi[data-glyph=trash]:before { content:'\e0cf'; }
+
+.oi[data-glyph=underline]:before { content:'\e0d0'; }
+
+.oi[data-glyph=vertical-align-bottom]:before { content:'\e0d1'; }
+
+.oi[data-glyph=vertical-align-center]:before { content:'\e0d2'; }
+
+.oi[data-glyph=vertical-align-top]:before { content:'\e0d3'; }
+
+.oi[data-glyph=video]:before { content:'\e0d4'; }
+
+.oi[data-glyph=volume-high]:before { content:'\e0d5'; }
+
+.oi[data-glyph=volume-low]:before { content:'\e0d6'; }
+
+.oi[data-glyph=volume-off]:before { content:'\e0d7'; }
+
+.oi[data-glyph=warning]:before { content:'\e0d8'; }
+
+.oi[data-glyph=wifi]:before { content:'\e0d9'; }
+
+.oi[data-glyph=wrench]:before { content:'\e0da'; }
+
+.oi[data-glyph=x]:before { content:'\e0db'; }
+
+.oi[data-glyph=yen]:before { content:'\e0dc'; }
+
+.oi[data-glyph=zoom-in]:before { content:'\e0dd'; }
+
+.oi[data-glyph=zoom-out]:before { content:'\e0de'; }
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.eot b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.eot
new file mode 100644
index 0000000..7ca7c17
Binary files /dev/null and b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.eot differ
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.otf b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.otf
new file mode 100644
index 0000000..d79fb13
Binary files /dev/null and b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.otf differ
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.svg b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.svg
new file mode 100644
index 0000000..0792c00
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.svg
@@ -0,0 +1,543 @@
+
+
+
+
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.ttf b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.ttf
new file mode 100644
index 0000000..0f94acd
Binary files /dev/null and b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.ttf differ
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.woff b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.woff
new file mode 100644
index 0000000..793176a
Binary files /dev/null and b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.woff differ
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/sabredav.css b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/sabredav.css
new file mode 100644
index 0000000..8869597
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/sabredav.css
@@ -0,0 +1,228 @@
+/* Start of reset */
+
+* {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+body {
+ margin: 0;
+}
+
+
+/**
+ * Define consistent border, margin, and padding.
+ */
+fieldset {
+ border: 1px solid #c0c0c0;
+ margin: 0 2px;
+ padding: 0.35em 0.625em 0.75em;
+}
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+td,
+th {
+ padding: 0;
+}
+
+/** End of reset */
+
+
+body {
+ font-family: 'Roboto', sans-serif;
+ font-size: 14px;
+ line-height: 22px;
+ font-weight: 300;
+}
+h1 {
+ font-size: 42px;
+ line-height: 44px;
+ padding-bottom: 5px;
+ color: #b10610;
+ margin-top: 10px;
+ margin-bottom: 30px;
+}
+h2 {
+ color: #333333;
+ font-size: 28px;
+ line-height: 44px;
+ font-weight: 300;
+}
+h3 {
+ font-size: 21px;
+ margin-top: 20px;
+ margin-bottom: 10px;
+}
+a {
+ color: #31a1cd;
+}
+h1 a {
+ text-decoration: none;
+}
+h2 a {
+ color: #333333;
+}
+a:visited {
+ color: #6098a2;
+}
+h2 a:visited {
+ color: #333333;
+}
+a:hover {
+ color: #b10610;
+}
+hr {
+ border: none;
+ border-top: 1px dashed #c9ea75;
+ margin-top: 30px;
+ margin-bottom: 30px;
+}
+header {
+ background: #eeeeee;
+}
+header a {
+ font-size: 28px;
+ font-weight: 500;
+ color: #333;
+ text-decoration: none;
+}
+.logo {
+ padding: 5px 10px;
+}
+.logo img {
+ vertical-align: middle;
+ border: 0;
+}
+input, button, select {
+ font: inherit;
+ color: inherit;
+}
+
+input[type=text], select {
+ border: 1px solid #bbbbbb;
+ line-height: 22px;
+ padding: 5px 10px;
+ border-radius: 3px;
+}
+
+nav {
+ padding: 5px;
+}
+
+.btn, button, input[type=submit] {
+ display: inline-block;
+ color: white;
+ background: #4fa3ac;
+ padding: 9px 15px;
+ border-radius: 2px;
+ border: 0;
+ text-decoration: none;
+}
+a.btn:visited {
+ color: white;
+}
+
+.btn.disabled {
+ background: #eeeeee;
+ color: #bbbbbb;
+}
+section {
+ margin: 40px 10px;
+}
+
+section table {
+ height: 40px;
+}
+
+.nodeTable tr {
+ border-bottom: 3px solid white;
+}
+
+.nodeTable td {
+ padding: 10px 10px 10px 10px;
+
+}
+
+.nodeTable a {
+ text-decoration: none;
+}
+
+.nodeTable .nameColumn {
+ font-weight: bold;
+ padding: 10px 20px;
+ background: #ebf5f6;
+ min-width: 200px;
+}
+.nodeTable .oi {
+ color: #b10610;
+}
+
+.propTable tr {
+ height: 40px;
+}
+
+.propTable th {
+ background: #f6f6f6;
+ padding: 0 10px;
+ text-align: left;
+}
+
+.propTable td {
+ padding: 0 10px;
+ background: #eeeeee;
+}
+
+.propTable pre {
+ font-size: 80%;
+ background: #f8f8f8;
+}
+
+.actions {
+ border: 1px dotted #76baa6;
+ padding: 20px;
+ margin-bottom: 20px;
+
+}
+
+.actions h3 {
+ margin-top: 10px;
+ margin-bottom: 30px;
+ padding-bottom: 20px;
+ border-bottom: 1px solid #eeeeee;
+}
+
+.actions label {
+ width: 150px;
+ display: inline-block;
+ line-height: 40px;
+}
+
+.actions input[type=text], select {
+ width: 450px;
+}
+
+.actions input[type=submit] {
+ display: inline-block;
+ margin-left: 153px;
+}
+
+footer {
+ padding: 50px 0;
+ font-size: 80%;
+ text-align: center;
+}
+
+ul.tree {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+ul.tree ul {
+ list-style: none;
+ padding-left: 10px;
+ border-left: 4px solid #ccc;
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/sabredav.png b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/sabredav.png
new file mode 100644
index 0000000..48a9739
Binary files /dev/null and b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/sabredav.png differ
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Client.php b/lib/composer/vendor/sabre/dav/lib/DAV/Client.php
new file mode 100644
index 0000000..1028a6b
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Client.php
@@ -0,0 +1,485 @@
+xml->elementMap.
+ * It's deprecated as of version 3.0.0, and should no longer be used.
+ *
+ * @deprecated
+ *
+ * @var array
+ */
+ public $propertyMap = [];
+
+ /**
+ * Base URI.
+ *
+ * This URI will be used to resolve relative urls.
+ *
+ * @var string
+ */
+ protected $baseUri;
+
+ /**
+ * Basic authentication.
+ */
+ const AUTH_BASIC = 1;
+
+ /**
+ * Digest authentication.
+ */
+ const AUTH_DIGEST = 2;
+
+ /**
+ * NTLM authentication.
+ */
+ const AUTH_NTLM = 4;
+
+ /**
+ * Identity encoding, which basically does not nothing.
+ */
+ const ENCODING_IDENTITY = 1;
+
+ /**
+ * Deflate encoding.
+ */
+ const ENCODING_DEFLATE = 2;
+
+ /**
+ * Gzip encoding.
+ */
+ const ENCODING_GZIP = 4;
+
+ /**
+ * Sends all encoding headers.
+ */
+ const ENCODING_ALL = 7;
+
+ /**
+ * Content-encoding.
+ *
+ * @var int
+ */
+ protected $encoding = self::ENCODING_IDENTITY;
+
+ /**
+ * Constructor.
+ *
+ * Settings are provided through the 'settings' argument. The following
+ * settings are supported:
+ *
+ * * baseUri
+ * * userName (optional)
+ * * password (optional)
+ * * proxy (optional)
+ * * authType (optional)
+ * * encoding (optional)
+ *
+ * authType must be a bitmap, using self::AUTH_BASIC, self::AUTH_DIGEST
+ * and self::AUTH_NTLM. If you know which authentication method will be
+ * used, it's recommended to set it, as it will save a great deal of
+ * requests to 'discover' this information.
+ *
+ * Encoding is a bitmap with one of the ENCODING constants.
+ */
+ public function __construct(array $settings)
+ {
+ if (!isset($settings['baseUri'])) {
+ throw new \InvalidArgumentException('A baseUri must be provided');
+ }
+
+ parent::__construct();
+
+ $this->baseUri = $settings['baseUri'];
+
+ if (isset($settings['proxy'])) {
+ $this->addCurlSetting(CURLOPT_PROXY, $settings['proxy']);
+ }
+
+ if (isset($settings['userName'])) {
+ $userName = $settings['userName'];
+ $password = isset($settings['password']) ? $settings['password'] : '';
+
+ if (isset($settings['authType'])) {
+ $curlType = 0;
+ if ($settings['authType'] & self::AUTH_BASIC) {
+ $curlType |= CURLAUTH_BASIC;
+ }
+ if ($settings['authType'] & self::AUTH_DIGEST) {
+ $curlType |= CURLAUTH_DIGEST;
+ }
+ if ($settings['authType'] & self::AUTH_NTLM) {
+ $curlType |= CURLAUTH_NTLM;
+ }
+ } else {
+ $curlType = CURLAUTH_BASIC | CURLAUTH_DIGEST;
+ }
+
+ $this->addCurlSetting(CURLOPT_HTTPAUTH, $curlType);
+ $this->addCurlSetting(CURLOPT_USERPWD, $userName.':'.$password);
+ }
+
+ if (isset($settings['encoding'])) {
+ $encoding = $settings['encoding'];
+
+ $encodings = [];
+ if ($encoding & self::ENCODING_IDENTITY) {
+ $encodings[] = 'identity';
+ }
+ if ($encoding & self::ENCODING_DEFLATE) {
+ $encodings[] = 'deflate';
+ }
+ if ($encoding & self::ENCODING_GZIP) {
+ $encodings[] = 'gzip';
+ }
+ $this->addCurlSetting(CURLOPT_ENCODING, implode(',', $encodings));
+ }
+
+ $this->addCurlSetting(CURLOPT_USERAGENT, 'sabre-dav/'.Version::VERSION.' (http://sabre.io/)');
+
+ $this->xml = new Xml\Service();
+ // BC
+ $this->propertyMap = &$this->xml->elementMap;
+ }
+
+ /**
+ * Does a PROPFIND request with filtered response returning only available properties.
+ *
+ * The list of requested properties must be specified as an array, in clark
+ * notation.
+ *
+ * Depth should be either 0 or 1. A depth of 1 will cause a request to be
+ * made to the server to also return all child resources.
+ *
+ * For depth 0, just the array of properties for the resource is returned.
+ *
+ * For depth 1, the returned array will contain a list of resource names as keys,
+ * and an array of properties as values.
+ *
+ * The array of properties will contain the properties as keys with their values as the value.
+ * Only properties that are actually returned from the server without error will be
+ * returned, anything else is discarded.
+ *
+ * @param 1|0 $depth
+ */
+ public function propFind($url, array $properties, $depth = 0): array
+ {
+ $result = $this->doPropFind($url, $properties, $depth);
+
+ // If depth was 0, we only return the top item
+ if (0 === $depth) {
+ reset($result);
+ $result = current($result);
+
+ return isset($result[200]) ? $result[200] : [];
+ }
+
+ $newResult = [];
+ foreach ($result as $href => $statusList) {
+ $newResult[$href] = isset($statusList[200]) ? $statusList[200] : [];
+ }
+
+ return $newResult;
+ }
+
+ /**
+ * Does a PROPFIND request with unfiltered response.
+ *
+ * The list of requested properties must be specified as an array, in clark
+ * notation.
+ *
+ * Depth should be either 0 or 1. A depth of 1 will cause a request to be
+ * made to the server to also return all child resources.
+ *
+ * For depth 0, just the multi-level array of status and properties for the resource is returned.
+ *
+ * For depth 1, the returned array will contain a list of resources as keys and
+ * a multi-level array containing status and properties as value.
+ *
+ * The multi-level array of status and properties is formatted the same as what is
+ * documented for parseMultiStatus.
+ *
+ * All properties that are actually returned from the server are returned by this method.
+ *
+ * @param 1|0 $depth
+ */
+ public function propFindUnfiltered(string $url, array $properties, int $depth = 0): array
+ {
+ $result = $this->doPropFind($url, $properties, $depth);
+
+ // If depth was 0, we only return the top item
+ if (0 === $depth) {
+ reset($result);
+
+ return current($result);
+ } else {
+ return $result;
+ }
+ }
+
+ /**
+ * Does a PROPFIND request.
+ *
+ * The list of requested properties must be specified as an array, in clark
+ * notation.
+ *
+ * Depth should be either 0 or 1. A depth of 1 will cause a request to be
+ * made to the server to also return all child resources.
+ *
+ * The returned array will contain a list of resources as keys and
+ * a multi-level array containing status and properties as value.
+ *
+ * The multi-level array of status and properties is formatted the same as what is
+ * documented for parseMultiStatus.
+ *
+ * @param 1|0 $depth
+ */
+ private function doPropFind($url, array $properties, $depth = 0): array
+ {
+ $dom = new \DOMDocument('1.0', 'UTF-8');
+ $dom->formatOutput = true;
+ $root = $dom->createElementNS('DAV:', 'd:propfind');
+ $prop = $dom->createElement('d:prop');
+
+ foreach ($properties as $property) {
+ list(
+ $namespace,
+ $elementName
+ ) = \Sabre\Xml\Service::parseClarkNotation($property);
+
+ if ('DAV:' === $namespace) {
+ $element = $dom->createElement('d:'.$elementName);
+ } else {
+ $element = $dom->createElementNS($namespace, 'x:'.$elementName);
+ }
+
+ $prop->appendChild($element);
+ }
+
+ $dom->appendChild($root)->appendChild($prop);
+ $body = $dom->saveXML();
+
+ $url = $this->getAbsoluteUrl($url);
+
+ $request = new HTTP\Request('PROPFIND', $url, [
+ 'Depth' => $depth,
+ 'Content-Type' => 'application/xml',
+ ], $body);
+
+ $response = $this->send($request);
+
+ if ((int) $response->getStatus() >= 400) {
+ throw new HTTP\ClientHttpException($response);
+ }
+
+ return $this->parseMultiStatus($response->getBodyAsString());
+ }
+
+ /**
+ * Updates a list of properties on the server.
+ *
+ * The list of properties must have clark-notation properties for the keys,
+ * and the actual (string) value for the value. If the value is null, an
+ * attempt is made to delete the property.
+ *
+ * @param string $url
+ *
+ * @return bool
+ */
+ public function propPatch($url, array $properties)
+ {
+ $propPatch = new Xml\Request\PropPatch();
+ $propPatch->properties = $properties;
+ $xml = $this->xml->write(
+ '{DAV:}propertyupdate',
+ $propPatch
+ );
+
+ $url = $this->getAbsoluteUrl($url);
+ $request = new HTTP\Request('PROPPATCH', $url, [
+ 'Content-Type' => 'application/xml',
+ ], $xml);
+ $response = $this->send($request);
+
+ if ($response->getStatus() >= 400) {
+ throw new HTTP\ClientHttpException($response);
+ }
+
+ if (207 === $response->getStatus()) {
+ // If it's a 207, the request could still have failed, but the
+ // information is hidden in the response body.
+ $result = $this->parseMultiStatus($response->getBodyAsString());
+
+ $errorProperties = [];
+ foreach ($result as $href => $statusList) {
+ foreach ($statusList as $status => $properties) {
+ if ($status >= 400) {
+ foreach ($properties as $propName => $propValue) {
+ $errorProperties[] = $propName.' ('.$status.')';
+ }
+ }
+ }
+ }
+ if ($errorProperties) {
+ throw new HTTP\ClientException('PROPPATCH failed. The following properties errored: '.implode(', ', $errorProperties));
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Performs an HTTP options request.
+ *
+ * This method returns all the features from the 'DAV:' header as an array.
+ * If there was no DAV header, or no contents this method will return an
+ * empty array.
+ *
+ * @return array
+ */
+ public function options()
+ {
+ $request = new HTTP\Request('OPTIONS', $this->getAbsoluteUrl(''));
+ $response = $this->send($request);
+
+ $dav = $response->getHeader('Dav');
+ if (!$dav) {
+ return [];
+ }
+
+ $features = explode(',', $dav);
+ foreach ($features as &$v) {
+ $v = trim($v);
+ }
+
+ return $features;
+ }
+
+ /**
+ * Performs an actual HTTP request, and returns the result.
+ *
+ * If the specified url is relative, it will be expanded based on the base
+ * url.
+ *
+ * The returned array contains 3 keys:
+ * * body - the response body
+ * * httpCode - a HTTP code (200, 404, etc)
+ * * headers - a list of response http headers. The header names have
+ * been lowercased.
+ *
+ * For large uploads, it's highly recommended to specify body as a stream
+ * resource. You can easily do this by simply passing the result of
+ * fopen(..., 'r').
+ *
+ * This method will throw an exception if an HTTP error was received. Any
+ * HTTP status code above 399 is considered an error.
+ *
+ * Note that it is no longer recommended to use this method, use the send()
+ * method instead.
+ *
+ * @param string $method
+ * @param string $url
+ * @param string|resource|null $body
+ *
+ * @throws clientException, in case a curl error occurred
+ *
+ * @return array
+ */
+ public function request($method, $url = '', $body = null, array $headers = [])
+ {
+ $url = $this->getAbsoluteUrl($url);
+
+ $response = $this->send(new HTTP\Request($method, $url, $headers, $body));
+
+ return [
+ 'body' => $response->getBodyAsString(),
+ 'statusCode' => (int) $response->getStatus(),
+ 'headers' => array_change_key_case($response->getHeaders()),
+ ];
+ }
+
+ /**
+ * Returns the full url based on the given url (which may be relative). All
+ * urls are expanded based on the base url as given by the server.
+ *
+ * @param string $url
+ *
+ * @return string
+ */
+ public function getAbsoluteUrl($url)
+ {
+ return Uri\resolve(
+ $this->baseUri,
+ (string) $url
+ );
+ }
+
+ /**
+ * Parses a WebDAV multistatus response body.
+ *
+ * This method returns an array with the following structure
+ *
+ * [
+ * 'url/to/resource' => [
+ * '200' => [
+ * '{DAV:}property1' => 'value1',
+ * '{DAV:}property2' => 'value2',
+ * ],
+ * '404' => [
+ * '{DAV:}property1' => null,
+ * '{DAV:}property2' => null,
+ * ],
+ * ],
+ * 'url/to/resource2' => [
+ * .. etc ..
+ * ]
+ * ]
+ *
+ * @param string $body xml body
+ *
+ * @return array
+ */
+ public function parseMultiStatus($body)
+ {
+ $multistatus = $this->xml->expect('{DAV:}multistatus', $body);
+
+ $result = [];
+
+ foreach ($multistatus->getResponses() as $response) {
+ $result[$response->getHref()] = $response->getResponseProperties();
+ }
+
+ return $result;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Collection.php b/lib/composer/vendor/sabre/dav/lib/DAV/Collection.php
new file mode 100644
index 0000000..2728bb2
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Collection.php
@@ -0,0 +1,106 @@
+getChildren() as $child) {
+ if ($child->getName() === $name) {
+ return $child;
+ }
+ }
+ throw new Exception\NotFound('File not found: '.$name);
+ }
+
+ /**
+ * Checks is a child-node exists.
+ *
+ * It is generally a good idea to try and override this. Usually it can be optimized.
+ *
+ * @param string $name
+ *
+ * @return bool
+ */
+ public function childExists($name)
+ {
+ try {
+ $this->getChild($name);
+
+ return true;
+ } catch (Exception\NotFound $e) {
+ return false;
+ }
+ }
+
+ /**
+ * Creates a new file in the directory.
+ *
+ * Data will either be supplied as a stream resource, or in certain cases
+ * as a string. Keep in mind that you may have to support either.
+ *
+ * After successful creation of the file, you may choose to return the ETag
+ * of the new file here.
+ *
+ * The returned ETag must be surrounded by double-quotes (The quotes should
+ * be part of the actual string).
+ *
+ * If you cannot accurately determine the ETag, you should not return it.
+ * If you don't store the file exactly as-is (you're transforming it
+ * somehow) you should also not return an ETag.
+ *
+ * This means that if a subsequent GET to this new file does not exactly
+ * return the same contents of what was submitted here, you are strongly
+ * recommended to omit the ETag.
+ *
+ * @param string $name Name of the file
+ * @param resource|string $data Initial payload
+ *
+ * @return string|null
+ */
+ public function createFile($name, $data = null)
+ {
+ throw new Exception\Forbidden('Permission denied to create file (filename '.$name.')');
+ }
+
+ /**
+ * Creates a new subdirectory.
+ *
+ * @param string $name
+ *
+ * @throws Exception\Forbidden
+ */
+ public function createDirectory($name)
+ {
+ throw new Exception\Forbidden('Permission denied to create directory');
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/CorePlugin.php b/lib/composer/vendor/sabre/dav/lib/DAV/CorePlugin.php
new file mode 100644
index 0000000..dbd8976
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/CorePlugin.php
@@ -0,0 +1,907 @@
+server = $server;
+ $server->on('method:GET', [$this, 'httpGet']);
+ $server->on('method:OPTIONS', [$this, 'httpOptions']);
+ $server->on('method:HEAD', [$this, 'httpHead']);
+ $server->on('method:DELETE', [$this, 'httpDelete']);
+ $server->on('method:PROPFIND', [$this, 'httpPropFind']);
+ $server->on('method:PROPPATCH', [$this, 'httpPropPatch']);
+ $server->on('method:PUT', [$this, 'httpPut']);
+ $server->on('method:MKCOL', [$this, 'httpMkcol']);
+ $server->on('method:MOVE', [$this, 'httpMove']);
+ $server->on('method:COPY', [$this, 'httpCopy']);
+ $server->on('method:REPORT', [$this, 'httpReport']);
+
+ $server->on('propPatch', [$this, 'propPatchProtectedPropertyCheck'], 90);
+ $server->on('propPatch', [$this, 'propPatchNodeUpdate'], 200);
+ $server->on('propFind', [$this, 'propFind']);
+ $server->on('propFind', [$this, 'propFindNode'], 120);
+ $server->on('propFind', [$this, 'propFindLate'], 200);
+
+ $server->on('exception', [$this, 'exception']);
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ public function getPluginName()
+ {
+ return 'core';
+ }
+
+ /**
+ * This is the default implementation for the GET method.
+ *
+ * @return bool
+ */
+ public function httpGet(RequestInterface $request, ResponseInterface $response)
+ {
+ $path = $request->getPath();
+ $node = $this->server->tree->getNodeForPath($path);
+
+ if (!$node instanceof IFile) {
+ return;
+ }
+
+ if ('HEAD' === $request->getHeader('X-Sabre-Original-Method')) {
+ $body = '';
+ } else {
+ $body = $node->get();
+
+ // Converting string into stream, if needed.
+ if (is_string($body)) {
+ $stream = fopen('php://temp', 'r+');
+ fwrite($stream, $body);
+ rewind($stream);
+ $body = $stream;
+ }
+ }
+
+ /*
+ * TODO: getetag, getlastmodified, getsize should also be used using
+ * this method
+ */
+ $httpHeaders = $this->server->getHTTPHeaders($path);
+
+ /* ContentType needs to get a default, because many webservers will otherwise
+ * default to text/html, and we don't want this for security reasons.
+ */
+ if (!isset($httpHeaders['Content-Type'])) {
+ $httpHeaders['Content-Type'] = 'application/octet-stream';
+ }
+
+ if (isset($httpHeaders['Content-Length'])) {
+ $nodeSize = $httpHeaders['Content-Length'];
+
+ // Need to unset Content-Length, because we'll handle that during figuring out the range
+ unset($httpHeaders['Content-Length']);
+ } else {
+ $nodeSize = null;
+ }
+
+ $response->addHeaders($httpHeaders);
+
+ $range = $this->server->getHTTPRange();
+ $ifRange = $request->getHeader('If-Range');
+ $ignoreRangeHeader = false;
+
+ // If ifRange is set, and range is specified, we first need to check
+ // the precondition.
+ if ($nodeSize && $range && $ifRange) {
+ // if IfRange is parsable as a date we'll treat it as a DateTime
+ // otherwise, we must treat it as an etag.
+ try {
+ $ifRangeDate = new \DateTime($ifRange);
+
+ // It's a date. We must check if the entity is modified since
+ // the specified date.
+ if (!isset($httpHeaders['Last-Modified'])) {
+ $ignoreRangeHeader = true;
+ } else {
+ $modified = new \DateTime($httpHeaders['Last-Modified']);
+ if ($modified > $ifRangeDate) {
+ $ignoreRangeHeader = true;
+ }
+ }
+ } catch (\Exception $e) {
+ // It's an entity. We can do a simple comparison.
+ if (!isset($httpHeaders['ETag'])) {
+ $ignoreRangeHeader = true;
+ } elseif ($httpHeaders['ETag'] !== $ifRange) {
+ $ignoreRangeHeader = true;
+ }
+ }
+ }
+
+ // We're only going to support HTTP ranges if the backend provided a filesize
+ if (!$ignoreRangeHeader && $nodeSize && $range) {
+ // Determining the exact byte offsets
+ if (!is_null($range[0])) {
+ $start = $range[0];
+ $end = $range[1] ? $range[1] : $nodeSize - 1;
+ if ($start >= $nodeSize) {
+ throw new Exception\RequestedRangeNotSatisfiable('The start offset ('.$range[0].') exceeded the size of the entity ('.$nodeSize.')');
+ }
+ if ($end < $start) {
+ throw new Exception\RequestedRangeNotSatisfiable('The end offset ('.$range[1].') is lower than the start offset ('.$range[0].')');
+ }
+ if ($end >= $nodeSize) {
+ $end = $nodeSize - 1;
+ }
+ } else {
+ $start = $nodeSize - $range[1];
+ $end = $nodeSize - 1;
+
+ if ($start < 0) {
+ $start = 0;
+ }
+ }
+
+ // Streams may advertise themselves as seekable, but still not
+ // actually allow fseek. We'll manually go forward in the stream
+ // if fseek failed.
+ if (!stream_get_meta_data($body)['seekable'] || -1 === fseek($body, $start, SEEK_SET)) {
+ $consumeBlock = 8192;
+ for ($consumed = 0; $start - $consumed > 0;) {
+ if (feof($body)) {
+ throw new Exception\RequestedRangeNotSatisfiable('The start offset ('.$start.') exceeded the size of the entity ('.$consumed.')');
+ }
+ $consumed += strlen(fread($body, min($start - $consumed, $consumeBlock)));
+ }
+ }
+
+ $response->setHeader('Content-Length', $end - $start + 1);
+ $response->setHeader('Content-Range', 'bytes '.$start.'-'.$end.'/'.$nodeSize);
+ $response->setStatus(206);
+ $response->setBody($body);
+ } else {
+ if ($nodeSize) {
+ $response->setHeader('Content-Length', $nodeSize);
+ }
+ $response->setStatus(200);
+ $response->setBody($body);
+ }
+ // Sending back false will interrupt the event chain and tell the server
+ // we've handled this method.
+ return false;
+ }
+
+ /**
+ * HTTP OPTIONS.
+ *
+ * @return bool
+ */
+ public function httpOptions(RequestInterface $request, ResponseInterface $response)
+ {
+ $methods = $this->server->getAllowedMethods($request->getPath());
+
+ $response->setHeader('Allow', strtoupper(implode(', ', $methods)));
+ $features = ['1', '3', 'extended-mkcol'];
+
+ foreach ($this->server->getPlugins() as $plugin) {
+ $features = array_merge($features, $plugin->getFeatures());
+ }
+
+ $response->setHeader('DAV', implode(', ', $features));
+ $response->setHeader('MS-Author-Via', 'DAV');
+ $response->setHeader('Accept-Ranges', 'bytes');
+ $response->setHeader('Content-Length', '0');
+ $response->setStatus(200);
+
+ // Sending back false will interrupt the event chain and tell the server
+ // we've handled this method.
+ return false;
+ }
+
+ /**
+ * HTTP HEAD.
+ *
+ * This method is normally used to take a peak at a url, and only get the
+ * HTTP response headers, without the body. This is used by clients to
+ * determine if a remote file was changed, so they can use a local cached
+ * version, instead of downloading it again
+ *
+ * @return bool
+ */
+ public function httpHead(RequestInterface $request, ResponseInterface $response)
+ {
+ // This is implemented by changing the HEAD request to a GET request,
+ // and telling the request handler that is doesn't need to create the body.
+ $subRequest = clone $request;
+ $subRequest->setMethod('GET');
+ $subRequest->setHeader('X-Sabre-Original-Method', 'HEAD');
+
+ try {
+ $this->server->invokeMethod($subRequest, $response, false);
+ } catch (Exception\NotImplemented $e) {
+ // Some clients may do HEAD requests on collections, however, GET
+ // requests and HEAD requests _may_ not be defined on a collection,
+ // which would trigger a 501.
+ // This breaks some clients though, so we're transforming these
+ // 501s into 200s.
+ $response->setStatus(200);
+ $response->setBody('');
+ $response->setHeader('Content-Type', 'text/plain');
+ $response->setHeader('X-Sabre-Real-Status', $e->getHTTPCode());
+ }
+
+ // Sending back false will interrupt the event chain and tell the server
+ // we've handled this method.
+ return false;
+ }
+
+ /**
+ * HTTP Delete.
+ *
+ * The HTTP delete method, deletes a given uri
+ */
+ public function httpDelete(RequestInterface $request, ResponseInterface $response)
+ {
+ $path = $request->getPath();
+
+ if (!$this->server->emit('beforeUnbind', [$path])) {
+ return false;
+ }
+ $this->server->tree->delete($path);
+ $this->server->emit('afterUnbind', [$path]);
+
+ $response->setStatus(204);
+ $response->setHeader('Content-Length', '0');
+
+ // Sending back false will interrupt the event chain and tell the server
+ // we've handled this method.
+ return false;
+ }
+
+ /**
+ * WebDAV PROPFIND.
+ *
+ * This WebDAV method requests information about an uri resource, or a list of resources
+ * If a client wants to receive the properties for a single resource it will add an HTTP Depth: header with a 0 value
+ * If the value is 1, it means that it also expects a list of sub-resources (e.g.: files in a directory)
+ *
+ * The request body contains an XML data structure that has a list of properties the client understands
+ * The response body is also an xml document, containing information about every uri resource and the requested properties
+ *
+ * It has to return a HTTP 207 Multi-status status code
+ */
+ public function httpPropFind(RequestInterface $request, ResponseInterface $response)
+ {
+ $path = $request->getPath();
+
+ $requestBody = $request->getBodyAsString();
+ if (strlen($requestBody)) {
+ try {
+ $propFindXml = $this->server->xml->expect('{DAV:}propfind', $requestBody);
+ } catch (ParseException $e) {
+ throw new BadRequest($e->getMessage(), 0, $e);
+ }
+ } else {
+ $propFindXml = new Xml\Request\PropFind();
+ $propFindXml->allProp = true;
+ $propFindXml->properties = [];
+ }
+
+ $depth = $this->server->getHTTPDepth(1);
+ // The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled
+ if (!$this->server->enablePropfindDepthInfinity && 0 != $depth) {
+ $depth = 1;
+ }
+
+ $newProperties = $this->server->getPropertiesIteratorForPath($path, $propFindXml->properties, $depth);
+
+ // This is a multi-status response
+ $response->setStatus(207);
+ $response->setHeader('Content-Type', 'application/xml; charset=utf-8');
+ $response->setHeader('Vary', 'Brief,Prefer');
+
+ // Normally this header is only needed for OPTIONS responses, however..
+ // iCal seems to also depend on these being set for PROPFIND. Since
+ // this is not harmful, we'll add it.
+ $features = ['1', '3', 'extended-mkcol'];
+ foreach ($this->server->getPlugins() as $plugin) {
+ $features = array_merge($features, $plugin->getFeatures());
+ }
+ $response->setHeader('DAV', implode(', ', $features));
+
+ $prefer = $this->server->getHTTPPrefer();
+ $minimal = 'minimal' === $prefer['return'];
+
+ $data = $this->server->generateMultiStatus($newProperties, $minimal);
+ $response->setBody($data);
+
+ // Sending back false will interrupt the event chain and tell the server
+ // we've handled this method.
+ return false;
+ }
+
+ /**
+ * WebDAV PROPPATCH.
+ *
+ * This method is called to update properties on a Node. The request is an XML body with all the mutations.
+ * In this XML body it is specified which properties should be set/updated and/or deleted
+ *
+ * @return bool
+ */
+ public function httpPropPatch(RequestInterface $request, ResponseInterface $response)
+ {
+ $path = $request->getPath();
+
+ try {
+ $propPatch = $this->server->xml->expect('{DAV:}propertyupdate', $request->getBody());
+ } catch (ParseException $e) {
+ throw new BadRequest($e->getMessage(), 0, $e);
+ }
+ $newProperties = $propPatch->properties;
+
+ $result = $this->server->updateProperties($path, $newProperties);
+
+ $prefer = $this->server->getHTTPPrefer();
+ $response->setHeader('Vary', 'Brief,Prefer');
+
+ if ('minimal' === $prefer['return']) {
+ // If return-minimal is specified, we only have to check if the
+ // request was successful, and don't need to return the
+ // multi-status.
+ $ok = true;
+ foreach ($result as $prop => $code) {
+ if ((int) $code > 299) {
+ $ok = false;
+ }
+ }
+
+ if ($ok) {
+ $response->setStatus(204);
+
+ return false;
+ }
+ }
+
+ $response->setStatus(207);
+ $response->setHeader('Content-Type', 'application/xml; charset=utf-8');
+
+ // Reorganizing the result for generateMultiStatus
+ $multiStatus = [];
+ foreach ($result as $propertyName => $code) {
+ if (isset($multiStatus[$code])) {
+ $multiStatus[$code][$propertyName] = null;
+ } else {
+ $multiStatus[$code] = [$propertyName => null];
+ }
+ }
+ $multiStatus['href'] = $path;
+
+ $response->setBody(
+ $this->server->generateMultiStatus([$multiStatus])
+ );
+
+ // Sending back false will interrupt the event chain and tell the server
+ // we've handled this method.
+ return false;
+ }
+
+ /**
+ * HTTP PUT method.
+ *
+ * This HTTP method updates a file, or creates a new one.
+ *
+ * If a new resource was created, a 201 Created status code should be returned. If an existing resource is updated, it's a 204 No Content
+ *
+ * @return bool
+ */
+ public function httpPut(RequestInterface $request, ResponseInterface $response)
+ {
+ $body = $request->getBodyAsStream();
+ $path = $request->getPath();
+
+ // Intercepting Content-Range
+ if ($request->getHeader('Content-Range')) {
+ /*
+ An origin server that allows PUT on a given target resource MUST send
+ a 400 (Bad Request) response to a PUT request that contains a
+ Content-Range header field.
+
+ Reference: http://tools.ietf.org/html/rfc7231#section-4.3.4
+ */
+ throw new Exception\BadRequest('Content-Range on PUT requests are forbidden.');
+ }
+
+ // Intercepting the Finder problem
+ if (($expected = $request->getHeader('X-Expected-Entity-Length')) && $expected > 0) {
+ /*
+ Many webservers will not cooperate well with Finder PUT requests,
+ because it uses 'Chunked' transfer encoding for the request body.
+
+ The symptom of this problem is that Finder sends files to the
+ server, but they arrive as 0-length files in PHP.
+
+ If we don't do anything, the user might think they are uploading
+ files successfully, but they end up empty on the server. Instead,
+ we throw back an error if we detect this.
+
+ The reason Finder uses Chunked, is because it thinks the files
+ might change as it's being uploaded, and therefore the
+ Content-Length can vary.
+
+ Instead it sends the X-Expected-Entity-Length header with the size
+ of the file at the very start of the request. If this header is set,
+ but we don't get a request body we will fail the request to
+ protect the end-user.
+ */
+
+ // Only reading first byte
+ $firstByte = fread($body, 1);
+ if (1 !== strlen($firstByte)) {
+ throw new Exception\Forbidden('This server is not compatible with OS/X finder. Consider using a different WebDAV client or webserver.');
+ }
+
+ // The body needs to stay intact, so we copy everything to a
+ // temporary stream.
+
+ $newBody = fopen('php://temp', 'r+');
+ fwrite($newBody, $firstByte);
+ stream_copy_to_stream($body, $newBody);
+ rewind($newBody);
+
+ $body = $newBody;
+ }
+
+ if ($this->server->tree->nodeExists($path)) {
+ $node = $this->server->tree->getNodeForPath($path);
+
+ // If the node is a collection, we'll deny it
+ if (!($node instanceof IFile)) {
+ throw new Exception\Conflict('PUT is not allowed on non-files.');
+ }
+ if (!$this->server->updateFile($path, $body, $etag)) {
+ return false;
+ }
+
+ $response->setHeader('Content-Length', '0');
+ if ($etag) {
+ $response->setHeader('ETag', $etag);
+ }
+ $response->setStatus(204);
+ } else {
+ $etag = null;
+ // If we got here, the resource didn't exist yet.
+ if (!$this->server->createFile($path, $body, $etag)) {
+ // For one reason or another the file was not created.
+ return false;
+ }
+
+ $response->setHeader('Content-Length', '0');
+ if ($etag) {
+ $response->setHeader('ETag', $etag);
+ }
+ $response->setStatus(201);
+ }
+
+ // Sending back false will interrupt the event chain and tell the server
+ // we've handled this method.
+ return false;
+ }
+
+ /**
+ * WebDAV MKCOL.
+ *
+ * The MKCOL method is used to create a new collection (directory) on the server
+ *
+ * @return bool
+ */
+ public function httpMkcol(RequestInterface $request, ResponseInterface $response)
+ {
+ $requestBody = $request->getBodyAsString();
+ $path = $request->getPath();
+
+ if ($requestBody) {
+ $contentType = $request->getHeader('Content-Type');
+ if (null === $contentType || (0 !== strpos($contentType, 'application/xml') && 0 !== strpos($contentType, 'text/xml'))) {
+ // We must throw 415 for unsupported mkcol bodies
+ throw new Exception\UnsupportedMediaType('The request body for the MKCOL request must have an xml Content-Type');
+ }
+
+ try {
+ $mkcol = $this->server->xml->expect('{DAV:}mkcol', $requestBody);
+ } catch (\Sabre\Xml\ParseException $e) {
+ throw new Exception\BadRequest($e->getMessage(), 0, $e);
+ }
+
+ $properties = $mkcol->getProperties();
+
+ if (!isset($properties['{DAV:}resourcetype'])) {
+ throw new Exception\BadRequest('The mkcol request must include a {DAV:}resourcetype property');
+ }
+ $resourceType = $properties['{DAV:}resourcetype']->getValue();
+ unset($properties['{DAV:}resourcetype']);
+ } else {
+ $properties = [];
+ $resourceType = ['{DAV:}collection'];
+ }
+
+ $mkcol = new MkCol($resourceType, $properties);
+
+ $result = $this->server->createCollection($path, $mkcol);
+
+ if (is_array($result)) {
+ $response->setStatus(207);
+ $response->setHeader('Content-Type', 'application/xml; charset=utf-8');
+
+ $response->setBody(
+ $this->server->generateMultiStatus([$result])
+ );
+ } else {
+ $response->setHeader('Content-Length', '0');
+ $response->setStatus(201);
+ }
+
+ // Sending back false will interrupt the event chain and tell the server
+ // we've handled this method.
+ return false;
+ }
+
+ /**
+ * WebDAV HTTP MOVE method.
+ *
+ * This method moves one uri to a different uri. A lot of the actual request processing is done in getCopyMoveInfo
+ *
+ * @return bool
+ */
+ public function httpMove(RequestInterface $request, ResponseInterface $response)
+ {
+ $path = $request->getPath();
+
+ $moveInfo = $this->server->getCopyAndMoveInfo($request);
+
+ if ($moveInfo['destinationExists']) {
+ if (!$this->server->emit('beforeUnbind', [$moveInfo['destination']])) {
+ return false;
+ }
+ }
+ if (!$this->server->emit('beforeUnbind', [$path])) {
+ return false;
+ }
+ if (!$this->server->emit('beforeBind', [$moveInfo['destination']])) {
+ return false;
+ }
+ if (!$this->server->emit('beforeMove', [$path, $moveInfo['destination']])) {
+ return false;
+ }
+
+ if ($moveInfo['destinationExists']) {
+ $this->server->tree->delete($moveInfo['destination']);
+ $this->server->emit('afterUnbind', [$moveInfo['destination']]);
+ }
+
+ $this->server->tree->move($path, $moveInfo['destination']);
+
+ // Its important afterMove is called before afterUnbind, because it
+ // allows systems to transfer data from one path to another.
+ // PropertyStorage uses this. If afterUnbind was first, it would clean
+ // up all the properties before it has a chance.
+ $this->server->emit('afterMove', [$path, $moveInfo['destination']]);
+ $this->server->emit('afterUnbind', [$path]);
+ $this->server->emit('afterBind', [$moveInfo['destination']]);
+
+ // If a resource was overwritten we should send a 204, otherwise a 201
+ $response->setHeader('Content-Length', '0');
+ $response->setStatus($moveInfo['destinationExists'] ? 204 : 201);
+
+ // Sending back false will interrupt the event chain and tell the server
+ // we've handled this method.
+ return false;
+ }
+
+ /**
+ * WebDAV HTTP COPY method.
+ *
+ * This method copies one uri to a different uri, and works much like the MOVE request
+ * A lot of the actual request processing is done in getCopyMoveInfo
+ *
+ * @return bool
+ */
+ public function httpCopy(RequestInterface $request, ResponseInterface $response)
+ {
+ $path = $request->getPath();
+
+ $copyInfo = $this->server->getCopyAndMoveInfo($request);
+
+ if (!$this->server->emit('beforeBind', [$copyInfo['destination']])) {
+ return false;
+ }
+ if (!$this->server->emit('beforeCopy', [$path, $copyInfo['destination']])) {
+ return false;
+ }
+
+ if ($copyInfo['destinationExists']) {
+ if (!$this->server->emit('beforeUnbind', [$copyInfo['destination']])) {
+ return false;
+ }
+ $this->server->tree->delete($copyInfo['destination']);
+ }
+
+ $this->server->tree->copy($path, $copyInfo['destination']);
+ $this->server->emit('afterCopy', [$path, $copyInfo['destination']]);
+ $this->server->emit('afterBind', [$copyInfo['destination']]);
+
+ // If a resource was overwritten we should send a 204, otherwise a 201
+ $response->setHeader('Content-Length', '0');
+ $response->setStatus($copyInfo['destinationExists'] ? 204 : 201);
+
+ // Sending back false will interrupt the event chain and tell the server
+ // we've handled this method.
+ return false;
+ }
+
+ /**
+ * HTTP REPORT method implementation.
+ *
+ * Although the REPORT method is not part of the standard WebDAV spec (it's from rfc3253)
+ * It's used in a lot of extensions, so it made sense to implement it into the core.
+ *
+ * @return bool
+ */
+ public function httpReport(RequestInterface $request, ResponseInterface $response)
+ {
+ $path = $request->getPath();
+
+ $result = $this->server->xml->parse(
+ $request->getBody(),
+ $request->getUrl(),
+ $rootElementName
+ );
+
+ if ($this->server->emit('report', [$rootElementName, $result, $path])) {
+ // If emit returned true, it means the report was not supported
+ throw new Exception\ReportNotSupported();
+ }
+
+ // Sending back false will interrupt the event chain and tell the server
+ // we've handled this method.
+ return false;
+ }
+
+ /**
+ * This method is called during property updates.
+ *
+ * Here we check if a user attempted to update a protected property and
+ * ensure that the process fails if this is the case.
+ *
+ * @param string $path
+ */
+ public function propPatchProtectedPropertyCheck($path, PropPatch $propPatch)
+ {
+ // Comparing the mutation list to the list of protected properties.
+ $mutations = $propPatch->getMutations();
+
+ $protected = array_intersect(
+ $this->server->protectedProperties,
+ array_keys($mutations)
+ );
+
+ if ($protected) {
+ $propPatch->setResultCode($protected, 403);
+ }
+ }
+
+ /**
+ * This method is called during property updates.
+ *
+ * Here we check if a node implements IProperties and let the node handle
+ * updating of (some) properties.
+ *
+ * @param string $path
+ */
+ public function propPatchNodeUpdate($path, PropPatch $propPatch)
+ {
+ // This should trigger a 404 if the node doesn't exist.
+ $node = $this->server->tree->getNodeForPath($path);
+
+ if ($node instanceof IProperties) {
+ $node->propPatch($propPatch);
+ }
+ }
+
+ /**
+ * This method is called when properties are retrieved.
+ *
+ * Here we add all the default properties.
+ */
+ public function propFind(PropFind $propFind, INode $node)
+ {
+ $propFind->handle('{DAV:}getlastmodified', function () use ($node) {
+ $lm = $node->getLastModified();
+ if ($lm) {
+ return new Xml\Property\GetLastModified($lm);
+ }
+ });
+
+ if ($node instanceof IFile) {
+ $propFind->handle('{DAV:}getcontentlength', [$node, 'getSize']);
+ $propFind->handle('{DAV:}getetag', [$node, 'getETag']);
+ $propFind->handle('{DAV:}getcontenttype', [$node, 'getContentType']);
+ }
+
+ if ($node instanceof IQuota) {
+ $quotaInfo = null;
+ $propFind->handle('{DAV:}quota-used-bytes', function () use (&$quotaInfo, $node) {
+ $quotaInfo = $node->getQuotaInfo();
+
+ return $quotaInfo[0];
+ });
+ $propFind->handle('{DAV:}quota-available-bytes', function () use (&$quotaInfo, $node) {
+ if (!$quotaInfo) {
+ $quotaInfo = $node->getQuotaInfo();
+ }
+
+ return $quotaInfo[1];
+ });
+ }
+
+ $propFind->handle('{DAV:}supported-report-set', function () use ($propFind) {
+ $reports = [];
+ foreach ($this->server->getPlugins() as $plugin) {
+ $reports = array_merge($reports, $plugin->getSupportedReportSet($propFind->getPath()));
+ }
+
+ return new Xml\Property\SupportedReportSet($reports);
+ });
+ $propFind->handle('{DAV:}resourcetype', function () use ($node) {
+ return new Xml\Property\ResourceType($this->server->getResourceTypeForNode($node));
+ });
+ $propFind->handle('{DAV:}supported-method-set', function () use ($propFind) {
+ return new Xml\Property\SupportedMethodSet(
+ $this->server->getAllowedMethods($propFind->getPath())
+ );
+ });
+ }
+
+ /**
+ * Fetches properties for a node.
+ *
+ * This event is called a bit later, so plugins have a chance first to
+ * populate the result.
+ */
+ public function propFindNode(PropFind $propFind, INode $node)
+ {
+ if ($node instanceof IProperties && $propertyNames = $propFind->get404Properties()) {
+ $nodeProperties = $node->getProperties($propertyNames);
+ foreach ($nodeProperties as $propertyName => $propertyValue) {
+ $propFind->set($propertyName, $propertyValue, 200);
+ }
+ }
+ }
+
+ /**
+ * This method is called when properties are retrieved.
+ *
+ * This specific handler is called very late in the process, because we
+ * want other systems to first have a chance to handle the properties.
+ */
+ public function propFindLate(PropFind $propFind, INode $node)
+ {
+ $propFind->handle('{http://calendarserver.org/ns/}getctag', function () use ($propFind) {
+ // If we already have a sync-token from the current propFind
+ // request, we can re-use that.
+ $val = $propFind->get('{http://sabredav.org/ns}sync-token');
+ if ($val) {
+ return $val;
+ }
+
+ $val = $propFind->get('{DAV:}sync-token');
+ if ($val && is_scalar($val)) {
+ return $val;
+ }
+ if ($val && $val instanceof Xml\Property\Href) {
+ return substr($val->getHref(), strlen(Sync\Plugin::SYNCTOKEN_PREFIX));
+ }
+
+ // If we got here, the earlier two properties may simply not have
+ // been part of the earlier request. We're going to fetch them.
+ $result = $this->server->getProperties($propFind->getPath(), [
+ '{http://sabredav.org/ns}sync-token',
+ '{DAV:}sync-token',
+ ]);
+
+ if (isset($result['{http://sabredav.org/ns}sync-token'])) {
+ return $result['{http://sabredav.org/ns}sync-token'];
+ }
+ if (isset($result['{DAV:}sync-token'])) {
+ $val = $result['{DAV:}sync-token'];
+ if (is_scalar($val)) {
+ return $val;
+ } elseif ($val instanceof Xml\Property\Href) {
+ return substr($val->getHref(), strlen(Sync\Plugin::SYNCTOKEN_PREFIX));
+ }
+ }
+ });
+ }
+
+ /**
+ * Listens for exception events, and automatically logs them.
+ *
+ * @param Exception $e
+ */
+ public function exception($e)
+ {
+ $logLevel = \Psr\Log\LogLevel::CRITICAL;
+ if ($e instanceof \Sabre\DAV\Exception) {
+ // If it's a standard sabre/dav exception, it means we have a http
+ // status code available.
+ $code = $e->getHTTPCode();
+
+ if ($code >= 400 && $code < 500) {
+ // user error
+ $logLevel = \Psr\Log\LogLevel::INFO;
+ } else {
+ // Server-side error. We mark it's as an error, but it's not
+ // critical.
+ $logLevel = \Psr\Log\LogLevel::ERROR;
+ }
+ }
+
+ $this->server->getLogger()->log(
+ $logLevel,
+ 'Uncaught exception',
+ [
+ 'exception' => $e,
+ ]
+ );
+ }
+
+ /**
+ * Returns a bunch of meta-data about the plugin.
+ *
+ * Providing this information is optional, and is mainly displayed by the
+ * Browser plugin.
+ *
+ * The description key in the returned array may contain html and will not
+ * be sanitized.
+ *
+ * @return array
+ */
+ public function getPluginInfo()
+ {
+ return [
+ 'name' => $this->getPluginName(),
+ 'description' => 'The Core plugin provides a lot of the basic functionality required by WebDAV, such as a default implementation for all HTTP and WebDAV methods.',
+ 'link' => null,
+ ];
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Exception.php b/lib/composer/vendor/sabre/dav/lib/DAV/Exception.php
new file mode 100644
index 0000000..9fc1d16
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Exception.php
@@ -0,0 +1,50 @@
+lock) {
+ $error = $errorNode->ownerDocument->createElementNS('DAV:', 'd:no-conflicting-lock');
+ $errorNode->appendChild($error);
+ $error->appendChild($errorNode->ownerDocument->createElementNS('DAV:', 'd:href', $this->lock->uri));
+ }
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Exception/Forbidden.php b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/Forbidden.php
new file mode 100644
index 0000000..2f882c3
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/Forbidden.php
@@ -0,0 +1,29 @@
+ownerDocument->createElementNS('DAV:', 'd:valid-resourcetype');
+ $errorNode->appendChild($error);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Exception/InvalidSyncToken.php b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/InvalidSyncToken.php
new file mode 100644
index 0000000..f28d20f
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/InvalidSyncToken.php
@@ -0,0 +1,34 @@
+ownerDocument->createElementNS('DAV:', 'd:valid-sync-token');
+ $errorNode->appendChild($error);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Exception/LengthRequired.php b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/LengthRequired.php
new file mode 100644
index 0000000..9d26fcb
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/LengthRequired.php
@@ -0,0 +1,30 @@
+ownerDocument->createElementNS('DAV:', 'd:lock-token-matches-request-uri');
+ $errorNode->appendChild($error);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Exception/Locked.php b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/Locked.php
new file mode 100644
index 0000000..24fad70
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/Locked.php
@@ -0,0 +1,68 @@
+lock = $lock;
+ }
+
+ /**
+ * Returns the HTTP statuscode for this exception.
+ *
+ * @return int
+ */
+ public function getHTTPCode()
+ {
+ return 423;
+ }
+
+ /**
+ * This method allows the exception to include additional information into the WebDAV error response.
+ */
+ public function serialize(DAV\Server $server, \DOMElement $errorNode)
+ {
+ if ($this->lock) {
+ $error = $errorNode->ownerDocument->createElementNS('DAV:', 'd:lock-token-submitted');
+ $errorNode->appendChild($error);
+
+ $href = $errorNode->ownerDocument->createElementNS('DAV:', 'd:href');
+ $href->appendChild($errorNode->ownerDocument->createTextNode($this->lock->uri));
+ $error->appendChild(
+ $href
+ );
+ }
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php
new file mode 100644
index 0000000..dbf42ed
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php
@@ -0,0 +1,46 @@
+getAllowedMethods($server->getRequestUri());
+
+ return [
+ 'Allow' => strtoupper(implode(', ', $methods)),
+ ];
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Exception/NotAuthenticated.php b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/NotAuthenticated.php
new file mode 100644
index 0000000..0a5ba9b
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/NotAuthenticated.php
@@ -0,0 +1,30 @@
+header = $header;
+ }
+
+ /**
+ * Returns the HTTP statuscode for this exception.
+ *
+ * @return int
+ */
+ public function getHTTPCode()
+ {
+ return 412;
+ }
+
+ /**
+ * This method allows the exception to include additional information into the WebDAV error response.
+ */
+ public function serialize(DAV\Server $server, \DOMElement $errorNode)
+ {
+ if ($this->header) {
+ $prop = $errorNode->ownerDocument->createElement('s:header');
+ $prop->nodeValue = $this->header;
+ $errorNode->appendChild($prop);
+ }
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Exception/ReportNotSupported.php b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/ReportNotSupported.php
new file mode 100644
index 0000000..a483838
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/ReportNotSupported.php
@@ -0,0 +1,28 @@
+ownerDocument->createElementNS('DAV:', 'd:supported-report');
+ $errorNode->appendChild($error);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Exception/RequestedRangeNotSatisfiable.php b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/RequestedRangeNotSatisfiable.php
new file mode 100644
index 0000000..6ccb5b8
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/RequestedRangeNotSatisfiable.php
@@ -0,0 +1,30 @@
+
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ServiceUnavailable extends DAV\Exception
+{
+ /**
+ * Returns the HTTP statuscode for this exception.
+ *
+ * @return int
+ */
+ public function getHTTPCode()
+ {
+ return 503;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Exception/TooManyMatches.php b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/TooManyMatches.php
new file mode 100644
index 0000000..ef6f502
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/TooManyMatches.php
@@ -0,0 +1,34 @@
+ownerDocument->createElementNS('DAV:', 'd:number-of-matches-within-limits');
+ $errorNode->appendChild($error);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Exception/UnsupportedMediaType.php b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/UnsupportedMediaType.php
new file mode 100644
index 0000000..bc9da30
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/UnsupportedMediaType.php
@@ -0,0 +1,30 @@
+path.'/'.$name;
+ file_put_contents($newPath, $data);
+ clearstatcache(true, $newPath);
+ }
+
+ /**
+ * Creates a new subdirectory.
+ *
+ * @param string $name
+ */
+ public function createDirectory($name)
+ {
+ $newPath = $this->path.'/'.$name;
+ mkdir($newPath);
+ clearstatcache(true, $newPath);
+ }
+
+ /**
+ * Returns a specific child node, referenced by its name.
+ *
+ * This method must throw DAV\Exception\NotFound if the node does not
+ * exist.
+ *
+ * @param string $name
+ *
+ * @throws DAV\Exception\NotFound
+ *
+ * @return DAV\INode
+ */
+ public function getChild($name)
+ {
+ $path = $this->path.'/'.$name;
+
+ if (!file_exists($path)) {
+ throw new DAV\Exception\NotFound('File with name '.$path.' could not be located');
+ }
+ if (is_dir($path)) {
+ return new self($path);
+ } else {
+ return new File($path);
+ }
+ }
+
+ /**
+ * Returns an array with all the child nodes.
+ *
+ * @return DAV\INode[]
+ */
+ public function getChildren()
+ {
+ $nodes = [];
+ $iterator = new \FilesystemIterator(
+ $this->path,
+ \FilesystemIterator::CURRENT_AS_SELF
+ | \FilesystemIterator::SKIP_DOTS
+ );
+ foreach ($iterator as $entry) {
+ $nodes[] = $this->getChild($entry->getFilename());
+ }
+
+ return $nodes;
+ }
+
+ /**
+ * Checks if a child exists.
+ *
+ * @param string $name
+ *
+ * @return bool
+ */
+ public function childExists($name)
+ {
+ $path = $this->path.'/'.$name;
+
+ return file_exists($path);
+ }
+
+ /**
+ * Deletes all files in this directory, and then itself.
+ */
+ public function delete()
+ {
+ foreach ($this->getChildren() as $child) {
+ $child->delete();
+ }
+ rmdir($this->path);
+ }
+
+ /**
+ * Returns available diskspace information.
+ *
+ * @return array
+ */
+ public function getQuotaInfo()
+ {
+ $absolute = realpath($this->path);
+
+ return [
+ disk_total_space($absolute) - disk_free_space($absolute),
+ disk_free_space($absolute),
+ ];
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/FS/File.php b/lib/composer/vendor/sabre/dav/lib/DAV/FS/File.php
new file mode 100644
index 0000000..b78a801
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/FS/File.php
@@ -0,0 +1,87 @@
+path, $data);
+ clearstatcache(true, $this->path);
+ }
+
+ /**
+ * Returns the data.
+ *
+ * @return resource
+ */
+ public function get()
+ {
+ return fopen($this->path, 'r');
+ }
+
+ /**
+ * Delete the current file.
+ */
+ public function delete()
+ {
+ unlink($this->path);
+ }
+
+ /**
+ * Returns the size of the node, in bytes.
+ *
+ * @return int
+ */
+ public function getSize()
+ {
+ return filesize($this->path);
+ }
+
+ /**
+ * Returns the ETag for a file.
+ *
+ * An ETag is a unique identifier representing the current version of the file. If the file changes, the ETag MUST change.
+ * The ETag is an arbitrary string, but MUST be surrounded by double-quotes.
+ *
+ * Return null if the ETag can not effectively be determined
+ *
+ * @return mixed
+ */
+ public function getETag()
+ {
+ return '"'.sha1(
+ fileinode($this->path).
+ filesize($this->path).
+ filemtime($this->path)
+ ).'"';
+ }
+
+ /**
+ * Returns the mime-type for a file.
+ *
+ * If null is returned, we'll assume application/octet-stream
+ *
+ * @return mixed
+ */
+ public function getContentType()
+ {
+ return null;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/FS/Node.php b/lib/composer/vendor/sabre/dav/lib/DAV/FS/Node.php
new file mode 100644
index 0000000..32aa747
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/FS/Node.php
@@ -0,0 +1,96 @@
+path = $path;
+ $this->overrideName = $overrideName;
+ }
+
+ /**
+ * Returns the name of the node.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ if ($this->overrideName) {
+ return $this->overrideName;
+ }
+
+ list(, $name) = Uri\split($this->path);
+
+ return $name;
+ }
+
+ /**
+ * Renames the node.
+ *
+ * @param string $name The new name
+ */
+ public function setName($name)
+ {
+ if ($this->overrideName) {
+ throw new Forbidden('This node cannot be renamed');
+ }
+
+ list($parentPath) = Uri\split($this->path);
+ list(, $newName) = Uri\split($name);
+
+ $newPath = $parentPath.'/'.$newName;
+ rename($this->path, $newPath);
+
+ $this->path = $newPath;
+ }
+
+ /**
+ * Returns the last modification time, as a unix timestamp.
+ *
+ * @return int
+ */
+ public function getLastModified()
+ {
+ return filemtime($this->path);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/FSExt/Directory.php b/lib/composer/vendor/sabre/dav/lib/DAV/FSExt/Directory.php
new file mode 100644
index 0000000..d6aea00
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/FSExt/Directory.php
@@ -0,0 +1,212 @@
+path.'/'.$name;
+ file_put_contents($newPath, $data);
+ clearstatcache(true, $newPath);
+
+ return '"'.sha1(
+ fileinode($newPath).
+ filesize($newPath).
+ filemtime($newPath)
+ ).'"';
+ }
+
+ /**
+ * Creates a new subdirectory.
+ *
+ * @param string $name
+ */
+ public function createDirectory($name)
+ {
+ // We're not allowing dots
+ if ('.' == $name || '..' == $name) {
+ throw new DAV\Exception\Forbidden('Permission denied to . and ..');
+ }
+ $newPath = $this->path.'/'.$name;
+ mkdir($newPath);
+ clearstatcache(true, $newPath);
+ }
+
+ /**
+ * Returns a specific child node, referenced by its name.
+ *
+ * This method must throw Sabre\DAV\Exception\NotFound if the node does not
+ * exist.
+ *
+ * @param string $name
+ *
+ * @throws DAV\Exception\NotFound
+ *
+ * @return DAV\INode
+ */
+ public function getChild($name)
+ {
+ $path = $this->path.'/'.$name;
+
+ if (!file_exists($path)) {
+ throw new DAV\Exception\NotFound('File could not be located');
+ }
+ if ('.' == $name || '..' == $name) {
+ throw new DAV\Exception\Forbidden('Permission denied to . and ..');
+ }
+ if (is_dir($path)) {
+ return new self($path);
+ } else {
+ return new File($path);
+ }
+ }
+
+ /**
+ * Checks if a child exists.
+ *
+ * @param string $name
+ *
+ * @return bool
+ */
+ public function childExists($name)
+ {
+ if ('.' == $name || '..' == $name) {
+ throw new DAV\Exception\Forbidden('Permission denied to . and ..');
+ }
+ $path = $this->path.'/'.$name;
+
+ return file_exists($path);
+ }
+
+ /**
+ * Returns an array with all the child nodes.
+ *
+ * @return DAV\INode[]
+ */
+ public function getChildren()
+ {
+ $nodes = [];
+ $iterator = new \FilesystemIterator(
+ $this->path,
+ \FilesystemIterator::CURRENT_AS_SELF
+ | \FilesystemIterator::SKIP_DOTS
+ );
+
+ foreach ($iterator as $entry) {
+ $nodes[] = $this->getChild($entry->getFilename());
+ }
+
+ return $nodes;
+ }
+
+ /**
+ * Deletes all files in this directory, and then itself.
+ *
+ * @return bool
+ */
+ public function delete()
+ {
+ // Deleting all children
+ foreach ($this->getChildren() as $child) {
+ $child->delete();
+ }
+
+ // Removing the directory itself
+ rmdir($this->path);
+
+ return true;
+ }
+
+ /**
+ * Returns available diskspace information.
+ *
+ * @return array
+ */
+ public function getQuotaInfo()
+ {
+ $total = disk_total_space(realpath($this->path));
+ $free = disk_free_space(realpath($this->path));
+
+ return [
+ $total - $free,
+ $free,
+ ];
+ }
+
+ /**
+ * Moves a node into this collection.
+ *
+ * It is up to the implementors to:
+ * 1. Create the new resource.
+ * 2. Remove the old resource.
+ * 3. Transfer any properties or other data.
+ *
+ * Generally you should make very sure that your collection can easily move
+ * the move.
+ *
+ * If you don't, just return false, which will trigger sabre/dav to handle
+ * the move itself. If you return true from this function, the assumption
+ * is that the move was successful.
+ *
+ * @param string $targetName new local file/collection name
+ * @param string $sourcePath Full path to source node
+ * @param DAV\INode $sourceNode Source node itself
+ *
+ * @return bool
+ */
+ public function moveInto($targetName, $sourcePath, DAV\INode $sourceNode)
+ {
+ // We only support FSExt\Directory or FSExt\File objects, so
+ // anything else we want to quickly reject.
+ if (!$sourceNode instanceof self && !$sourceNode instanceof File) {
+ return false;
+ }
+
+ // PHP allows us to access protected properties from other objects, as
+ // long as they are defined in a class that has a shared inheritance
+ // with the current class.
+ return rename($sourceNode->path, $this->path.'/'.$targetName);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/FSExt/File.php b/lib/composer/vendor/sabre/dav/lib/DAV/FSExt/File.php
new file mode 100644
index 0000000..74849b5
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/FSExt/File.php
@@ -0,0 +1,153 @@
+path, $data);
+ clearstatcache(true, $this->path);
+
+ return $this->getETag();
+ }
+
+ /**
+ * Updates the file based on a range specification.
+ *
+ * The first argument is the data, which is either a readable stream
+ * resource or a string.
+ *
+ * The second argument is the type of update we're doing.
+ * This is either:
+ * * 1. append (default)
+ * * 2. update based on a start byte
+ * * 3. update based on an end byte
+ *;
+ * The third argument is the start or end byte.
+ *
+ * After a successful put operation, you may choose to return an ETag. The
+ * ETAG must always be surrounded by double-quotes. These quotes must
+ * appear in the actual string you're returning.
+ *
+ * Clients may use the ETag from a PUT request to later on make sure that
+ * when they update the file, the contents haven't changed in the mean
+ * time.
+ *
+ * @param resource|string $data
+ * @param int $rangeType
+ * @param int $offset
+ *
+ * @return string|null
+ */
+ public function patch($data, $rangeType, $offset = null)
+ {
+ switch ($rangeType) {
+ case 1:
+ $f = fopen($this->path, 'a');
+ break;
+ case 2:
+ $f = fopen($this->path, 'c');
+ fseek($f, $offset);
+ break;
+ case 3:
+ $f = fopen($this->path, 'c');
+ fseek($f, $offset, SEEK_END);
+ break;
+ default:
+ $f = fopen($this->path, 'a');
+ break;
+ }
+ if (is_string($data)) {
+ fwrite($f, $data);
+ } else {
+ stream_copy_to_stream($data, $f);
+ }
+ fclose($f);
+ clearstatcache(true, $this->path);
+
+ return $this->getETag();
+ }
+
+ /**
+ * Returns the data.
+ *
+ * @return resource
+ */
+ public function get()
+ {
+ return fopen($this->path, 'r');
+ }
+
+ /**
+ * Delete the current file.
+ *
+ * @return bool
+ */
+ public function delete()
+ {
+ return unlink($this->path);
+ }
+
+ /**
+ * Returns the ETag for a file.
+ *
+ * An ETag is a unique identifier representing the current version of the file. If the file changes, the ETag MUST change.
+ * The ETag is an arbitrary string, but MUST be surrounded by double-quotes.
+ *
+ * Return null if the ETag can not effectively be determined
+ *
+ * @return string|null
+ */
+ public function getETag()
+ {
+ return '"'.sha1(
+ fileinode($this->path).
+ filesize($this->path).
+ filemtime($this->path)
+ ).'"';
+ }
+
+ /**
+ * Returns the mime-type for a file.
+ *
+ * If null is returned, we'll assume application/octet-stream
+ *
+ * @return string|null
+ */
+ public function getContentType()
+ {
+ return null;
+ }
+
+ /**
+ * Returns the size of the file, in bytes.
+ *
+ * @return int
+ */
+ public function getSize()
+ {
+ return filesize($this->path);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/File.php b/lib/composer/vendor/sabre/dav/lib/DAV/File.php
new file mode 100644
index 0000000..daf83aa
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/File.php
@@ -0,0 +1,93 @@
+locksFile = $locksFile;
+ }
+
+ /**
+ * Returns a list of Sabre\DAV\Locks\LockInfo objects.
+ *
+ * This method should return all the locks for a particular uri, including
+ * locks that might be set on a parent uri.
+ *
+ * If returnChildLocks is set to true, this method should also look for
+ * any locks in the subtree of the uri for locks.
+ *
+ * @param string $uri
+ * @param bool $returnChildLocks
+ *
+ * @return array
+ */
+ public function getLocks($uri, $returnChildLocks)
+ {
+ $newLocks = [];
+
+ $locks = $this->getData();
+
+ foreach ($locks as $lock) {
+ if ($lock->uri === $uri ||
+ //deep locks on parents
+ (0 != $lock->depth && 0 === strpos($uri, $lock->uri.'/')) ||
+
+ // locks on children
+ ($returnChildLocks && (0 === strpos($lock->uri, $uri.'/')))) {
+ $newLocks[] = $lock;
+ }
+ }
+
+ // Checking if we can remove any of these locks
+ foreach ($newLocks as $k => $lock) {
+ if (time() > $lock->timeout + $lock->created) {
+ unset($newLocks[$k]);
+ }
+ }
+
+ return $newLocks;
+ }
+
+ /**
+ * Locks a uri.
+ *
+ * @param string $uri
+ *
+ * @return bool
+ */
+ public function lock($uri, LockInfo $lockInfo)
+ {
+ // We're making the lock timeout 30 minutes
+ $lockInfo->timeout = 1800;
+ $lockInfo->created = time();
+ $lockInfo->uri = $uri;
+
+ $locks = $this->getData();
+
+ foreach ($locks as $k => $lock) {
+ if (
+ ($lock->token == $lockInfo->token) ||
+ (time() > $lock->timeout + $lock->created)
+ ) {
+ unset($locks[$k]);
+ }
+ }
+ $locks[] = $lockInfo;
+ $this->putData($locks);
+
+ return true;
+ }
+
+ /**
+ * Removes a lock from a uri.
+ *
+ * @param string $uri
+ *
+ * @return bool
+ */
+ public function unlock($uri, LockInfo $lockInfo)
+ {
+ $locks = $this->getData();
+ foreach ($locks as $k => $lock) {
+ if ($lock->token == $lockInfo->token) {
+ unset($locks[$k]);
+ $this->putData($locks);
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Loads the lockdata from the filesystem.
+ *
+ * @return array
+ */
+ protected function getData()
+ {
+ if (!file_exists($this->locksFile)) {
+ return [];
+ }
+
+ // opening up the file, and creating a shared lock
+ $handle = fopen($this->locksFile, 'r');
+ flock($handle, LOCK_SH);
+
+ // Reading data until the eof
+ $data = stream_get_contents($handle);
+
+ // We're all good
+ flock($handle, LOCK_UN);
+ fclose($handle);
+
+ // Unserializing and checking if the resource file contains data for this file
+ $data = unserialize($data);
+ if (!$data) {
+ return [];
+ }
+
+ return $data;
+ }
+
+ /**
+ * Saves the lockdata.
+ */
+ protected function putData(array $newData)
+ {
+ // opening up the file, and creating an exclusive lock
+ $handle = fopen($this->locksFile, 'a+');
+ flock($handle, LOCK_EX);
+
+ // We can only truncate and rewind once the lock is acquired.
+ ftruncate($handle, 0);
+ rewind($handle);
+
+ fwrite($handle, serialize($newData));
+ flock($handle, LOCK_UN);
+ fclose($handle);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Locks/Backend/PDO.php b/lib/composer/vendor/sabre/dav/lib/DAV/Locks/Backend/PDO.php
new file mode 100644
index 0000000..3f425f9
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Locks/Backend/PDO.php
@@ -0,0 +1,172 @@
+pdo = $pdo;
+ }
+
+ /**
+ * Returns a list of Sabre\DAV\Locks\LockInfo objects.
+ *
+ * This method should return all the locks for a particular uri, including
+ * locks that might be set on a parent uri.
+ *
+ * If returnChildLocks is set to true, this method should also look for
+ * any locks in the subtree of the uri for locks.
+ *
+ * @param string $uri
+ * @param bool $returnChildLocks
+ *
+ * @return array
+ */
+ public function getLocks($uri, $returnChildLocks)
+ {
+ // NOTE: the following 10 lines or so could be easily replaced by
+ // pure sql. MySQL's non-standard string concatenation prevents us
+ // from doing this though.
+ $query = 'SELECT owner, token, timeout, created, scope, depth, uri FROM '.$this->tableName.' WHERE (created > (? - timeout)) AND ((uri = ?)';
+ $params = [time(), $uri];
+
+ // We need to check locks for every part in the uri.
+ $uriParts = explode('/', $uri);
+
+ // We already covered the last part of the uri
+ array_pop($uriParts);
+
+ $currentPath = '';
+
+ foreach ($uriParts as $part) {
+ if ($currentPath) {
+ $currentPath .= '/';
+ }
+ $currentPath .= $part;
+
+ $query .= ' OR (depth!=0 AND uri = ?)';
+ $params[] = $currentPath;
+ }
+
+ if ($returnChildLocks) {
+ $query .= ' OR (uri LIKE ?)';
+ $params[] = $uri.'/%';
+ }
+ $query .= ')';
+
+ $stmt = $this->pdo->prepare($query);
+ $stmt->execute($params);
+ $result = $stmt->fetchAll();
+
+ $lockList = [];
+ foreach ($result as $row) {
+ $lockInfo = new LockInfo();
+ $lockInfo->owner = $row['owner'];
+ $lockInfo->token = $row['token'];
+ $lockInfo->timeout = $row['timeout'];
+ $lockInfo->created = $row['created'];
+ $lockInfo->scope = $row['scope'];
+ $lockInfo->depth = $row['depth'];
+ $lockInfo->uri = $row['uri'];
+ $lockList[] = $lockInfo;
+ }
+
+ return $lockList;
+ }
+
+ /**
+ * Locks a uri.
+ *
+ * @param string $uri
+ *
+ * @return bool
+ */
+ public function lock($uri, LockInfo $lockInfo)
+ {
+ // We're making the lock timeout 30 minutes
+ $lockInfo->timeout = 30 * 60;
+ $lockInfo->created = time();
+ $lockInfo->uri = $uri;
+
+ $locks = $this->getLocks($uri, false);
+ $exists = false;
+ foreach ($locks as $lock) {
+ if ($lock->token == $lockInfo->token) {
+ $exists = true;
+ }
+ }
+
+ if ($exists) {
+ $stmt = $this->pdo->prepare('UPDATE '.$this->tableName.' SET owner = ?, timeout = ?, scope = ?, depth = ?, uri = ?, created = ? WHERE token = ?');
+ $stmt->execute([
+ $lockInfo->owner,
+ $lockInfo->timeout,
+ $lockInfo->scope,
+ $lockInfo->depth,
+ $uri,
+ $lockInfo->created,
+ $lockInfo->token,
+ ]);
+ } else {
+ $stmt = $this->pdo->prepare('INSERT INTO '.$this->tableName.' (owner,timeout,scope,depth,uri,created,token) VALUES (?,?,?,?,?,?,?)');
+ $stmt->execute([
+ $lockInfo->owner,
+ $lockInfo->timeout,
+ $lockInfo->scope,
+ $lockInfo->depth,
+ $uri,
+ $lockInfo->created,
+ $lockInfo->token,
+ ]);
+ }
+
+ return true;
+ }
+
+ /**
+ * Removes a lock from a uri.
+ *
+ * @param string $uri
+ *
+ * @return bool
+ */
+ public function unlock($uri, LockInfo $lockInfo)
+ {
+ $stmt = $this->pdo->prepare('DELETE FROM '.$this->tableName.' WHERE uri = ? AND token = ?');
+ $stmt->execute([$uri, $lockInfo->token]);
+
+ return 1 === $stmt->rowCount();
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Locks/LockInfo.php b/lib/composer/vendor/sabre/dav/lib/DAV/Locks/LockInfo.php
new file mode 100644
index 0000000..df82275
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Locks/LockInfo.php
@@ -0,0 +1,82 @@
+addPlugin($lockPlugin);
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Plugin extends DAV\ServerPlugin
+{
+ /**
+ * locksBackend.
+ *
+ * @var Backend\BackendInterface
+ */
+ protected $locksBackend;
+
+ /**
+ * server.
+ *
+ * @var DAV\Server
+ */
+ protected $server;
+
+ /**
+ * __construct.
+ */
+ public function __construct(Backend\BackendInterface $locksBackend)
+ {
+ $this->locksBackend = $locksBackend;
+ }
+
+ /**
+ * Initializes the plugin.
+ *
+ * This method is automatically called by the Server class after addPlugin.
+ */
+ public function initialize(DAV\Server $server)
+ {
+ $this->server = $server;
+
+ $this->server->xml->elementMap['{DAV:}lockinfo'] = 'Sabre\\DAV\\Xml\\Request\\Lock';
+
+ $server->on('method:LOCK', [$this, 'httpLock']);
+ $server->on('method:UNLOCK', [$this, 'httpUnlock']);
+ $server->on('validateTokens', [$this, 'validateTokens']);
+ $server->on('propFind', [$this, 'propFind']);
+ $server->on('afterUnbind', [$this, 'afterUnbind']);
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using Sabre\DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ public function getPluginName()
+ {
+ return 'locks';
+ }
+
+ /**
+ * This method is called after most properties have been found
+ * it allows us to add in any Lock-related properties.
+ */
+ public function propFind(DAV\PropFind $propFind, DAV\INode $node)
+ {
+ $propFind->handle('{DAV:}supportedlock', function () {
+ return new DAV\Xml\Property\SupportedLock();
+ });
+ $propFind->handle('{DAV:}lockdiscovery', function () use ($propFind) {
+ return new DAV\Xml\Property\LockDiscovery(
+ $this->getLocks($propFind->getPath())
+ );
+ });
+ }
+
+ /**
+ * Use this method to tell the server this plugin defines additional
+ * HTTP methods.
+ *
+ * This method is passed a uri. It should only return HTTP methods that are
+ * available for the specified uri.
+ *
+ * @param string $uri
+ *
+ * @return array
+ */
+ public function getHTTPMethods($uri)
+ {
+ return ['LOCK', 'UNLOCK'];
+ }
+
+ /**
+ * Returns a list of features for the HTTP OPTIONS Dav: header.
+ *
+ * In this case this is only the number 2. The 2 in the Dav: header
+ * indicates the server supports locks.
+ *
+ * @return array
+ */
+ public function getFeatures()
+ {
+ return [2];
+ }
+
+ /**
+ * Returns all lock information on a particular uri.
+ *
+ * This function should return an array with Sabre\DAV\Locks\LockInfo objects. If there are no locks on a file, return an empty array.
+ *
+ * Additionally there is also the possibility of locks on parent nodes, so we'll need to traverse every part of the tree
+ * If the $returnChildLocks argument is set to true, we'll also traverse all the children of the object
+ * for any possible locks and return those as well.
+ *
+ * @param string $uri
+ * @param bool $returnChildLocks
+ *
+ * @return array
+ */
+ public function getLocks($uri, $returnChildLocks = false)
+ {
+ return $this->locksBackend->getLocks($uri, $returnChildLocks);
+ }
+
+ /**
+ * Locks an uri.
+ *
+ * The WebDAV lock request can be operated to either create a new lock on a file, or to refresh an existing lock
+ * If a new lock is created, a full XML body should be supplied, containing information about the lock such as the type
+ * of lock (shared or exclusive) and the owner of the lock
+ *
+ * If a lock is to be refreshed, no body should be supplied and there should be a valid If header containing the lock
+ *
+ * Additionally, a lock can be requested for a non-existent file. In these case we're obligated to create an empty file as per RFC4918:S7.3
+ *
+ * @return bool
+ */
+ public function httpLock(RequestInterface $request, ResponseInterface $response)
+ {
+ $uri = $request->getPath();
+
+ $existingLocks = $this->getLocks($uri);
+
+ if ($body = $request->getBodyAsString()) {
+ // This is a new lock request
+
+ $existingLock = null;
+ // Checking if there's already non-shared locks on the uri.
+ foreach ($existingLocks as $existingLock) {
+ if (LockInfo::EXCLUSIVE === $existingLock->scope) {
+ throw new DAV\Exception\ConflictingLock($existingLock);
+ }
+ }
+
+ $lockInfo = $this->parseLockRequest($body);
+ $lockInfo->depth = $this->server->getHTTPDepth();
+ $lockInfo->uri = $uri;
+ if ($existingLock && LockInfo::SHARED != $lockInfo->scope) {
+ throw new DAV\Exception\ConflictingLock($existingLock);
+ }
+ } else {
+ // Gonna check if this was a lock refresh.
+ $existingLocks = $this->getLocks($uri);
+ $conditions = $this->server->getIfConditions($request);
+ $found = null;
+
+ foreach ($existingLocks as $existingLock) {
+ foreach ($conditions as $condition) {
+ foreach ($condition['tokens'] as $token) {
+ if ($token['token'] === 'opaquelocktoken:'.$existingLock->token) {
+ $found = $existingLock;
+ break 3;
+ }
+ }
+ }
+ }
+
+ // If none were found, this request is in error.
+ if (is_null($found)) {
+ if ($existingLocks) {
+ throw new DAV\Exception\Locked(reset($existingLocks));
+ } else {
+ throw new DAV\Exception\BadRequest('An xml body is required for lock requests');
+ }
+ }
+
+ // This must have been a lock refresh
+ $lockInfo = $found;
+
+ // The resource could have been locked through another uri.
+ if ($uri != $lockInfo->uri) {
+ $uri = $lockInfo->uri;
+ }
+ }
+
+ if ($timeout = $this->getTimeoutHeader()) {
+ $lockInfo->timeout = $timeout;
+ }
+
+ $newFile = false;
+
+ // If we got this far.. we should go check if this node actually exists. If this is not the case, we need to create it first
+ try {
+ $this->server->tree->getNodeForPath($uri);
+
+ // We need to call the beforeWriteContent event for RFC3744
+ // Edit: looks like this is not used, and causing problems now.
+ //
+ // See Issue 222
+ // $this->server->emit('beforeWriteContent',array($uri));
+ } catch (DAV\Exception\NotFound $e) {
+ // It didn't, lets create it
+ $this->server->createFile($uri, fopen('php://memory', 'r'));
+ $newFile = true;
+ }
+
+ $this->lockNode($uri, $lockInfo);
+
+ $response->setHeader('Content-Type', 'application/xml; charset=utf-8');
+ $response->setHeader('Lock-Token', 'token.'>');
+ $response->setStatus($newFile ? 201 : 200);
+ $response->setBody($this->generateLockResponse($lockInfo));
+
+ // Returning false will interrupt the event chain and mark this method
+ // as 'handled'.
+ return false;
+ }
+
+ /**
+ * Unlocks a uri.
+ *
+ * This WebDAV method allows you to remove a lock from a node. The client should provide a valid locktoken through the Lock-token http header
+ * The server should return 204 (No content) on success
+ */
+ public function httpUnlock(RequestInterface $request, ResponseInterface $response)
+ {
+ $lockToken = $request->getHeader('Lock-Token');
+
+ // If the locktoken header is not supplied, we need to throw a bad request exception
+ if (!$lockToken) {
+ throw new DAV\Exception\BadRequest('No lock token was supplied');
+ }
+ $path = $request->getPath();
+ $locks = $this->getLocks($path);
+
+ // Windows sometimes forgets to include < and > in the Lock-Token
+ // header
+ if ('<' !== $lockToken[0]) {
+ $lockToken = '<'.$lockToken.'>';
+ }
+
+ foreach ($locks as $lock) {
+ if ('token.'>' == $lockToken) {
+ $this->unlockNode($path, $lock);
+ $response->setHeader('Content-Length', '0');
+ $response->setStatus(204);
+
+ // Returning false will break the method chain, and mark the
+ // method as 'handled'.
+ return false;
+ }
+ }
+
+ // If we got here, it means the locktoken was invalid
+ throw new DAV\Exception\LockTokenMatchesRequestUri();
+ }
+
+ /**
+ * This method is called after a node is deleted.
+ *
+ * We use this event to clean up any locks that still exist on the node.
+ *
+ * @param string $path
+ */
+ public function afterUnbind($path)
+ {
+ $locks = $this->getLocks($path, $includeChildren = true);
+ foreach ($locks as $lock) {
+ // don't delete a lock on a parent dir
+ if (0 !== strpos($lock->uri, $path)) {
+ continue;
+ }
+ $this->unlockNode($path, $lock);
+ }
+ }
+
+ /**
+ * Locks a uri.
+ *
+ * All the locking information is supplied in the lockInfo object. The object has a suggested timeout, but this can be safely ignored
+ * It is important that if the existing timeout is ignored, the property is overwritten, as this needs to be sent back to the client
+ *
+ * @param string $uri
+ *
+ * @return bool
+ */
+ public function lockNode($uri, LockInfo $lockInfo)
+ {
+ if (!$this->server->emit('beforeLock', [$uri, $lockInfo])) {
+ return;
+ }
+
+ return $this->locksBackend->lock($uri, $lockInfo);
+ }
+
+ /**
+ * Unlocks a uri.
+ *
+ * This method removes a lock from a uri. It is assumed all the supplied information is correct and verified
+ *
+ * @param string $uri
+ *
+ * @return bool
+ */
+ public function unlockNode($uri, LockInfo $lockInfo)
+ {
+ if (!$this->server->emit('beforeUnlock', [$uri, $lockInfo])) {
+ return;
+ }
+
+ return $this->locksBackend->unlock($uri, $lockInfo);
+ }
+
+ /**
+ * Returns the contents of the HTTP Timeout header.
+ *
+ * The method formats the header into an integer.
+ *
+ * @return int
+ */
+ public function getTimeoutHeader()
+ {
+ $header = $this->server->httpRequest->getHeader('Timeout');
+
+ if ($header) {
+ if (0 === stripos($header, 'second-')) {
+ $header = (int) (substr($header, 7));
+ } elseif (0 === stripos($header, 'infinite')) {
+ $header = LockInfo::TIMEOUT_INFINITE;
+ } else {
+ throw new DAV\Exception\BadRequest('Invalid HTTP timeout header');
+ }
+ } else {
+ $header = 0;
+ }
+
+ return $header;
+ }
+
+ /**
+ * Generates the response for successful LOCK requests.
+ *
+ * @return string
+ */
+ protected function generateLockResponse(LockInfo $lockInfo)
+ {
+ return $this->server->xml->write('{DAV:}prop', [
+ '{DAV:}lockdiscovery' => new DAV\Xml\Property\LockDiscovery([$lockInfo]),
+ ], $this->server->getBaseUri());
+ }
+
+ /**
+ * The validateTokens event is triggered before every request.
+ *
+ * It's a moment where this plugin can check all the supplied lock tokens
+ * in the If: header, and check if they are valid.
+ *
+ * In addition, it will also ensure that it checks any missing lokens that
+ * must be present in the request, and reject requests without the proper
+ * tokens.
+ *
+ * @param mixed $conditions
+ */
+ public function validateTokens(RequestInterface $request, &$conditions)
+ {
+ // First we need to gather a list of locks that must be satisfied.
+ $mustLocks = [];
+ $method = $request->getMethod();
+
+ // Methods not in that list are operations that doesn't alter any
+ // resources, and we don't need to check the lock-states for.
+ switch ($method) {
+ case 'DELETE':
+ $mustLocks = array_merge($mustLocks, $this->getLocks(
+ $request->getPath(),
+ true
+ ));
+ break;
+ case 'MKCOL':
+ case 'MKCALENDAR':
+ case 'PROPPATCH':
+ case 'PUT':
+ case 'PATCH':
+ $mustLocks = array_merge($mustLocks, $this->getLocks(
+ $request->getPath(),
+ false
+ ));
+ break;
+ case 'MOVE':
+ $mustLocks = array_merge($mustLocks, $this->getLocks(
+ $request->getPath(),
+ true
+ ));
+ $mustLocks = array_merge($mustLocks, $this->getLocks(
+ $this->server->calculateUri($request->getHeader('Destination')),
+ false
+ ));
+ break;
+ case 'COPY':
+ $mustLocks = array_merge($mustLocks, $this->getLocks(
+ $this->server->calculateUri($request->getHeader('Destination')),
+ false
+ ));
+ break;
+ case 'LOCK':
+ //Temporary measure.. figure out later why this is needed
+ // Here we basically ignore all incoming tokens...
+ foreach ($conditions as $ii => $condition) {
+ foreach ($condition['tokens'] as $jj => $token) {
+ $conditions[$ii]['tokens'][$jj]['validToken'] = true;
+ }
+ }
+
+ return;
+ }
+
+ // It's possible that there's identical locks, because of shared
+ // parents. We're removing the duplicates here.
+ $tmp = [];
+ foreach ($mustLocks as $lock) {
+ $tmp[$lock->token] = $lock;
+ }
+ $mustLocks = array_values($tmp);
+
+ foreach ($conditions as $kk => $condition) {
+ foreach ($condition['tokens'] as $ii => $token) {
+ // Lock tokens always start with opaquelocktoken:
+ if ('opaquelocktoken:' !== substr($token['token'], 0, 16)) {
+ continue;
+ }
+
+ $checkToken = substr($token['token'], 16);
+ // Looping through our list with locks.
+ foreach ($mustLocks as $jj => $mustLock) {
+ if ($mustLock->token == $checkToken) {
+ // We have a match!
+ // Removing this one from mustlocks
+ unset($mustLocks[$jj]);
+
+ // Marking the condition as valid.
+ $conditions[$kk]['tokens'][$ii]['validToken'] = true;
+
+ // Advancing to the next token
+ continue 2;
+ }
+ }
+
+ // If we got here, it means that there was a
+ // lock-token, but it was not in 'mustLocks'.
+ //
+ // This is an edge-case, as it could mean that token
+ // was specified with a url that was not 'required' to
+ // check. So we're doing one extra lookup to make sure
+ // we really don't know this token.
+ //
+ // This also gets triggered when the user specified a
+ // lock-token that was expired.
+ $oddLocks = $this->getLocks($condition['uri']);
+ foreach ($oddLocks as $oddLock) {
+ if ($oddLock->token === $checkToken) {
+ // We have a hit!
+ $conditions[$kk]['tokens'][$ii]['validToken'] = true;
+ continue 2;
+ }
+ }
+
+ // If we get all the way here, the lock-token was
+ // really unknown.
+ }
+ }
+
+ // If there's any locks left in the 'mustLocks' array, it means that
+ // the resource was locked and we must block it.
+ if ($mustLocks) {
+ throw new DAV\Exception\Locked(reset($mustLocks));
+ }
+ }
+
+ /**
+ * Parses a webdav lock xml body, and returns a new Sabre\DAV\Locks\LockInfo object.
+ *
+ * @param string $body
+ *
+ * @return LockInfo
+ */
+ protected function parseLockRequest($body)
+ {
+ $result = $this->server->xml->expect(
+ '{DAV:}lockinfo',
+ $body
+ );
+
+ $lockInfo = new LockInfo();
+
+ $lockInfo->owner = $result->owner;
+ $lockInfo->token = DAV\UUIDUtil::getUUID();
+ $lockInfo->scope = $result->scope;
+
+ return $lockInfo;
+ }
+
+ /**
+ * Returns a bunch of meta-data about the plugin.
+ *
+ * Providing this information is optional, and is mainly displayed by the
+ * Browser plugin.
+ *
+ * The description key in the returned array may contain html and will not
+ * be sanitized.
+ *
+ * @return array
+ */
+ public function getPluginInfo()
+ {
+ return [
+ 'name' => $this->getPluginName(),
+ 'description' => 'The locks plugin turns this server into a class-2 WebDAV server and adds support for LOCK and UNLOCK',
+ 'link' => 'http://sabre.io/dav/locks/',
+ ];
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/MkCol.php b/lib/composer/vendor/sabre/dav/lib/DAV/MkCol.php
new file mode 100644
index 0000000..f3c5ea5
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/MkCol.php
@@ -0,0 +1,71 @@
+resourceType = $resourceType;
+ parent::__construct($mutations);
+ }
+
+ /**
+ * Returns the resourcetype of the new collection.
+ *
+ * @return string[]
+ */
+ public function getResourceType()
+ {
+ return $this->resourceType;
+ }
+
+ /**
+ * Returns true or false if the MKCOL operation has at least the specified
+ * resource type.
+ *
+ * If the resourcetype is specified as an array, all resourcetypes are
+ * checked.
+ *
+ * @param string|string[] $resourceType
+ *
+ * @return bool
+ */
+ public function hasResourceType($resourceType)
+ {
+ return 0 === count(array_diff((array) $resourceType, $this->resourceType));
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Mount/Plugin.php b/lib/composer/vendor/sabre/dav/lib/DAV/Mount/Plugin.php
new file mode 100644
index 0000000..b7f4851
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Mount/Plugin.php
@@ -0,0 +1,78 @@
+server = $server;
+ $this->server->on('method:GET', [$this, 'httpGet'], 90);
+ }
+
+ /**
+ * 'beforeMethod' event handles. This event handles intercepts GET requests ending
+ * with ?mount.
+ *
+ * @return bool
+ */
+ public function httpGet(RequestInterface $request, ResponseInterface $response)
+ {
+ $queryParams = $request->getQueryParameters();
+ if (!array_key_exists('mount', $queryParams)) {
+ return;
+ }
+
+ $currentUri = $request->getAbsoluteUrl();
+
+ // Stripping off everything after the ?
+ list($currentUri) = explode('?', $currentUri);
+
+ $this->davMount($response, $currentUri);
+
+ // Returning false to break the event chain
+ return false;
+ }
+
+ /**
+ * Generates the davmount response.
+ *
+ * @param string $uri absolute uri
+ */
+ public function davMount(ResponseInterface $response, $uri)
+ {
+ $response->setStatus(200);
+ $response->setHeader('Content-Type', 'application/davmount+xml');
+ ob_start();
+ echo '', "\n";
+ echo "\n";
+ echo ' ', htmlspecialchars($uri, ENT_NOQUOTES, 'UTF-8'), "\n";
+ echo '';
+ $response->setBody(ob_get_clean());
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Node.php b/lib/composer/vendor/sabre/dav/lib/DAV/Node.php
new file mode 100644
index 0000000..948060d
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Node.php
@@ -0,0 +1,51 @@
+addPlugin($patchPlugin);
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Jean-Tiare LE BIGOT (http://www.jtlebi.fr/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Plugin extends DAV\ServerPlugin
+{
+ const RANGE_APPEND = 1;
+ const RANGE_START = 2;
+ const RANGE_END = 3;
+
+ /**
+ * Reference to server.
+ *
+ * @var DAV\Server
+ */
+ protected $server;
+
+ /**
+ * Initializes the plugin.
+ *
+ * This method is automatically called by the Server class after addPlugin.
+ */
+ public function initialize(DAV\Server $server)
+ {
+ $this->server = $server;
+ $server->on('method:PATCH', [$this, 'httpPatch']);
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ public function getPluginName()
+ {
+ return 'partialupdate';
+ }
+
+ /**
+ * Use this method to tell the server this plugin defines additional
+ * HTTP methods.
+ *
+ * This method is passed a uri. It should only return HTTP methods that are
+ * available for the specified uri.
+ *
+ * We claim to support PATCH method (partirl update) if and only if
+ * - the node exist
+ * - the node implements our partial update interface
+ *
+ * @param string $uri
+ *
+ * @return array
+ */
+ public function getHTTPMethods($uri)
+ {
+ $tree = $this->server->tree;
+
+ if ($tree->nodeExists($uri)) {
+ $node = $tree->getNodeForPath($uri);
+ if ($node instanceof IPatchSupport) {
+ return ['PATCH'];
+ }
+ }
+
+ return [];
+ }
+
+ /**
+ * Returns a list of features for the HTTP OPTIONS Dav: header.
+ *
+ * @return array
+ */
+ public function getFeatures()
+ {
+ return ['sabredav-partialupdate'];
+ }
+
+ /**
+ * Patch an uri.
+ *
+ * The WebDAV patch request can be used to modify only a part of an
+ * existing resource. If the resource does not exist yet and the first
+ * offset is not 0, the request fails
+ */
+ public function httpPatch(RequestInterface $request, ResponseInterface $response)
+ {
+ $path = $request->getPath();
+
+ // Get the node. Will throw a 404 if not found
+ $node = $this->server->tree->getNodeForPath($path);
+ if (!$node instanceof IPatchSupport) {
+ throw new DAV\Exception\MethodNotAllowed('The target resource does not support the PATCH method.');
+ }
+
+ $range = $this->getHTTPUpdateRange($request);
+
+ if (!$range) {
+ throw new DAV\Exception\BadRequest('No valid "X-Update-Range" found in the headers');
+ }
+
+ $contentType = strtolower(
+ (string) $request->getHeader('Content-Type')
+ );
+
+ if ('application/x-sabredav-partialupdate' != $contentType) {
+ throw new DAV\Exception\UnsupportedMediaType('Unknown Content-Type header "'.$contentType.'"');
+ }
+
+ $len = $this->server->httpRequest->getHeader('Content-Length');
+ if (!$len) {
+ throw new DAV\Exception\LengthRequired('A Content-Length header is required');
+ }
+ switch ($range[0]) {
+ case self::RANGE_START:
+ // Calculate the end-range if it doesn't exist.
+ if (!$range[2]) {
+ $range[2] = $range[1] + $len - 1;
+ } else {
+ if ($range[2] < $range[1]) {
+ throw new DAV\Exception\RequestedRangeNotSatisfiable('The end offset ('.$range[2].') is lower than the start offset ('.$range[1].')');
+ }
+ if ($range[2] - $range[1] + 1 != $len) {
+ throw new DAV\Exception\RequestedRangeNotSatisfiable('Actual data length ('.$len.') is not consistent with begin ('.$range[1].') and end ('.$range[2].') offsets');
+ }
+ }
+ break;
+ }
+
+ if (!$this->server->emit('beforeWriteContent', [$path, $node, null])) {
+ return;
+ }
+
+ $body = $this->server->httpRequest->getBody();
+
+ $etag = $node->patch($body, $range[0], isset($range[1]) ? $range[1] : null);
+
+ $this->server->emit('afterWriteContent', [$path, $node]);
+
+ $response->setHeader('Content-Length', '0');
+ if ($etag) {
+ $response->setHeader('ETag', $etag);
+ }
+ $response->setStatus(204);
+
+ // Breaks the event chain
+ return false;
+ }
+
+ /**
+ * Returns the HTTP custom range update header.
+ *
+ * This method returns null if there is no well-formed HTTP range request
+ * header. It returns array(1) if it was an append request, array(2,
+ * $start, $end) if it's a start and end range, lastly it's array(3,
+ * $endoffset) if the offset was negative, and should be calculated from
+ * the end of the file.
+ *
+ * Examples:
+ *
+ * null - invalid
+ * [1] - append
+ * [2,10,15] - update bytes 10, 11, 12, 13, 14, 15
+ * [2,10,null] - update bytes 10 until the end of the patch body
+ * [3,-5] - update from 5 bytes from the end of the file.
+ *
+ * @return array|null
+ */
+ public function getHTTPUpdateRange(RequestInterface $request)
+ {
+ $range = $request->getHeader('X-Update-Range');
+ if (is_null($range)) {
+ return null;
+ }
+
+ // Matching "Range: bytes=1234-5678: both numbers are optional
+
+ if (!preg_match('/^(append)|(?:bytes=([0-9]+)-([0-9]*))|(?:bytes=(-[0-9]+))$/i', $range, $matches)) {
+ return null;
+ }
+
+ if ('append' === $matches[1]) {
+ return [self::RANGE_APPEND];
+ } elseif (strlen($matches[2]) > 0) {
+ return [self::RANGE_START, (int) $matches[2], (int) $matches[3] ?: null];
+ } else {
+ return [self::RANGE_END, (int) $matches[4]];
+ }
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/PropFind.php b/lib/composer/vendor/sabre/dav/lib/DAV/PropFind.php
new file mode 100644
index 0000000..e9ffb07
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/PropFind.php
@@ -0,0 +1,335 @@
+path = $path;
+ $this->properties = $properties;
+ $this->depth = $depth;
+ $this->requestType = $requestType;
+
+ if (self::ALLPROPS === $requestType) {
+ $this->properties = [
+ '{DAV:}getlastmodified',
+ '{DAV:}getcontentlength',
+ '{DAV:}resourcetype',
+ '{DAV:}quota-used-bytes',
+ '{DAV:}quota-available-bytes',
+ '{DAV:}getetag',
+ '{DAV:}getcontenttype',
+ ];
+ }
+
+ foreach ($this->properties as $propertyName) {
+ // Seeding properties with 404's.
+ $this->result[$propertyName] = [404, null];
+ }
+ $this->itemsLeft = count($this->result);
+ }
+
+ /**
+ * Handles a specific property.
+ *
+ * This method checks whether the specified property was requested in this
+ * PROPFIND request, and if so, it will call the callback and use the
+ * return value for it's value.
+ *
+ * Example:
+ *
+ * $propFind->handle('{DAV:}displayname', function() {
+ * return 'hello';
+ * });
+ *
+ * Note that handle will only work the first time. If null is returned, the
+ * value is ignored.
+ *
+ * It's also possible to not pass a callback, but immediately pass a value
+ *
+ * @param string $propertyName
+ * @param mixed $valueOrCallBack
+ */
+ public function handle($propertyName, $valueOrCallBack)
+ {
+ if ($this->itemsLeft && isset($this->result[$propertyName]) && 404 === $this->result[$propertyName][0]) {
+ if (is_callable($valueOrCallBack)) {
+ $value = $valueOrCallBack();
+ } else {
+ $value = $valueOrCallBack;
+ }
+ if (!is_null($value)) {
+ --$this->itemsLeft;
+ $this->result[$propertyName] = [200, $value];
+ }
+ }
+ }
+
+ /**
+ * Sets the value of the property.
+ *
+ * If status is not supplied, the status will default to 200 for non-null
+ * properties, and 404 for null properties.
+ *
+ * @param string $propertyName
+ * @param mixed $value
+ * @param int $status
+ */
+ public function set($propertyName, $value, $status = null)
+ {
+ if (is_null($status)) {
+ $status = is_null($value) ? 404 : 200;
+ }
+ // If this is an ALLPROPS request and the property is
+ // unknown, add it to the result; else ignore it:
+ if (!isset($this->result[$propertyName])) {
+ if (self::ALLPROPS === $this->requestType) {
+ $this->result[$propertyName] = [$status, $value];
+ }
+
+ return;
+ }
+ if (404 !== $status && 404 === $this->result[$propertyName][0]) {
+ --$this->itemsLeft;
+ } elseif (404 === $status && 404 !== $this->result[$propertyName][0]) {
+ ++$this->itemsLeft;
+ }
+ $this->result[$propertyName] = [$status, $value];
+ }
+
+ /**
+ * Returns the current value for a property.
+ *
+ * @param string $propertyName
+ *
+ * @return mixed
+ */
+ public function get($propertyName)
+ {
+ return isset($this->result[$propertyName]) ? $this->result[$propertyName][1] : null;
+ }
+
+ /**
+ * Returns the current status code for a property name.
+ *
+ * If the property does not appear in the list of requested properties,
+ * null will be returned.
+ *
+ * @param string $propertyName
+ *
+ * @return int|null
+ */
+ public function getStatus($propertyName)
+ {
+ return isset($this->result[$propertyName]) ? $this->result[$propertyName][0] : null;
+ }
+
+ /**
+ * Updates the path for this PROPFIND.
+ *
+ * @param string $path
+ */
+ public function setPath($path)
+ {
+ $this->path = $path;
+ }
+
+ /**
+ * Returns the path this PROPFIND request is for.
+ *
+ * @return string
+ */
+ public function getPath()
+ {
+ return $this->path;
+ }
+
+ /**
+ * Returns the depth of this propfind request.
+ *
+ * @return int
+ */
+ public function getDepth()
+ {
+ return $this->depth;
+ }
+
+ /**
+ * Updates the depth of this propfind request.
+ *
+ * @param int $depth
+ */
+ public function setDepth($depth)
+ {
+ $this->depth = $depth;
+ }
+
+ /**
+ * Returns all propertynames that have a 404 status, and thus don't have a
+ * value yet.
+ *
+ * @return array
+ */
+ public function get404Properties()
+ {
+ if (0 === $this->itemsLeft) {
+ return [];
+ }
+ $result = [];
+ foreach ($this->result as $propertyName => $stuff) {
+ if (404 === $stuff[0]) {
+ $result[] = $propertyName;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns the full list of requested properties.
+ *
+ * This returns just their names, not a status or value.
+ *
+ * @return array
+ */
+ public function getRequestedProperties()
+ {
+ return $this->properties;
+ }
+
+ /**
+ * Returns true if this was an '{DAV:}allprops' request.
+ *
+ * @return bool
+ */
+ public function isAllProps()
+ {
+ return self::ALLPROPS === $this->requestType;
+ }
+
+ /**
+ * Returns a result array that's often used in multistatus responses.
+ *
+ * The array uses status codes as keys, and property names and value pairs
+ * as the value of the top array.. such as :
+ *
+ * [
+ * 200 => [ '{DAV:}displayname' => 'foo' ],
+ * ]
+ *
+ * @return array
+ */
+ public function getResultForMultiStatus()
+ {
+ $r = [
+ 200 => [],
+ 404 => [],
+ ];
+ foreach ($this->result as $propertyName => $info) {
+ if (!isset($r[$info[0]])) {
+ $r[$info[0]] = [$propertyName => $info[1]];
+ } else {
+ $r[$info[0]][$propertyName] = $info[1];
+ }
+ }
+ // Removing the 404's for multi-status requests.
+ if (self::ALLPROPS === $this->requestType) {
+ unset($r[404]);
+ }
+
+ return $r;
+ }
+
+ /**
+ * The path that we're fetching properties for.
+ *
+ * @var string
+ */
+ protected $path;
+
+ /**
+ * The Depth of the request.
+ *
+ * 0 means only the current item. 1 means the current item + its children.
+ * It can also be DEPTH_INFINITY if this is enabled in the server.
+ *
+ * @var int
+ */
+ protected $depth = 0;
+
+ /**
+ * The type of request. See the TYPE constants.
+ */
+ protected $requestType;
+
+ /**
+ * A list of requested properties.
+ *
+ * @var array
+ */
+ protected $properties = [];
+
+ /**
+ * The result of the operation.
+ *
+ * The keys in this array are property names.
+ * The values are an array with two elements: the http status code and then
+ * optionally a value.
+ *
+ * Example:
+ *
+ * [
+ * "{DAV:}owner" : [404],
+ * "{DAV:}displayname" : [200, "Admin"]
+ * ]
+ *
+ * @var array
+ */
+ protected $result = [];
+
+ /**
+ * This is used as an internal counter for the number of properties that do
+ * not yet have a value.
+ *
+ * @var int
+ */
+ protected $itemsLeft;
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/PropPatch.php b/lib/composer/vendor/sabre/dav/lib/DAV/PropPatch.php
new file mode 100644
index 0000000..092909d
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/PropPatch.php
@@ -0,0 +1,337 @@
+mutations = $mutations;
+ }
+
+ /**
+ * Call this function if you wish to handle updating certain properties.
+ * For instance, your class may be responsible for handling updates for the
+ * {DAV:}displayname property.
+ *
+ * In that case, call this method with the first argument
+ * "{DAV:}displayname" and a second argument that's a method that does the
+ * actual updating.
+ *
+ * It's possible to specify more than one property as an array.
+ *
+ * The callback must return a boolean or an it. If the result is true, the
+ * operation was considered successful. If it's false, it's consided
+ * failed.
+ *
+ * If the result is an integer, we'll use that integer as the http status
+ * code associated with the operation.
+ *
+ * @param string|string[] $properties
+ */
+ public function handle($properties, callable $callback)
+ {
+ $usedProperties = [];
+ foreach ((array) $properties as $propertyName) {
+ if (array_key_exists($propertyName, $this->mutations) && !isset($this->result[$propertyName])) {
+ $usedProperties[] = $propertyName;
+ // HTTP Accepted
+ $this->result[$propertyName] = 202;
+ }
+ }
+
+ // Only registering if there's any unhandled properties.
+ if (!$usedProperties) {
+ return;
+ }
+ $this->propertyUpdateCallbacks[] = [
+ // If the original argument to this method was a string, we need
+ // to also make sure that it stays that way, so the commit function
+ // knows how to format the arguments to the callback.
+ is_string($properties) ? $properties : $usedProperties,
+ $callback,
+ ];
+ }
+
+ /**
+ * Call this function if you wish to handle _all_ properties that haven't
+ * been handled by anything else yet. Note that you effectively claim with
+ * this that you promise to process _all_ properties that are coming in.
+ */
+ public function handleRemaining(callable $callback)
+ {
+ $properties = $this->getRemainingMutations();
+ if (!$properties) {
+ // Nothing to do, don't register callback
+ return;
+ }
+
+ foreach ($properties as $propertyName) {
+ // HTTP Accepted
+ $this->result[$propertyName] = 202;
+
+ $this->propertyUpdateCallbacks[] = [
+ $properties,
+ $callback,
+ ];
+ }
+ }
+
+ /**
+ * Sets the result code for one or more properties.
+ *
+ * @param string|string[] $properties
+ * @param int $resultCode
+ */
+ public function setResultCode($properties, $resultCode)
+ {
+ foreach ((array) $properties as $propertyName) {
+ $this->result[$propertyName] = $resultCode;
+ }
+
+ if ($resultCode >= 400) {
+ $this->failed = true;
+ }
+ }
+
+ /**
+ * Sets the result code for all properties that did not have a result yet.
+ *
+ * @param int $resultCode
+ */
+ public function setRemainingResultCode($resultCode)
+ {
+ $this->setResultCode(
+ $this->getRemainingMutations(),
+ $resultCode
+ );
+ }
+
+ /**
+ * Returns the list of properties that don't have a result code yet.
+ *
+ * This method returns a list of property names, but not its values.
+ *
+ * @return string[]
+ */
+ public function getRemainingMutations()
+ {
+ $remaining = [];
+ foreach ($this->mutations as $propertyName => $propValue) {
+ if (!isset($this->result[$propertyName])) {
+ $remaining[] = $propertyName;
+ }
+ }
+
+ return $remaining;
+ }
+
+ /**
+ * Returns the list of properties that don't have a result code yet.
+ *
+ * This method returns list of properties and their values.
+ *
+ * @return array
+ */
+ public function getRemainingValues()
+ {
+ $remaining = [];
+ foreach ($this->mutations as $propertyName => $propValue) {
+ if (!isset($this->result[$propertyName])) {
+ $remaining[$propertyName] = $propValue;
+ }
+ }
+
+ return $remaining;
+ }
+
+ /**
+ * Performs the actual update, and calls all callbacks.
+ *
+ * This method returns true or false depending on if the operation was
+ * successful.
+ *
+ * @return bool
+ */
+ public function commit()
+ {
+ // First we validate if every property has a handler
+ foreach ($this->mutations as $propertyName => $value) {
+ if (!isset($this->result[$propertyName])) {
+ $this->failed = true;
+ $this->result[$propertyName] = 403;
+ }
+ }
+
+ foreach ($this->propertyUpdateCallbacks as $callbackInfo) {
+ if ($this->failed) {
+ break;
+ }
+ if (is_string($callbackInfo[0])) {
+ $this->doCallbackSingleProp($callbackInfo[0], $callbackInfo[1]);
+ } else {
+ $this->doCallbackMultiProp($callbackInfo[0], $callbackInfo[1]);
+ }
+ }
+
+ /*
+ * If anywhere in this operation updating a property failed, we must
+ * update all other properties accordingly.
+ */
+ if ($this->failed) {
+ foreach ($this->result as $propertyName => $status) {
+ if (202 === $status) {
+ // Failed dependency
+ $this->result[$propertyName] = 424;
+ }
+ }
+ }
+
+ return !$this->failed;
+ }
+
+ /**
+ * Executes a property callback with the single-property syntax.
+ *
+ * @param string $propertyName
+ */
+ private function doCallBackSingleProp($propertyName, callable $callback)
+ {
+ $result = $callback($this->mutations[$propertyName]);
+ if (is_bool($result)) {
+ if ($result) {
+ if (is_null($this->mutations[$propertyName])) {
+ // Delete
+ $result = 204;
+ } else {
+ // Update
+ $result = 200;
+ }
+ } else {
+ // Fail
+ $result = 403;
+ }
+ }
+ if (!is_int($result)) {
+ throw new UnexpectedValueException('A callback sent to handle() did not return an int or a bool');
+ }
+ $this->result[$propertyName] = $result;
+ if ($result >= 400) {
+ $this->failed = true;
+ }
+ }
+
+ /**
+ * Executes a property callback with the multi-property syntax.
+ */
+ private function doCallBackMultiProp(array $propertyList, callable $callback)
+ {
+ $argument = [];
+ foreach ($propertyList as $propertyName) {
+ $argument[$propertyName] = $this->mutations[$propertyName];
+ }
+
+ $result = $callback($argument);
+
+ if (is_array($result)) {
+ foreach ($propertyList as $propertyName) {
+ if (!isset($result[$propertyName])) {
+ $resultCode = 500;
+ } else {
+ $resultCode = $result[$propertyName];
+ }
+ if ($resultCode >= 400) {
+ $this->failed = true;
+ }
+ $this->result[$propertyName] = $resultCode;
+ }
+ } elseif (true === $result) {
+ // Success
+ foreach ($argument as $propertyName => $propertyValue) {
+ $this->result[$propertyName] = is_null($propertyValue) ? 204 : 200;
+ }
+ } elseif (false === $result) {
+ // Fail :(
+ $this->failed = true;
+ foreach ($propertyList as $propertyName) {
+ $this->result[$propertyName] = 403;
+ }
+ } else {
+ throw new UnexpectedValueException('A callback sent to handle() did not return an array or a bool');
+ }
+ }
+
+ /**
+ * Returns the result of the operation.
+ *
+ * @return array
+ */
+ public function getResult()
+ {
+ return $this->result;
+ }
+
+ /**
+ * Returns the full list of mutations.
+ *
+ * @return array
+ */
+ public function getMutations()
+ {
+ return $this->mutations;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/BackendInterface.php b/lib/composer/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/BackendInterface.php
new file mode 100644
index 0000000..64a8825
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/BackendInterface.php
@@ -0,0 +1,75 @@
+isAllProps().
+ *
+ * @param string $path
+ */
+ public function propFind($path, PropFind $propFind);
+
+ /**
+ * Updates properties for a path.
+ *
+ * This method received a PropPatch object, which contains all the
+ * information about the update.
+ *
+ * Usually you would want to call 'handleRemaining' on this object, to get;
+ * a list of all properties that need to be stored.
+ *
+ * @param string $path
+ */
+ public function propPatch($path, PropPatch $propPatch);
+
+ /**
+ * This method is called after a node is deleted.
+ *
+ * This allows a backend to clean up all associated properties.
+ *
+ * The delete method will get called once for the deletion of an entire
+ * tree.
+ *
+ * @param string $path
+ */
+ public function delete($path);
+
+ /**
+ * This method is called after a successful MOVE.
+ *
+ * This should be used to migrate all properties from one path to another.
+ * Note that entire collections may be moved, so ensure that all properties
+ * for children are also moved along.
+ *
+ * @param string $source
+ * @param string $destination
+ */
+ public function move($source, $destination);
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php b/lib/composer/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php
new file mode 100644
index 0000000..8960331
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php
@@ -0,0 +1,224 @@
+pdo = $pdo;
+ }
+
+ /**
+ * Fetches properties for a path.
+ *
+ * This method received a PropFind object, which contains all the
+ * information about the properties that need to be fetched.
+ *
+ * Usually you would just want to call 'get404Properties' on this object,
+ * as this will give you the _exact_ list of properties that need to be
+ * fetched, and haven't yet.
+ *
+ * However, you can also support the 'allprops' property here. In that
+ * case, you should check for $propFind->isAllProps().
+ *
+ * @param string $path
+ */
+ public function propFind($path, PropFind $propFind)
+ {
+ if (!$propFind->isAllProps() && 0 === count($propFind->get404Properties())) {
+ return;
+ }
+
+ $query = 'SELECT name, value, valuetype FROM '.$this->tableName.' WHERE path = ?';
+ $stmt = $this->pdo->prepare($query);
+ $stmt->execute([$path]);
+
+ while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ if ('resource' === gettype($row['value'])) {
+ $row['value'] = stream_get_contents($row['value']);
+ }
+ switch ($row['valuetype']) {
+ case null:
+ case self::VT_STRING:
+ $propFind->set($row['name'], $row['value']);
+ break;
+ case self::VT_XML:
+ $propFind->set($row['name'], new Complex($row['value']));
+ break;
+ case self::VT_OBJECT:
+ $propFind->set($row['name'], unserialize($row['value']));
+ break;
+ }
+ }
+ }
+
+ /**
+ * Updates properties for a path.
+ *
+ * This method received a PropPatch object, which contains all the
+ * information about the update.
+ *
+ * Usually you would want to call 'handleRemaining' on this object, to get;
+ * a list of all properties that need to be stored.
+ *
+ * @param string $path
+ */
+ public function propPatch($path, PropPatch $propPatch)
+ {
+ $propPatch->handleRemaining(function ($properties) use ($path) {
+ if ('pgsql' === $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME)) {
+ $updateSql = <<tableName} (path, name, valuetype, value)
+VALUES (:path, :name, :valuetype, :value)
+ON CONFLICT (path, name)
+DO UPDATE SET valuetype = :valuetype, value = :value
+SQL;
+ } else {
+ $updateSql = <<tableName} (path, name, valuetype, value)
+VALUES (:path, :name, :valuetype, :value)
+SQL;
+ }
+
+ $updateStmt = $this->pdo->prepare($updateSql);
+ $deleteStmt = $this->pdo->prepare('DELETE FROM '.$this->tableName.' WHERE path = ? AND name = ?');
+
+ foreach ($properties as $name => $value) {
+ if (!is_null($value)) {
+ if (is_scalar($value)) {
+ $valueType = self::VT_STRING;
+ } elseif ($value instanceof Complex) {
+ $valueType = self::VT_XML;
+ $value = $value->getXml();
+ } else {
+ $valueType = self::VT_OBJECT;
+ $value = serialize($value);
+ }
+
+ $updateStmt->bindParam('path', $path, \PDO::PARAM_STR);
+ $updateStmt->bindParam('name', $name, \PDO::PARAM_STR);
+ $updateStmt->bindParam('valuetype', $valueType, \PDO::PARAM_INT);
+ $updateStmt->bindParam('value', $value, \PDO::PARAM_LOB);
+
+ $updateStmt->execute();
+ } else {
+ $deleteStmt->execute([$path, $name]);
+ }
+ }
+
+ return true;
+ });
+ }
+
+ /**
+ * This method is called after a node is deleted.
+ *
+ * This allows a backend to clean up all associated properties.
+ *
+ * The delete method will get called once for the deletion of an entire
+ * tree.
+ *
+ * @param string $path
+ */
+ public function delete($path)
+ {
+ $stmt = $this->pdo->prepare('DELETE FROM '.$this->tableName." WHERE path = ? OR path LIKE ? ESCAPE '='");
+ $childPath = strtr(
+ $path,
+ [
+ '=' => '==',
+ '%' => '=%',
+ '_' => '=_',
+ ]
+ ).'/%';
+
+ $stmt->execute([$path, $childPath]);
+ }
+
+ /**
+ * This method is called after a successful MOVE.
+ *
+ * This should be used to migrate all properties from one path to another.
+ * Note that entire collections may be moved, so ensure that all properties
+ * for children are also moved along.
+ *
+ * @param string $source
+ * @param string $destination
+ */
+ public function move($source, $destination)
+ {
+ // I don't know a way to write this all in a single sql query that's
+ // also compatible across db engines, so we're letting PHP do all the
+ // updates. Much slower, but it should still be pretty fast in most
+ // cases.
+ $select = $this->pdo->prepare('SELECT id, path FROM '.$this->tableName.' WHERE path = ? OR path LIKE ?');
+ $select->execute([$source, $source.'/%']);
+
+ $update = $this->pdo->prepare('UPDATE '.$this->tableName.' SET path = ? WHERE id = ?');
+ while ($row = $select->fetch(\PDO::FETCH_ASSOC)) {
+ // Sanity check. SQL may select too many records, such as records
+ // with different cases.
+ if ($row['path'] !== $source && 0 !== strpos($row['path'], $source.'/')) {
+ continue;
+ }
+
+ $trailingPart = substr($row['path'], strlen($source) + 1);
+ $newPath = $destination;
+ if ($trailingPart) {
+ $newPath .= '/'.$trailingPart;
+ }
+ $update->execute([$newPath, $row['id']]);
+ }
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/PropertyStorage/Plugin.php b/lib/composer/vendor/sabre/dav/lib/DAV/PropertyStorage/Plugin.php
new file mode 100644
index 0000000..da47ec9
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/PropertyStorage/Plugin.php
@@ -0,0 +1,176 @@
+backend = $backend;
+ }
+
+ /**
+ * This initializes the plugin.
+ *
+ * This function is called by Sabre\DAV\Server, after
+ * addPlugin is called.
+ *
+ * This method should set up the required event subscriptions.
+ */
+ public function initialize(Server $server)
+ {
+ $server->on('propFind', [$this, 'propFind'], 130);
+ $server->on('propPatch', [$this, 'propPatch'], 300);
+ $server->on('afterMove', [$this, 'afterMove']);
+ $server->on('afterUnbind', [$this, 'afterUnbind']);
+ }
+
+ /**
+ * Called during PROPFIND operations.
+ *
+ * If there's any requested properties that don't have a value yet, this
+ * plugin will look in the property storage backend to find them.
+ */
+ public function propFind(PropFind $propFind, INode $node)
+ {
+ $path = $propFind->getPath();
+ $pathFilter = $this->pathFilter;
+ if ($pathFilter && !$pathFilter($path)) {
+ return;
+ }
+ $this->backend->propFind($propFind->getPath(), $propFind);
+ }
+
+ /**
+ * Called during PROPPATCH operations.
+ *
+ * If there's any updated properties that haven't been stored, the
+ * propertystorage backend can handle it.
+ *
+ * @param string $path
+ */
+ public function propPatch($path, PropPatch $propPatch)
+ {
+ $pathFilter = $this->pathFilter;
+ if ($pathFilter && !$pathFilter($path)) {
+ return;
+ }
+ $this->backend->propPatch($path, $propPatch);
+ }
+
+ /**
+ * Called after a node is deleted.
+ *
+ * This allows the backend to clean up any properties still in the
+ * database.
+ *
+ * @param string $path
+ */
+ public function afterUnbind($path)
+ {
+ $pathFilter = $this->pathFilter;
+ if ($pathFilter && !$pathFilter($path)) {
+ return;
+ }
+ $this->backend->delete($path);
+ }
+
+ /**
+ * Called after a node is moved.
+ *
+ * This allows the backend to move all the associated properties.
+ *
+ * @param string $source
+ * @param string $destination
+ */
+ public function afterMove($source, $destination)
+ {
+ $pathFilter = $this->pathFilter;
+ if ($pathFilter && !$pathFilter($source)) {
+ return;
+ }
+ // If the destination is filtered, afterUnbind will handle cleaning up
+ // the properties.
+ if ($pathFilter && !$pathFilter($destination)) {
+ return;
+ }
+
+ $this->backend->move($source, $destination);
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using \Sabre\DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ public function getPluginName()
+ {
+ return 'property-storage';
+ }
+
+ /**
+ * Returns a bunch of meta-data about the plugin.
+ *
+ * Providing this information is optional, and is mainly displayed by the
+ * Browser plugin.
+ *
+ * The description key in the returned array may contain html and will not
+ * be sanitized.
+ *
+ * @return array
+ */
+ public function getPluginInfo()
+ {
+ return [
+ 'name' => $this->getPluginName(),
+ 'description' => 'This plugin allows any arbitrary WebDAV property to be set on any resource.',
+ 'link' => 'http://sabre.io/dav/property-storage/',
+ ];
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Server.php b/lib/composer/vendor/sabre/dav/lib/DAV/Server.php
new file mode 100644
index 0000000..3133e54
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Server.php
@@ -0,0 +1,1682 @@
+ '{DAV:}collection',
+ ];
+
+ /**
+ * This property allows the usage of Depth: infinity on PROPFIND requests.
+ *
+ * By default Depth: infinity is treated as Depth: 1. Allowing Depth:
+ * infinity is potentially risky, as it allows a single client to do a full
+ * index of the webdav server, which is an easy DoS attack vector.
+ *
+ * Only turn this on if you know what you're doing.
+ *
+ * @var bool
+ */
+ public $enablePropfindDepthInfinity = false;
+
+ /**
+ * Reference to the XML utility object.
+ *
+ * @var Xml\Service
+ */
+ public $xml;
+
+ /**
+ * If this setting is turned off, SabreDAV's version number will be hidden
+ * from various places.
+ *
+ * Some people feel this is a good security measure.
+ *
+ * @var bool
+ */
+ public static $exposeVersion = true;
+
+ /**
+ * If this setting is turned on, any multi status response on any PROPFIND will be streamed to the output buffer.
+ * This will be beneficial for large result sets which will no longer consume a large amount of memory as well as
+ * send back data to the client earlier.
+ *
+ * @var bool
+ */
+ public static $streamMultiStatus = false;
+
+ /**
+ * Sets up the server.
+ *
+ * If a Sabre\DAV\Tree object is passed as an argument, it will
+ * use it as the directory tree. If a Sabre\DAV\INode is passed, it
+ * will create a Sabre\DAV\Tree and use the node as the root.
+ *
+ * If nothing is passed, a Sabre\DAV\SimpleCollection is created in
+ * a Sabre\DAV\Tree.
+ *
+ * If an array is passed, we automatically create a root node, and use
+ * the nodes in the array as top-level children.
+ *
+ * @param Tree|INode|array|null $treeOrNode The tree object
+ *
+ * @throws Exception
+ */
+ public function __construct($treeOrNode = null, ?HTTP\Sapi $sapi = null)
+ {
+ if ($treeOrNode instanceof Tree) {
+ $this->tree = $treeOrNode;
+ } elseif ($treeOrNode instanceof INode) {
+ $this->tree = new Tree($treeOrNode);
+ } elseif (is_array($treeOrNode)) {
+ $root = new SimpleCollection('root', $treeOrNode);
+ $this->tree = new Tree($root);
+ } elseif (is_null($treeOrNode)) {
+ $root = new SimpleCollection('root');
+ $this->tree = new Tree($root);
+ } else {
+ throw new Exception('Invalid argument passed to constructor. Argument must either be an instance of Sabre\\DAV\\Tree, Sabre\\DAV\\INode, an array or null');
+ }
+
+ $this->xml = new Xml\Service();
+ $this->sapi = $sapi ?? new HTTP\Sapi();
+ $this->httpResponse = new HTTP\Response();
+ $this->httpRequest = $this->sapi->getRequest();
+ $this->addPlugin(new CorePlugin());
+ }
+
+ /**
+ * Starts the DAV Server.
+ */
+ public function start()
+ {
+ try {
+ // If nginx (pre-1.2) is used as a proxy server, and SabreDAV as an
+ // origin, we must make sure we send back HTTP/1.0 if this was
+ // requested.
+ // This is mainly because nginx doesn't support Chunked Transfer
+ // Encoding, and this forces the webserver SabreDAV is running on,
+ // to buffer entire responses to calculate Content-Length.
+ $this->httpResponse->setHTTPVersion($this->httpRequest->getHTTPVersion());
+
+ // Setting the base url
+ $this->httpRequest->setBaseUrl($this->getBaseUri());
+ $this->invokeMethod($this->httpRequest, $this->httpResponse);
+ } catch (\Throwable $e) {
+ try {
+ $this->emit('exception', [$e]);
+ } catch (\Exception $ignore) {
+ }
+ $DOM = new \DOMDocument('1.0', 'utf-8');
+ $DOM->formatOutput = true;
+
+ $error = $DOM->createElementNS('DAV:', 'd:error');
+ $error->setAttribute('xmlns:s', self::NS_SABREDAV);
+ $DOM->appendChild($error);
+
+ $h = function ($v) {
+ return htmlspecialchars((string) $v, ENT_NOQUOTES, 'UTF-8');
+ };
+
+ if (self::$exposeVersion) {
+ $error->appendChild($DOM->createElement('s:sabredav-version', $h(Version::VERSION)));
+ }
+
+ $error->appendChild($DOM->createElement('s:exception', $h(get_class($e))));
+ $error->appendChild($DOM->createElement('s:message', $h($e->getMessage())));
+ if ($this->debugExceptions) {
+ $error->appendChild($DOM->createElement('s:file', $h($e->getFile())));
+ $error->appendChild($DOM->createElement('s:line', $h($e->getLine())));
+ $error->appendChild($DOM->createElement('s:code', $h($e->getCode())));
+ $error->appendChild($DOM->createElement('s:stacktrace', $h($e->getTraceAsString())));
+ }
+
+ if ($this->debugExceptions) {
+ $previous = $e;
+ while ($previous = $previous->getPrevious()) {
+ $xPrevious = $DOM->createElement('s:previous-exception');
+ $xPrevious->appendChild($DOM->createElement('s:exception', $h(get_class($previous))));
+ $xPrevious->appendChild($DOM->createElement('s:message', $h($previous->getMessage())));
+ $xPrevious->appendChild($DOM->createElement('s:file', $h($previous->getFile())));
+ $xPrevious->appendChild($DOM->createElement('s:line', $h($previous->getLine())));
+ $xPrevious->appendChild($DOM->createElement('s:code', $h($previous->getCode())));
+ $xPrevious->appendChild($DOM->createElement('s:stacktrace', $h($previous->getTraceAsString())));
+ $error->appendChild($xPrevious);
+ }
+ }
+
+ if ($e instanceof Exception) {
+ $httpCode = $e->getHTTPCode();
+ $e->serialize($this, $error);
+ $headers = $e->getHTTPHeaders($this);
+ } else {
+ $httpCode = 500;
+ $headers = [];
+ }
+ $headers['Content-Type'] = 'application/xml; charset=utf-8';
+
+ $this->httpResponse->setStatus($httpCode);
+ $this->httpResponse->setHeaders($headers);
+ $this->httpResponse->setBody($DOM->saveXML());
+ $this->sapi->sendResponse($this->httpResponse);
+ }
+ }
+
+ /**
+ * Alias of start().
+ *
+ * @deprecated
+ */
+ public function exec()
+ {
+ $this->start();
+ }
+
+ /**
+ * Sets the base server uri.
+ *
+ * @param string $uri
+ */
+ public function setBaseUri($uri)
+ {
+ // If the baseUri does not end with a slash, we must add it
+ if ('/' !== $uri[strlen($uri) - 1]) {
+ $uri .= '/';
+ }
+
+ $this->baseUri = $uri;
+ }
+
+ /**
+ * Returns the base responding uri.
+ *
+ * @return string
+ */
+ public function getBaseUri()
+ {
+ if (is_null($this->baseUri)) {
+ $this->baseUri = $this->guessBaseUri();
+ }
+
+ return $this->baseUri;
+ }
+
+ /**
+ * This method attempts to detect the base uri.
+ * Only the PATH_INFO variable is considered.
+ *
+ * If this variable is not set, the root (/) is assumed.
+ *
+ * @return string
+ */
+ public function guessBaseUri()
+ {
+ $pathInfo = $this->httpRequest->getRawServerValue('PATH_INFO');
+ $uri = $this->httpRequest->getRawServerValue('REQUEST_URI');
+
+ // If PATH_INFO is found, we can assume it's accurate.
+ if (!empty($pathInfo)) {
+ // We need to make sure we ignore the QUERY_STRING part
+ if ($pos = strpos($uri, '?')) {
+ $uri = substr($uri, 0, $pos);
+ }
+
+ // PATH_INFO is only set for urls, such as: /example.php/path
+ // in that case PATH_INFO contains '/path'.
+ // Note that REQUEST_URI is percent encoded, while PATH_INFO is
+ // not, Therefore they are only comparable if we first decode
+ // REQUEST_INFO as well.
+ $decodedUri = HTTP\decodePath($uri);
+
+ // A simple sanity check:
+ if (substr($decodedUri, strlen($decodedUri) - strlen($pathInfo)) === $pathInfo) {
+ $baseUri = substr($decodedUri, 0, strlen($decodedUri) - strlen($pathInfo));
+
+ return rtrim($baseUri, '/').'/';
+ }
+
+ throw new Exception('The REQUEST_URI ('.$uri.') did not end with the contents of PATH_INFO ('.$pathInfo.'). This server might be misconfigured.');
+ }
+
+ // The last fallback is that we're just going to assume the server root.
+ return '/';
+ }
+
+ /**
+ * Adds a plugin to the server.
+ *
+ * For more information, console the documentation of Sabre\DAV\ServerPlugin
+ */
+ public function addPlugin(ServerPlugin $plugin)
+ {
+ $this->plugins[$plugin->getPluginName()] = $plugin;
+ $plugin->initialize($this);
+ }
+
+ /**
+ * Returns an initialized plugin by it's name.
+ *
+ * This function returns null if the plugin was not found.
+ *
+ * @param string $name
+ *
+ * @return ServerPlugin
+ */
+ public function getPlugin($name)
+ {
+ if (isset($this->plugins[$name])) {
+ return $this->plugins[$name];
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns all plugins.
+ *
+ * @return array
+ */
+ public function getPlugins()
+ {
+ return $this->plugins;
+ }
+
+ /**
+ * Returns the PSR-3 logger object.
+ *
+ * @return LoggerInterface
+ */
+ public function getLogger()
+ {
+ if (!$this->logger) {
+ $this->logger = new NullLogger();
+ }
+
+ return $this->logger;
+ }
+
+ /**
+ * Handles a http request, and execute a method based on its name.
+ *
+ * @param bool $sendResponse whether to send the HTTP response to the DAV client
+ */
+ public function invokeMethod(RequestInterface $request, ResponseInterface $response, $sendResponse = true)
+ {
+ $method = $request->getMethod();
+
+ if (!$this->emit('beforeMethod:'.$method, [$request, $response])) {
+ return;
+ }
+
+ if (self::$exposeVersion) {
+ $response->setHeader('X-Sabre-Version', Version::VERSION);
+ }
+
+ $this->transactionType = strtolower($method);
+
+ if (!$this->checkPreconditions($request, $response)) {
+ $this->sapi->sendResponse($response);
+
+ return;
+ }
+
+ if ($this->emit('method:'.$method, [$request, $response])) {
+ $exMessage = 'There was no plugin in the system that was willing to handle this '.$method.' method.';
+ if ('GET' === $method) {
+ $exMessage .= ' Enable the Browser plugin to get a better result here.';
+ }
+
+ // Unsupported method
+ throw new Exception\NotImplemented($exMessage);
+ }
+
+ if (!$this->emit('afterMethod:'.$method, [$request, $response])) {
+ return;
+ }
+
+ if (null === $response->getStatus()) {
+ throw new Exception('No subsystem set a valid HTTP status code. Something must have interrupted the request without providing further detail.');
+ }
+ if ($sendResponse) {
+ $this->sapi->sendResponse($response);
+ $this->emit('afterResponse', [$request, $response]);
+ }
+ }
+
+ // {{{ HTTP/WebDAV protocol helpers
+
+ /**
+ * Returns an array with all the supported HTTP methods for a specific uri.
+ *
+ * @param string $path
+ *
+ * @return array
+ */
+ public function getAllowedMethods($path)
+ {
+ $methods = [
+ 'OPTIONS',
+ 'GET',
+ 'HEAD',
+ 'DELETE',
+ 'PROPFIND',
+ 'PUT',
+ 'PROPPATCH',
+ 'COPY',
+ 'MOVE',
+ 'REPORT',
+ ];
+
+ // The MKCOL is only allowed on an unmapped uri
+ try {
+ $this->tree->getNodeForPath($path);
+ } catch (Exception\NotFound $e) {
+ $methods[] = 'MKCOL';
+ }
+
+ // We're also checking if any of the plugins register any new methods
+ foreach ($this->plugins as $plugin) {
+ $methods = array_merge($methods, $plugin->getHTTPMethods($path));
+ }
+ array_unique($methods);
+
+ return $methods;
+ }
+
+ /**
+ * Gets the uri for the request, keeping the base uri into consideration.
+ *
+ * @return string
+ */
+ public function getRequestUri()
+ {
+ return $this->calculateUri($this->httpRequest->getUrl());
+ }
+
+ /**
+ * Turns a URI such as the REQUEST_URI into a local path.
+ *
+ * This method:
+ * * strips off the base path
+ * * normalizes the path
+ * * uri-decodes the path
+ *
+ * @param string $uri
+ *
+ * @throws Exception\Forbidden A permission denied exception is thrown whenever there was an attempt to supply a uri outside of the base uri
+ *
+ * @return string
+ */
+ public function calculateUri($uri)
+ {
+ if ('' != $uri && '/' != $uri[0] && strpos($uri, '://')) {
+ $uri = parse_url($uri, PHP_URL_PATH);
+ }
+
+ $uri = Uri\normalize(preg_replace('|/+|', '/', $uri));
+ $baseUri = Uri\normalize($this->getBaseUri());
+
+ if (0 === strpos($uri, $baseUri)) {
+ return trim(HTTP\decodePath(substr($uri, strlen($baseUri))), '/');
+
+ // A special case, if the baseUri was accessed without a trailing
+ // slash, we'll accept it as well.
+ } elseif ($uri.'/' === $baseUri) {
+ return '';
+ } else {
+ throw new Exception\Forbidden('Requested uri ('.$uri.') is out of base uri ('.$this->getBaseUri().')');
+ }
+ }
+
+ /**
+ * Returns the HTTP depth header.
+ *
+ * This method returns the contents of the HTTP depth request header. If the depth header was 'infinity' it will return the Sabre\DAV\Server::DEPTH_INFINITY object
+ * It is possible to supply a default depth value, which is used when the depth header has invalid content, or is completely non-existent
+ *
+ * @param mixed $default
+ *
+ * @return int
+ */
+ public function getHTTPDepth($default = self::DEPTH_INFINITY)
+ {
+ // If its not set, we'll grab the default
+ $depth = $this->httpRequest->getHeader('Depth');
+
+ if (is_null($depth)) {
+ return $default;
+ }
+
+ if ('infinity' == $depth) {
+ return self::DEPTH_INFINITY;
+ }
+
+ // If its an unknown value. we'll grab the default
+ if (!ctype_digit($depth)) {
+ return $default;
+ }
+
+ return (int) $depth;
+ }
+
+ /**
+ * Returns the HTTP range header.
+ *
+ * This method returns null if there is no well-formed HTTP range request
+ * header or array($start, $end).
+ *
+ * The first number is the offset of the first byte in the range.
+ * The second number is the offset of the last byte in the range.
+ *
+ * If the second offset is null, it should be treated as the offset of the last byte of the entity
+ * If the first offset is null, the second offset should be used to retrieve the last x bytes of the entity
+ *
+ * @return int[]|null
+ */
+ public function getHTTPRange()
+ {
+ $range = $this->httpRequest->getHeader('range');
+ if (is_null($range)) {
+ return null;
+ }
+
+ // Matching "Range: bytes=1234-5678: both numbers are optional
+
+ if (!preg_match('/^bytes=([0-9]*)-([0-9]*)$/i', $range, $matches)) {
+ return null;
+ }
+
+ if ('' === $matches[1] && '' === $matches[2]) {
+ return null;
+ }
+
+ return [
+ '' !== $matches[1] ? (int) $matches[1] : null,
+ '' !== $matches[2] ? (int) $matches[2] : null,
+ ];
+ }
+
+ /**
+ * Returns the HTTP Prefer header information.
+ *
+ * The prefer header is defined in:
+ * http://tools.ietf.org/html/draft-snell-http-prefer-14
+ *
+ * This method will return an array with options.
+ *
+ * Currently, the following options may be returned:
+ * [
+ * 'return-asynch' => true,
+ * 'return-minimal' => true,
+ * 'return-representation' => true,
+ * 'wait' => 30,
+ * 'strict' => true,
+ * 'lenient' => true,
+ * ]
+ *
+ * This method also supports the Brief header, and will also return
+ * 'return-minimal' if the brief header was set to 't'.
+ *
+ * For the boolean options, false will be returned if the headers are not
+ * specified. For the integer options it will be 'null'.
+ *
+ * @return array
+ */
+ public function getHTTPPrefer()
+ {
+ $result = [
+ // can be true or false
+ 'respond-async' => false,
+ // Could be set to 'representation' or 'minimal'.
+ 'return' => null,
+ // Used as a timeout, is usually a number.
+ 'wait' => null,
+ // can be 'strict' or 'lenient'.
+ 'handling' => false,
+ ];
+
+ if ($prefer = $this->httpRequest->getHeader('Prefer')) {
+ $result = array_merge(
+ $result,
+ HTTP\parsePrefer($prefer)
+ );
+ } elseif ('t' == $this->httpRequest->getHeader('Brief')) {
+ $result['return'] = 'minimal';
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns information about Copy and Move requests.
+ *
+ * This function is created to help getting information about the source and the destination for the
+ * WebDAV MOVE and COPY HTTP request. It also validates a lot of information and throws proper exceptions
+ *
+ * The returned value is an array with the following keys:
+ * * destination - Destination path
+ * * destinationExists - Whether or not the destination is an existing url (and should therefore be overwritten)
+ *
+ * @throws Exception\BadRequest upon missing or broken request headers
+ * @throws Exception\UnsupportedMediaType when trying to copy into a
+ * non-collection
+ * @throws Exception\PreconditionFailed if overwrite is set to false, but
+ * the destination exists
+ * @throws Exception\Forbidden when source and destination paths are
+ * identical
+ * @throws Exception\Conflict when trying to copy a node into its own
+ * subtree
+ *
+ * @return array
+ */
+ public function getCopyAndMoveInfo(RequestInterface $request)
+ {
+ // Collecting the relevant HTTP headers
+ if (!$request->getHeader('Destination')) {
+ throw new Exception\BadRequest('The destination header was not supplied');
+ }
+ $destination = $this->calculateUri($request->getHeader('Destination'));
+ $overwrite = $request->getHeader('Overwrite');
+ if (!$overwrite) {
+ $overwrite = 'T';
+ }
+ if ('T' == strtoupper($overwrite)) {
+ $overwrite = true;
+ } elseif ('F' == strtoupper($overwrite)) {
+ $overwrite = false;
+ }
+ // We need to throw a bad request exception, if the header was invalid
+ else {
+ throw new Exception\BadRequest('The HTTP Overwrite header should be either T or F');
+ }
+ list($destinationDir) = Uri\split($destination);
+
+ try {
+ $destinationParent = $this->tree->getNodeForPath($destinationDir);
+ if (!($destinationParent instanceof ICollection)) {
+ throw new Exception\UnsupportedMediaType('The destination node is not a collection');
+ }
+ } catch (Exception\NotFound $e) {
+ // If the destination parent node is not found, we throw a 409
+ throw new Exception\Conflict('The destination node is not found');
+ }
+
+ try {
+ $destinationNode = $this->tree->getNodeForPath($destination);
+
+ // If this succeeded, it means the destination already exists
+ // we'll need to throw precondition failed in case overwrite is false
+ if (!$overwrite) {
+ throw new Exception\PreconditionFailed('The destination node already exists, and the overwrite header is set to false', 'Overwrite');
+ }
+ } catch (Exception\NotFound $e) {
+ // Destination didn't exist, we're all good
+ $destinationNode = false;
+ }
+
+ $requestPath = $request->getPath();
+ if ($destination === $requestPath) {
+ throw new Exception\Forbidden('Source and destination uri are identical.');
+ }
+ if (substr($destination, 0, strlen($requestPath) + 1) === $requestPath.'/') {
+ throw new Exception\Conflict('The destination may not be part of the same subtree as the source path.');
+ }
+
+ // These are the three relevant properties we need to return
+ return [
+ 'destination' => $destination,
+ 'destinationExists' => (bool) $destinationNode,
+ 'destinationNode' => $destinationNode,
+ ];
+ }
+
+ /**
+ * Returns a list of properties for a path.
+ *
+ * This is a simplified version getPropertiesForPath. If you aren't
+ * interested in status codes, but you just want to have a flat list of
+ * properties, use this method.
+ *
+ * Please note though that any problems related to retrieving properties,
+ * such as permission issues will just result in an empty array being
+ * returned.
+ *
+ * @param string $path
+ * @param array $propertyNames
+ *
+ * @return array
+ */
+ public function getProperties($path, $propertyNames)
+ {
+ $result = $this->getPropertiesForPath($path, $propertyNames, 0);
+ if (isset($result[0][200])) {
+ return $result[0][200];
+ } else {
+ return [];
+ }
+ }
+
+ /**
+ * A kid-friendly way to fetch properties for a node's children.
+ *
+ * The returned array will be indexed by the path of the of child node.
+ * Only properties that are actually found will be returned.
+ *
+ * The parent node will not be returned.
+ *
+ * @param string $path
+ * @param array $propertyNames
+ *
+ * @return array
+ */
+ public function getPropertiesForChildren($path, $propertyNames)
+ {
+ $result = [];
+ foreach ($this->getPropertiesForPath($path, $propertyNames, 1) as $k => $row) {
+ // Skipping the parent path
+ if (0 === $k) {
+ continue;
+ }
+
+ $result[$row['href']] = $row[200];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns a list of HTTP headers for a particular resource.
+ *
+ * The generated http headers are based on properties provided by the
+ * resource. The method basically provides a simple mapping between
+ * DAV property and HTTP header.
+ *
+ * The headers are intended to be used for HEAD and GET requests.
+ *
+ * @param string $path
+ *
+ * @return array
+ */
+ public function getHTTPHeaders($path)
+ {
+ $propertyMap = [
+ '{DAV:}getcontenttype' => 'Content-Type',
+ '{DAV:}getcontentlength' => 'Content-Length',
+ '{DAV:}getlastmodified' => 'Last-Modified',
+ '{DAV:}getetag' => 'ETag',
+ ];
+
+ $properties = $this->getProperties($path, array_keys($propertyMap));
+
+ $headers = [];
+ foreach ($propertyMap as $property => $header) {
+ if (!isset($properties[$property])) {
+ continue;
+ }
+
+ if (is_scalar($properties[$property])) {
+ $headers[$header] = $properties[$property];
+
+ // GetLastModified gets special cased
+ } elseif ($properties[$property] instanceof Xml\Property\GetLastModified) {
+ $headers[$header] = HTTP\toDate($properties[$property]->getTime());
+ }
+ }
+
+ return $headers;
+ }
+
+ /**
+ * Small helper to support PROPFIND with DEPTH_INFINITY.
+ *
+ * @param array $yieldFirst
+ *
+ * @return \Traversable
+ */
+ private function generatePathNodes(PropFind $propFind, ?array $yieldFirst = null)
+ {
+ if (null !== $yieldFirst) {
+ yield $yieldFirst;
+ }
+ $newDepth = $propFind->getDepth();
+ $path = $propFind->getPath();
+
+ if (self::DEPTH_INFINITY !== $newDepth) {
+ --$newDepth;
+ }
+
+ $propertyNames = $propFind->getRequestedProperties();
+ $propFindType = !$propFind->isAllProps() ? PropFind::NORMAL : PropFind::ALLPROPS;
+
+ foreach ($this->tree->getChildren($path) as $childNode) {
+ if ('' !== $path) {
+ $subPath = $path.'/'.$childNode->getName();
+ } else {
+ $subPath = $childNode->getName();
+ }
+ $subPropFind = new PropFind($subPath, $propertyNames, $newDepth, $propFindType);
+
+ yield [
+ $subPropFind,
+ $childNode,
+ ];
+
+ if ((self::DEPTH_INFINITY === $newDepth || $newDepth >= 1) && $childNode instanceof ICollection) {
+ foreach ($this->generatePathNodes($subPropFind) as $subItem) {
+ yield $subItem;
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns a list of properties for a given path.
+ *
+ * The path that should be supplied should have the baseUrl stripped out
+ * The list of properties should be supplied in Clark notation. If the list is empty
+ * 'allprops' is assumed.
+ *
+ * If a depth of 1 is requested child elements will also be returned.
+ *
+ * @param string $path
+ * @param array $propertyNames
+ * @param int $depth
+ *
+ * @return array
+ *
+ * @deprecated Use getPropertiesIteratorForPath() instead (as it's more memory efficient)
+ * @see getPropertiesIteratorForPath()
+ */
+ public function getPropertiesForPath($path, $propertyNames = [], $depth = 0)
+ {
+ return iterator_to_array($this->getPropertiesIteratorForPath($path, $propertyNames, $depth));
+ }
+
+ /**
+ * Returns a list of properties for a given path.
+ *
+ * The path that should be supplied should have the baseUrl stripped out
+ * The list of properties should be supplied in Clark notation. If the list is empty
+ * 'allprops' is assumed.
+ *
+ * If a depth of 1 is requested child elements will also be returned.
+ *
+ * @param string $path
+ * @param array $propertyNames
+ * @param int $depth
+ *
+ * @return \Iterator
+ */
+ public function getPropertiesIteratorForPath($path, $propertyNames = [], $depth = 0)
+ {
+ // The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled
+ if (!$this->enablePropfindDepthInfinity && 0 != $depth) {
+ $depth = 1;
+ }
+
+ $path = trim($path, '/');
+
+ $propFindType = $propertyNames ? PropFind::NORMAL : PropFind::ALLPROPS;
+ $propFind = new PropFind($path, (array) $propertyNames, $depth, $propFindType);
+
+ $parentNode = $this->tree->getNodeForPath($path);
+
+ $propFindRequests = [[
+ $propFind,
+ $parentNode,
+ ]];
+
+ if (($depth > 0 || self::DEPTH_INFINITY === $depth) && $parentNode instanceof ICollection) {
+ $propFindRequests = $this->generatePathNodes(clone $propFind, current($propFindRequests));
+ }
+
+ foreach ($propFindRequests as $propFindRequest) {
+ list($propFind, $node) = $propFindRequest;
+ $r = $this->getPropertiesByNode($propFind, $node);
+ if ($r) {
+ $result = $propFind->getResultForMultiStatus();
+ $result['href'] = $propFind->getPath();
+
+ // WebDAV recommends adding a slash to the path, if the path is
+ // a collection.
+ // Furthermore, iCal also demands this to be the case for
+ // principals. This is non-standard, but we support it.
+ $resourceType = $this->getResourceTypeForNode($node);
+ if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) {
+ $result['href'] .= '/';
+ }
+ yield $result;
+ }
+ }
+ }
+
+ /**
+ * Returns a list of properties for a list of paths.
+ *
+ * The path that should be supplied should have the baseUrl stripped out
+ * The list of properties should be supplied in Clark notation. If the list is empty
+ * 'allprops' is assumed.
+ *
+ * The result is returned as an array, with paths for it's keys.
+ * The result may be returned out of order.
+ *
+ * @return array
+ */
+ public function getPropertiesForMultiplePaths(array $paths, array $propertyNames = [])
+ {
+ $result = [
+ ];
+
+ $nodes = $this->tree->getMultipleNodes($paths);
+
+ foreach ($nodes as $path => $node) {
+ $propFind = new PropFind($path, $propertyNames);
+ $r = $this->getPropertiesByNode($propFind, $node);
+ if ($r) {
+ $result[$path] = $propFind->getResultForMultiStatus();
+ $result[$path]['href'] = $path;
+
+ $resourceType = $this->getResourceTypeForNode($node);
+ if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) {
+ $result[$path]['href'] .= '/';
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Determines all properties for a node.
+ *
+ * This method tries to grab all properties for a node. This method is used
+ * internally getPropertiesForPath and a few others.
+ *
+ * It could be useful to call this, if you already have an instance of your
+ * target node and simply want to run through the system to get a correct
+ * list of properties.
+ *
+ * @return bool
+ */
+ public function getPropertiesByNode(PropFind $propFind, INode $node)
+ {
+ return $this->emit('propFind', [$propFind, $node]);
+ }
+
+ /**
+ * This method is invoked by sub-systems creating a new file.
+ *
+ * Currently this is done by HTTP PUT and HTTP LOCK (in the Locks_Plugin).
+ * It was important to get this done through a centralized function,
+ * allowing plugins to intercept this using the beforeCreateFile event.
+ *
+ * This method will return true if the file was actually created
+ *
+ * @param string $uri
+ * @param resource $data
+ * @param string $etag
+ *
+ * @return bool
+ */
+ public function createFile($uri, $data, &$etag = null)
+ {
+ list($dir, $name) = Uri\split($uri);
+
+ if (!$this->emit('beforeBind', [$uri])) {
+ return false;
+ }
+
+ try {
+ $parent = $this->tree->getNodeForPath($dir);
+ } catch (Exception\NotFound $e) {
+ throw new Exception\Conflict('Files cannot be created in non-existent collections');
+ }
+
+ if (!$parent instanceof ICollection) {
+ throw new Exception\Conflict('Files can only be created as children of collections');
+ }
+
+ // It is possible for an event handler to modify the content of the
+ // body, before it gets written. If this is the case, $modified
+ // should be set to true.
+ //
+ // If $modified is true, we must not send back an ETag.
+ $modified = false;
+ if (!$this->emit('beforeCreateFile', [$uri, &$data, $parent, &$modified])) {
+ return false;
+ }
+
+ $etag = $parent->createFile($name, $data);
+
+ if ($modified) {
+ $etag = null;
+ }
+
+ $this->tree->markDirty($dir.'/'.$name);
+
+ $this->emit('afterBind', [$uri]);
+ $this->emit('afterCreateFile', [$uri, $parent]);
+
+ return true;
+ }
+
+ /**
+ * This method is invoked by sub-systems updating a file.
+ *
+ * This method will return true if the file was actually updated
+ *
+ * @param string $uri
+ * @param resource $data
+ * @param string $etag
+ *
+ * @return bool
+ */
+ public function updateFile($uri, $data, &$etag = null)
+ {
+ $node = $this->tree->getNodeForPath($uri);
+
+ // It is possible for an event handler to modify the content of the
+ // body, before it gets written. If this is the case, $modified
+ // should be set to true.
+ //
+ // If $modified is true, we must not send back an ETag.
+ $modified = false;
+ if (!$this->emit('beforeWriteContent', [$uri, $node, &$data, &$modified])) {
+ return false;
+ }
+
+ $etag = $node->put($data);
+ if ($modified) {
+ $etag = null;
+ }
+ $this->emit('afterWriteContent', [$uri, $node]);
+
+ return true;
+ }
+
+ /**
+ * This method is invoked by sub-systems creating a new directory.
+ *
+ * @param string $uri
+ */
+ public function createDirectory($uri)
+ {
+ $this->createCollection($uri, new MkCol(['{DAV:}collection'], []));
+ }
+
+ /**
+ * Use this method to create a new collection.
+ *
+ * @param string $uri The new uri
+ *
+ * @return array|null
+ */
+ public function createCollection($uri, MkCol $mkCol)
+ {
+ list($parentUri, $newName) = Uri\split($uri);
+
+ // Making sure the parent exists
+ try {
+ $parent = $this->tree->getNodeForPath($parentUri);
+ } catch (Exception\NotFound $e) {
+ throw new Exception\Conflict('Parent node does not exist');
+ }
+
+ // Making sure the parent is a collection
+ if (!$parent instanceof ICollection) {
+ throw new Exception\Conflict('Parent node is not a collection');
+ }
+
+ // Making sure the child does not already exist
+ try {
+ $parent->getChild($newName);
+
+ // If we got here.. it means there's already a node on that url, and we need to throw a 405
+ throw new Exception\MethodNotAllowed('The resource you tried to create already exists');
+ } catch (Exception\NotFound $e) {
+ // NotFound is the expected behavior.
+ }
+
+ if (!$this->emit('beforeBind', [$uri])) {
+ return;
+ }
+
+ if ($parent instanceof IExtendedCollection) {
+ /*
+ * If the parent is an instance of IExtendedCollection, it means that
+ * we can pass the MkCol object directly as it may be able to store
+ * properties immediately.
+ */
+ $parent->createExtendedCollection($newName, $mkCol);
+ } else {
+ /*
+ * If the parent is a standard ICollection, it means only
+ * 'standard' collections can be created, so we should fail any
+ * MKCOL operation that carries extra resourcetypes.
+ */
+ if (count($mkCol->getResourceType()) > 1) {
+ throw new Exception\InvalidResourceType('The {DAV:}resourcetype you specified is not supported here.');
+ }
+
+ $parent->createDirectory($newName);
+ }
+
+ // If there are any properties that have not been handled/stored,
+ // we ask the 'propPatch' event to handle them. This will allow for
+ // example the propertyStorage system to store properties upon MKCOL.
+ if ($mkCol->getRemainingMutations()) {
+ $this->emit('propPatch', [$uri, $mkCol]);
+ }
+ $success = $mkCol->commit();
+
+ if (!$success) {
+ $result = $mkCol->getResult();
+
+ $formattedResult = [
+ 'href' => $uri,
+ ];
+
+ foreach ($result as $propertyName => $status) {
+ if (!isset($formattedResult[$status])) {
+ $formattedResult[$status] = [];
+ }
+ $formattedResult[$status][$propertyName] = null;
+ }
+
+ return $formattedResult;
+ }
+
+ $this->tree->markDirty($parentUri);
+ $this->emit('afterBind', [$uri]);
+ $this->emit('afterCreateCollection', [$uri]);
+ }
+
+ /**
+ * This method updates a resource's properties.
+ *
+ * The properties array must be a list of properties. Array-keys are
+ * property names in clarknotation, array-values are it's values.
+ * If a property must be deleted, the value should be null.
+ *
+ * Note that this request should either completely succeed, or
+ * completely fail.
+ *
+ * The response is an array with properties for keys, and http status codes
+ * as their values.
+ *
+ * @param string $path
+ *
+ * @return array
+ */
+ public function updateProperties($path, array $properties)
+ {
+ $propPatch = new PropPatch($properties);
+ $this->emit('propPatch', [$path, $propPatch]);
+ $propPatch->commit();
+
+ return $propPatch->getResult();
+ }
+
+ /**
+ * This method checks the main HTTP preconditions.
+ *
+ * Currently these are:
+ * * If-Match
+ * * If-None-Match
+ * * If-Modified-Since
+ * * If-Unmodified-Since
+ *
+ * The method will return true if all preconditions are met
+ * The method will return false, or throw an exception if preconditions
+ * failed. If false is returned the operation should be aborted, and
+ * the appropriate HTTP response headers are already set.
+ *
+ * Normally this method will throw 412 Precondition Failed for failures
+ * related to If-None-Match, If-Match and If-Unmodified Since. It will
+ * set the status to 304 Not Modified for If-Modified_since.
+ *
+ * @return bool
+ */
+ public function checkPreconditions(RequestInterface $request, ResponseInterface $response)
+ {
+ $path = $request->getPath();
+ $node = null;
+ $lastMod = null;
+ $etag = null;
+
+ if ($ifMatch = $request->getHeader('If-Match')) {
+ // If-Match contains an entity tag. Only if the entity-tag
+ // matches we are allowed to make the request succeed.
+ // If the entity-tag is '*' we are only allowed to make the
+ // request succeed if a resource exists at that url.
+ try {
+ $node = $this->tree->getNodeForPath($path);
+ } catch (Exception\NotFound $e) {
+ throw new Exception\PreconditionFailed('An If-Match header was specified and the resource did not exist', 'If-Match');
+ }
+
+ // Only need to check entity tags if they are not *
+ if ('*' !== $ifMatch) {
+ // There can be multiple ETags
+ $ifMatch = explode(',', $ifMatch);
+ $haveMatch = false;
+ foreach ($ifMatch as $ifMatchItem) {
+ // Stripping any extra spaces
+ $ifMatchItem = trim($ifMatchItem, ' ');
+
+ $etag = $node instanceof IFile ? $node->getETag() : null;
+ if ($etag === $ifMatchItem) {
+ $haveMatch = true;
+ } else {
+ // Evolution has a bug where it sometimes prepends the "
+ // with a \. This is our workaround.
+ if (str_replace('\\"', '"', $ifMatchItem) === $etag) {
+ $haveMatch = true;
+ }
+ }
+ }
+ if (!$haveMatch) {
+ if ($etag) {
+ $response->setHeader('ETag', $etag);
+ }
+ throw new Exception\PreconditionFailed('An If-Match header was specified, but none of the specified ETags matched.', 'If-Match');
+ }
+ }
+ }
+
+ if ($ifNoneMatch = $request->getHeader('If-None-Match')) {
+ // The If-None-Match header contains an ETag.
+ // Only if the ETag does not match the current ETag, the request will succeed
+ // The header can also contain *, in which case the request
+ // will only succeed if the entity does not exist at all.
+ $nodeExists = true;
+ if (!$node) {
+ try {
+ $node = $this->tree->getNodeForPath($path);
+ } catch (Exception\NotFound $e) {
+ $nodeExists = false;
+ }
+ }
+ if ($nodeExists) {
+ $haveMatch = false;
+ if ('*' === $ifNoneMatch) {
+ $haveMatch = true;
+ } else {
+ // There might be multiple ETags
+ $ifNoneMatch = explode(',', $ifNoneMatch);
+ $etag = $node instanceof IFile ? $node->getETag() : null;
+
+ foreach ($ifNoneMatch as $ifNoneMatchItem) {
+ // Stripping any extra spaces
+ $ifNoneMatchItem = trim($ifNoneMatchItem, ' ');
+
+ if ($etag === $ifNoneMatchItem) {
+ $haveMatch = true;
+ }
+ }
+ }
+
+ if ($haveMatch) {
+ if ($etag) {
+ $response->setHeader('ETag', $etag);
+ }
+ if ('GET' === $request->getMethod()) {
+ $response->setStatus(304);
+
+ return false;
+ } else {
+ throw new Exception\PreconditionFailed('An If-None-Match header was specified, but the ETag matched (or * was specified).', 'If-None-Match');
+ }
+ }
+ }
+ }
+
+ if (!$ifNoneMatch && ($ifModifiedSince = $request->getHeader('If-Modified-Since'))) {
+ // The If-Modified-Since header contains a date. We
+ // will only return the entity if it has been changed since
+ // that date. If it hasn't been changed, we return a 304
+ // header
+ // Note that this header only has to be checked if there was no If-None-Match header
+ // as per the HTTP spec.
+ $date = HTTP\parseDate($ifModifiedSince);
+
+ if ($date) {
+ if (is_null($node)) {
+ $node = $this->tree->getNodeForPath($path);
+ }
+ $lastMod = $node->getLastModified();
+ if ($lastMod) {
+ $lastMod = new \DateTime('@'.$lastMod);
+ if ($lastMod <= $date) {
+ $response->setStatus(304);
+ $response->setHeader('Last-Modified', HTTP\toDate($lastMod));
+
+ return false;
+ }
+ }
+ }
+ }
+
+ if ($ifUnmodifiedSince = $request->getHeader('If-Unmodified-Since')) {
+ // The If-Unmodified-Since will allow allow the request if the
+ // entity has not changed since the specified date.
+ $date = HTTP\parseDate($ifUnmodifiedSince);
+
+ // We must only check the date if it's valid
+ if ($date) {
+ if (is_null($node)) {
+ $node = $this->tree->getNodeForPath($path);
+ }
+ $lastMod = $node->getLastModified();
+ if ($lastMod) {
+ $lastMod = new \DateTime('@'.$lastMod);
+ if ($lastMod > $date) {
+ throw new Exception\PreconditionFailed('An If-Unmodified-Since header was specified, but the entity has been changed since the specified date.', 'If-Unmodified-Since');
+ }
+ }
+ }
+ }
+
+ // Now the hardest, the If: header. The If: header can contain multiple
+ // urls, ETags and so-called 'state tokens'.
+ //
+ // Examples of state tokens include lock-tokens (as defined in rfc4918)
+ // and sync-tokens (as defined in rfc6578).
+ //
+ // The only proper way to deal with these, is to emit events, that a
+ // Sync and Lock plugin can pick up.
+ $ifConditions = $this->getIfConditions($request);
+
+ foreach ($ifConditions as $kk => $ifCondition) {
+ foreach ($ifCondition['tokens'] as $ii => $token) {
+ $ifConditions[$kk]['tokens'][$ii]['validToken'] = false;
+ }
+ }
+
+ // Plugins are responsible for validating all the tokens.
+ // If a plugin deemed a token 'valid', it will set 'validToken' to
+ // true.
+ $this->emit('validateTokens', [$request, &$ifConditions]);
+
+ // Now we're going to analyze the result.
+
+ // Every ifCondition needs to validate to true, so we exit as soon as
+ // we have an invalid condition.
+ foreach ($ifConditions as $ifCondition) {
+ $uri = $ifCondition['uri'];
+ $tokens = $ifCondition['tokens'];
+
+ // We only need 1 valid token for the condition to succeed.
+ foreach ($tokens as $token) {
+ $tokenValid = $token['validToken'] || !$token['token'];
+
+ $etagValid = false;
+ if (!$token['etag']) {
+ $etagValid = true;
+ }
+ // Checking the ETag, only if the token was already deemed
+ // valid and there is one.
+ if ($token['etag'] && $tokenValid) {
+ // The token was valid, and there was an ETag. We must
+ // grab the current ETag and check it.
+ $node = $this->tree->getNodeForPath($uri);
+ $etagValid = $node instanceof IFile && $node->getETag() == $token['etag'];
+ }
+
+ if (($tokenValid && $etagValid) ^ $token['negate']) {
+ // Both were valid, so we can go to the next condition.
+ continue 2;
+ }
+ }
+
+ // If we ended here, it means there was no valid ETag + token
+ // combination found for the current condition. This means we fail!
+ throw new Exception\PreconditionFailed('Failed to find a valid token/etag combination for '.$uri, 'If');
+ }
+
+ return true;
+ }
+
+ /**
+ * This method is created to extract information from the WebDAV HTTP 'If:' header.
+ *
+ * The If header can be quite complex, and has a bunch of features. We're using a regex to extract all relevant information
+ * The function will return an array, containing structs with the following keys
+ *
+ * * uri - the uri the condition applies to.
+ * * tokens - The lock token. another 2 dimensional array containing 3 elements
+ *
+ * Example 1:
+ *
+ * If: ()
+ *
+ * Would result in:
+ *
+ * [
+ * [
+ * 'uri' => '/request/uri',
+ * 'tokens' => [
+ * [
+ * [
+ * 'negate' => false,
+ * 'token' => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2',
+ * 'etag' => ""
+ * ]
+ * ]
+ * ],
+ * ]
+ * ]
+ *
+ * Example 2:
+ *
+ * If: (Not ["Im An ETag"]) (["Another ETag"]) (Not ["Path2 ETag"])
+ *
+ * Would result in:
+ *
+ * [
+ * [
+ * 'uri' => 'path',
+ * 'tokens' => [
+ * [
+ * [
+ * 'negate' => true,
+ * 'token' => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2',
+ * 'etag' => '"Im An ETag"'
+ * ],
+ * [
+ * 'negate' => false,
+ * 'token' => '',
+ * 'etag' => '"Another ETag"'
+ * ]
+ * ]
+ * ],
+ * ],
+ * [
+ * 'uri' => 'path2',
+ * 'tokens' => [
+ * [
+ * [
+ * 'negate' => true,
+ * 'token' => '',
+ * 'etag' => '"Path2 ETag"'
+ * ]
+ * ]
+ * ],
+ * ],
+ * ]
+ *
+ * @return array
+ */
+ public function getIfConditions(RequestInterface $request)
+ {
+ $header = $request->getHeader('If');
+ if (!$header) {
+ return [];
+ }
+
+ $matches = [];
+
+ $regex = '/(?:\<(?P.*?)\>\s)?\((?PNot\s)?(?:\<(?P[^\>]*)\>)?(?:\s?)(?:\[(?P[^\]]*)\])?\)/im';
+ preg_match_all($regex, $header, $matches, PREG_SET_ORDER);
+
+ $conditions = [];
+
+ foreach ($matches as $match) {
+ // If there was no uri specified in this match, and there were
+ // already conditions parsed, we add the condition to the list of
+ // conditions for the previous uri.
+ if (!$match['uri'] && count($conditions)) {
+ $conditions[count($conditions) - 1]['tokens'][] = [
+ 'negate' => $match['not'] ? true : false,
+ 'token' => $match['token'],
+ 'etag' => isset($match['etag']) ? $match['etag'] : '',
+ ];
+ } else {
+ if (!$match['uri']) {
+ $realUri = $request->getPath();
+ } else {
+ $realUri = $this->calculateUri($match['uri']);
+ }
+
+ $conditions[] = [
+ 'uri' => $realUri,
+ 'tokens' => [
+ [
+ 'negate' => $match['not'] ? true : false,
+ 'token' => $match['token'],
+ 'etag' => isset($match['etag']) ? $match['etag'] : '',
+ ],
+ ],
+ ];
+ }
+ }
+
+ return $conditions;
+ }
+
+ /**
+ * Returns an array with resourcetypes for a node.
+ *
+ * @return array
+ */
+ public function getResourceTypeForNode(INode $node)
+ {
+ $result = [];
+ foreach ($this->resourceTypeMapping as $className => $resourceType) {
+ if ($node instanceof $className) {
+ $result[] = $resourceType;
+ }
+ }
+
+ return $result;
+ }
+
+ // }}}
+ // {{{ XML Readers & Writers
+
+ /**
+ * Returns a callback generating a WebDAV propfind response body based on a list of nodes.
+ *
+ * If 'strip404s' is set to true, all 404 responses will be removed.
+ *
+ * @param array|\Traversable $fileProperties The list with nodes
+ * @param bool $strip404s
+ *
+ * @return callable|string
+ */
+ public function generateMultiStatus($fileProperties, $strip404s = false)
+ {
+ $this->emit('beforeMultiStatus', [&$fileProperties]);
+
+ $w = $this->xml->getWriter();
+ if (self::$streamMultiStatus) {
+ return function () use ($fileProperties, $strip404s, $w) {
+ $w->openUri('php://output');
+ $this->writeMultiStatus($w, $fileProperties, $strip404s);
+ $w->flush();
+ };
+ }
+ $w->openMemory();
+ $this->writeMultiStatus($w, $fileProperties, $strip404s);
+
+ return $w->outputMemory();
+ }
+
+ /**
+ * @param $fileProperties
+ */
+ private function writeMultiStatus(Writer $w, $fileProperties, bool $strip404s)
+ {
+ $w->contextUri = $this->baseUri;
+ $w->startDocument();
+
+ $w->startElement('{DAV:}multistatus');
+
+ foreach ($fileProperties as $entry) {
+ $href = $entry['href'];
+ unset($entry['href']);
+ if ($strip404s) {
+ unset($entry[404]);
+ }
+ $response = new Xml\Element\Response(
+ ltrim($href, '/'),
+ $entry
+ );
+ $w->write([
+ 'name' => '{DAV:}response',
+ 'value' => $response,
+ ]);
+ }
+ $w->endElement();
+ $w->endDocument();
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/ServerPlugin.php b/lib/composer/vendor/sabre/dav/lib/DAV/ServerPlugin.php
new file mode 100644
index 0000000..70acb01
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/ServerPlugin.php
@@ -0,0 +1,105 @@
+ $this->getPluginName(),
+ 'description' => null,
+ 'link' => null,
+ ];
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Sharing/ISharedNode.php b/lib/composer/vendor/sabre/dav/lib/DAV/Sharing/ISharedNode.php
new file mode 100644
index 0000000..a746ac7
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Sharing/ISharedNode.php
@@ -0,0 +1,69 @@
+server = $server;
+
+ $server->xml->elementMap['{DAV:}share-resource'] = 'Sabre\\DAV\\Xml\\Request\\ShareResource';
+
+ array_push(
+ $server->protectedProperties,
+ '{DAV:}share-mode'
+ );
+
+ $server->on('method:POST', [$this, 'httpPost']);
+ $server->on('propFind', [$this, 'propFind']);
+ $server->on('getSupportedPrivilegeSet', [$this, 'getSupportedPrivilegeSet']);
+ $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']);
+ $server->on('onBrowserPostAction', [$this, 'browserPostAction']);
+ }
+
+ /**
+ * Updates the list of sharees on a shared resource.
+ *
+ * The sharees array is a list of people that are to be added modified
+ * or removed in the list of shares.
+ *
+ * @param string $path
+ * @param Sharee[] $sharees
+ */
+ public function shareResource($path, array $sharees)
+ {
+ $node = $this->server->tree->getNodeForPath($path);
+
+ if (!$node instanceof ISharedNode) {
+ throw new Forbidden('Sharing is not allowed on this node');
+ }
+
+ // Getting ACL info
+ $acl = $this->server->getPlugin('acl');
+
+ // If there's no ACL support, we allow everything
+ if ($acl) {
+ $acl->checkPrivileges($path, '{DAV:}share');
+ }
+
+ foreach ($sharees as $sharee) {
+ // We're going to attempt to get a local principal uri for a share
+ // href by emitting the getPrincipalByUri event.
+ $principal = null;
+ $this->server->emit('getPrincipalByUri', [$sharee->href, &$principal]);
+ $sharee->principal = $principal;
+ }
+ $node->updateInvites($sharees);
+ }
+
+ /**
+ * This event is triggered when properties are requested for nodes.
+ *
+ * This allows us to inject any sharings-specific properties.
+ */
+ public function propFind(PropFind $propFind, INode $node)
+ {
+ if ($node instanceof ISharedNode) {
+ $propFind->handle('{DAV:}share-access', function () use ($node) {
+ return new Property\ShareAccess($node->getShareAccess());
+ });
+ $propFind->handle('{DAV:}invite', function () use ($node) {
+ return new Property\Invite($node->getInvites());
+ });
+ $propFind->handle('{DAV:}share-resource-uri', function () use ($node) {
+ return new Property\Href($node->getShareResourceUri());
+ });
+ }
+ }
+
+ /**
+ * We intercept this to handle POST requests on shared resources.
+ *
+ * @return bool|null
+ */
+ public function httpPost(RequestInterface $request, ResponseInterface $response)
+ {
+ $path = $request->getPath();
+ $contentType = $request->getHeader('Content-Type');
+ if (null === $contentType) {
+ return;
+ }
+
+ // We're only interested in the davsharing content type.
+ if (false === strpos($contentType, 'application/davsharing+xml')) {
+ return;
+ }
+
+ $message = $this->server->xml->parse(
+ $request->getBody(),
+ $request->getUrl(),
+ $documentType
+ );
+
+ switch ($documentType) {
+ case '{DAV:}share-resource':
+ $this->shareResource($path, $message->sharees);
+ $response->setStatus(200);
+ // Adding this because sending a response body may cause issues,
+ // and I wanted some type of indicator the response was handled.
+ $response->setHeader('X-Sabre-Status', 'everything-went-well');
+
+ // Breaking the event chain
+ return false;
+
+ default:
+ throw new BadRequest('Unexpected document type: '.$documentType.' for this Content-Type');
+ }
+ }
+
+ /**
+ * This method is triggered whenever a subsystem requests the privileges
+ * hat are supported on a particular node.
+ *
+ * We need to add a number of privileges for scheduling purposes.
+ */
+ public function getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet)
+ {
+ if ($node instanceof ISharedNode) {
+ $supportedPrivilegeSet['{DAV:}share'] = [
+ 'abstract' => false,
+ 'aggregates' => [],
+ ];
+ }
+ }
+
+ /**
+ * Returns a bunch of meta-data about the plugin.
+ *
+ * Providing this information is optional, and is mainly displayed by the
+ * Browser plugin.
+ *
+ * The description key in the returned array may contain html and will not
+ * be sanitized.
+ *
+ * @return array
+ */
+ public function getPluginInfo()
+ {
+ return [
+ 'name' => $this->getPluginName(),
+ 'description' => 'This plugin implements WebDAV resource sharing',
+ 'link' => 'https://github.com/evert/webdav-sharing',
+ ];
+ }
+
+ /**
+ * This method is used to generate HTML output for the
+ * DAV\Browser\Plugin.
+ *
+ * @param string $output
+ * @param string $path
+ *
+ * @return bool|null
+ */
+ public function htmlActionsPanel(INode $node, &$output, $path)
+ {
+ if (!$node instanceof ISharedNode) {
+ return;
+ }
+
+ $aclPlugin = $this->server->getPlugin('acl');
+ if ($aclPlugin) {
+ if (!$aclPlugin->checkPrivileges($path, '{DAV:}share', \Sabre\DAVACL\Plugin::R_PARENT, false)) {
+ // Sharing is not permitted, we will not draw this interface.
+ return;
+ }
+ }
+
+ $output .= '
+
';
+ }
+
+ /**
+ * This method is triggered for POST actions generated by the browser
+ * plugin.
+ *
+ * @param string $path
+ * @param string $action
+ * @param array $postVars
+ */
+ public function browserPostAction($path, $action, $postVars)
+ {
+ if ('share' !== $action) {
+ return;
+ }
+
+ if (empty($postVars['href'])) {
+ throw new BadRequest('The "href" POST parameter is required');
+ }
+ if (empty($postVars['access'])) {
+ throw new BadRequest('The "access" POST parameter is required');
+ }
+
+ $accessMap = [
+ 'readwrite' => self::ACCESS_READWRITE,
+ 'read' => self::ACCESS_READ,
+ 'no-access' => self::ACCESS_NOACCESS,
+ ];
+
+ if (!isset($accessMap[$postVars['access']])) {
+ throw new BadRequest('The "access" POST must be readwrite, read or no-access');
+ }
+ $sharee = new Sharee([
+ 'href' => $postVars['href'],
+ 'access' => $accessMap[$postVars['access']],
+ ]);
+
+ $this->shareResource(
+ $path,
+ [$sharee]
+ );
+
+ return false;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/SimpleCollection.php b/lib/composer/vendor/sabre/dav/lib/DAV/SimpleCollection.php
new file mode 100644
index 0000000..3cd14d9
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/SimpleCollection.php
@@ -0,0 +1,109 @@
+name = $name;
+ foreach ($children as $key => $child) {
+ if (is_string($child)) {
+ $child = new SimpleFile($key, $child);
+ } elseif (is_array($child)) {
+ $child = new self($key, $child);
+ } elseif (!$child instanceof INode) {
+ throw new InvalidArgumentException('Children must be specified as strings, arrays or instances of Sabre\DAV\INode');
+ }
+ $this->addChild($child);
+ }
+ }
+
+ /**
+ * Adds a new childnode to this collection.
+ */
+ public function addChild(INode $child)
+ {
+ $this->children[$child->getName()] = $child;
+ }
+
+ /**
+ * Returns the name of the collection.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Returns a child object, by its name.
+ *
+ * This method makes use of the getChildren method to grab all the child nodes, and compares the name.
+ * Generally its wise to override this, as this can usually be optimized
+ *
+ * This method must throw Sabre\DAV\Exception\NotFound if the node does not
+ * exist.
+ *
+ * @param string $name
+ *
+ * @throws Exception\NotFound
+ *
+ * @return INode
+ */
+ public function getChild($name)
+ {
+ if (isset($this->children[$name])) {
+ return $this->children[$name];
+ }
+ throw new Exception\NotFound('File not found: '.$name.' in \''.$this->getName().'\'');
+ }
+
+ /**
+ * Returns a list of children for this collection.
+ *
+ * @return INode[]
+ */
+ public function getChildren()
+ {
+ return array_values($this->children);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/SimpleFile.php b/lib/composer/vendor/sabre/dav/lib/DAV/SimpleFile.php
new file mode 100644
index 0000000..ca808b6
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/SimpleFile.php
@@ -0,0 +1,118 @@
+name = $name;
+ $this->contents = $contents;
+ $this->mimeType = $mimeType;
+ }
+
+ /**
+ * Returns the node name for this file.
+ *
+ * This name is used to construct the url.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Returns the data.
+ *
+ * This method may either return a string or a readable stream resource
+ *
+ * @return mixed
+ */
+ public function get()
+ {
+ return $this->contents;
+ }
+
+ /**
+ * Returns the size of the file, in bytes.
+ *
+ * @return int
+ */
+ public function getSize()
+ {
+ return strlen($this->contents);
+ }
+
+ /**
+ * Returns the ETag for a file.
+ *
+ * An ETag is a unique identifier representing the current version of the file. If the file changes, the ETag MUST change.
+ * The ETag is an arbitrary string, but MUST be surrounded by double-quotes.
+ *
+ * Return null if the ETag can not effectively be determined
+ *
+ * @return string
+ */
+ public function getETag()
+ {
+ return '"'.sha1($this->contents).'"';
+ }
+
+ /**
+ * Returns the mime-type for a file.
+ *
+ * If null is returned, we'll assume application/octet-stream
+ *
+ * @return string
+ */
+ public function getContentType()
+ {
+ return $this->mimeType;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/StringUtil.php b/lib/composer/vendor/sabre/dav/lib/DAV/StringUtil.php
new file mode 100644
index 0000000..edfb7fa
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/StringUtil.php
@@ -0,0 +1,86 @@
+ 'The current synctoken',
+ * 'added' => [
+ * 'new.txt',
+ * ],
+ * 'modified' => [
+ * 'modified.txt',
+ * ],
+ * 'deleted' => array(
+ * 'foo.php.bak',
+ * 'old.txt'
+ * )
+ * ];
+ *
+ * The syncToken property should reflect the *current* syncToken of the
+ * collection, as reported getSyncToken(). This is needed here too, to
+ * ensure the operation is atomic.
+ *
+ * If the syncToken is specified as null, this is an initial sync, and all
+ * members should be reported.
+ *
+ * The modified property is an array of nodenames that have changed since
+ * the last token.
+ *
+ * The deleted property is an array with nodenames, that have been deleted
+ * from collection.
+ *
+ * The second argument is basically the 'depth' of the report. If it's 1,
+ * you only have to report changes that happened only directly in immediate
+ * descendants. If it's 2, it should also include changes from the nodes
+ * below the child collections. (grandchildren)
+ *
+ * The third (optional) argument allows a client to specify how many
+ * results should be returned at most. If the limit is not specified, it
+ * should be treated as infinite.
+ *
+ * If the limit (infinite or not) is higher than you're willing to return,
+ * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
+ *
+ * If the syncToken is expired (due to data cleanup) or unknown, you must
+ * return null.
+ *
+ * The limit is 'suggestive'. You are free to ignore it.
+ *
+ * @param string $syncToken
+ * @param int $syncLevel
+ * @param int $limit
+ *
+ * @return array|null
+ */
+ public function getChanges($syncToken, $syncLevel, $limit = null);
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Sync/Plugin.php b/lib/composer/vendor/sabre/dav/lib/DAV/Sync/Plugin.php
new file mode 100644
index 0000000..8609f75
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Sync/Plugin.php
@@ -0,0 +1,249 @@
+server = $server;
+ $server->xml->elementMap['{DAV:}sync-collection'] = 'Sabre\\DAV\\Xml\\Request\\SyncCollectionReport';
+
+ $self = $this;
+
+ $server->on('report', function ($reportName, $dom, $uri) use ($self) {
+ if ('{DAV:}sync-collection' === $reportName) {
+ $this->server->transactionType = 'report-sync-collection';
+ $self->syncCollection($uri, $dom);
+
+ return false;
+ }
+ });
+
+ $server->on('propFind', [$this, 'propFind']);
+ $server->on('validateTokens', [$this, 'validateTokens']);
+ }
+
+ /**
+ * Returns a list of reports this plugin supports.
+ *
+ * This will be used in the {DAV:}supported-report-set property.
+ * Note that you still need to subscribe to the 'report' event to actually
+ * implement them
+ *
+ * @param string $uri
+ *
+ * @return array
+ */
+ public function getSupportedReportSet($uri)
+ {
+ $node = $this->server->tree->getNodeForPath($uri);
+ if ($node instanceof ISyncCollection && $node->getSyncToken()) {
+ return [
+ '{DAV:}sync-collection',
+ ];
+ }
+
+ return [];
+ }
+
+ /**
+ * This method handles the {DAV:}sync-collection HTTP REPORT.
+ *
+ * @param string $uri
+ */
+ public function syncCollection($uri, SyncCollectionReport $report)
+ {
+ // Getting the data
+ $node = $this->server->tree->getNodeForPath($uri);
+ if (!$node instanceof ISyncCollection) {
+ throw new DAV\Exception\ReportNotSupported('The {DAV:}sync-collection REPORT is not supported on this url.');
+ }
+ $token = $node->getSyncToken();
+ if (!$token) {
+ throw new DAV\Exception\ReportNotSupported('No sync information is available at this node');
+ }
+
+ $syncToken = $report->syncToken;
+ if (!is_null($syncToken)) {
+ // Sync-token must start with our prefix
+ if (self::SYNCTOKEN_PREFIX !== substr($syncToken, 0, strlen(self::SYNCTOKEN_PREFIX))) {
+ throw new DAV\Exception\InvalidSyncToken('Invalid or unknown sync token');
+ }
+
+ $syncToken = substr($syncToken, strlen(self::SYNCTOKEN_PREFIX));
+ }
+ $changeInfo = $node->getChanges($syncToken, $report->syncLevel, $report->limit);
+
+ if (is_null($changeInfo)) {
+ throw new DAV\Exception\InvalidSyncToken('Invalid or unknown sync token');
+ }
+
+ if (!array_key_exists('result_truncated', $changeInfo)) {
+ $changeInfo['result_truncated'] = false;
+ }
+
+ // Encoding the response
+ $this->sendSyncCollectionResponse(
+ $changeInfo['syncToken'],
+ $uri,
+ $changeInfo['added'],
+ $changeInfo['modified'],
+ $changeInfo['deleted'],
+ $report->properties,
+ $changeInfo['result_truncated']
+ );
+ }
+
+ /**
+ * Sends the response to a sync-collection request.
+ *
+ * @param string $syncToken
+ * @param string $collectionUrl
+ */
+ protected function sendSyncCollectionResponse($syncToken, $collectionUrl, array $added, array $modified, array $deleted, array $properties, bool $resultTruncated = false)
+ {
+ $fullPaths = [];
+
+ // Pre-fetching children, if this is possible.
+ foreach (array_merge($added, $modified) as $item) {
+ $fullPath = $collectionUrl.'/'.$item;
+ $fullPaths[] = $fullPath;
+ }
+
+ $responses = [];
+ foreach ($this->server->getPropertiesForMultiplePaths($fullPaths, $properties) as $fullPath => $props) {
+ // The 'Property_Response' class is responsible for generating a
+ // single {DAV:}response xml element.
+ $responses[] = new DAV\Xml\Element\Response($fullPath, $props);
+ }
+
+ // Deleted items also show up as 'responses'. They have no properties,
+ // and a single {DAV:}status element set as 'HTTP/1.1 404 Not Found'.
+ foreach ($deleted as $item) {
+ $fullPath = $collectionUrl.'/'.$item;
+ $responses[] = new DAV\Xml\Element\Response($fullPath, [], 404);
+ }
+ if ($resultTruncated) {
+ $responses[] = new DAV\Xml\Element\Response($collectionUrl.'/', [], 507);
+ }
+
+ $multiStatus = new DAV\Xml\Response\MultiStatus($responses, self::SYNCTOKEN_PREFIX.$syncToken);
+
+ $this->server->httpResponse->setStatus(207);
+ $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
+ $this->server->httpResponse->setBody(
+ $this->server->xml->write('{DAV:}multistatus', $multiStatus, $this->server->getBaseUri())
+ );
+ }
+
+ /**
+ * This method is triggered whenever properties are requested for a node.
+ * We intercept this to see if we must return a {DAV:}sync-token.
+ */
+ public function propFind(DAV\PropFind $propFind, DAV\INode $node)
+ {
+ $propFind->handle('{DAV:}sync-token', function () use ($node) {
+ if (!$node instanceof ISyncCollection || !$token = $node->getSyncToken()) {
+ return;
+ }
+
+ return self::SYNCTOKEN_PREFIX.$token;
+ });
+ }
+
+ /**
+ * The validateTokens event is triggered before every request.
+ *
+ * It's a moment where this plugin can check all the supplied lock tokens
+ * in the If: header, and check if they are valid.
+ *
+ * @param array $conditions
+ */
+ public function validateTokens(RequestInterface $request, &$conditions)
+ {
+ foreach ($conditions as $kk => $condition) {
+ foreach ($condition['tokens'] as $ii => $token) {
+ // Sync-tokens must always start with our designated prefix.
+ if (self::SYNCTOKEN_PREFIX !== substr($token['token'], 0, strlen(self::SYNCTOKEN_PREFIX))) {
+ continue;
+ }
+
+ // Checking if the token is a match.
+ $node = $this->server->tree->getNodeForPath($condition['uri']);
+
+ if (
+ $node instanceof ISyncCollection &&
+ $node->getSyncToken() == substr($token['token'], strlen(self::SYNCTOKEN_PREFIX))
+ ) {
+ $conditions[$kk]['tokens'][$ii]['validToken'] = true;
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns a bunch of meta-data about the plugin.
+ *
+ * Providing this information is optional, and is mainly displayed by the
+ * Browser plugin.
+ *
+ * The description key in the returned array may contain html and will not
+ * be sanitized.
+ *
+ * @return array
+ */
+ public function getPluginInfo()
+ {
+ return [
+ 'name' => $this->getPluginName(),
+ 'description' => 'Adds support for WebDAV Collection Sync (rfc6578)',
+ 'link' => 'http://sabre.io/dav/sync/',
+ ];
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/TemporaryFileFilterPlugin.php b/lib/composer/vendor/sabre/dav/lib/DAV/TemporaryFileFilterPlugin.php
new file mode 100644
index 0000000..9f8ec5b
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/TemporaryFileFilterPlugin.php
@@ -0,0 +1,298 @@
+dataDir = $dataDir;
+ }
+
+ /**
+ * Initialize the plugin.
+ *
+ * This is called automatically be the Server class after this plugin is
+ * added with Sabre\DAV\Server::addPlugin()
+ */
+ public function initialize(Server $server)
+ {
+ $this->server = $server;
+ $server->on('beforeMethod:*', [$this, 'beforeMethod']);
+ $server->on('beforeCreateFile', [$this, 'beforeCreateFile']);
+ }
+
+ /**
+ * This method is called before any HTTP method handler.
+ *
+ * This method intercepts any GET, DELETE, PUT and PROPFIND calls to
+ * filenames that are known to match the 'temporary file' regex.
+ *
+ * @return bool
+ */
+ public function beforeMethod(RequestInterface $request, ResponseInterface $response)
+ {
+ if (!$tempLocation = $this->isTempFile($request->getPath())) {
+ return;
+ }
+
+ switch ($request->getMethod()) {
+ case 'GET':
+ return $this->httpGet($request, $response, $tempLocation);
+ case 'PUT':
+ return $this->httpPut($request, $response, $tempLocation);
+ case 'PROPFIND':
+ return $this->httpPropfind($request, $response, $tempLocation);
+ case 'DELETE':
+ return $this->httpDelete($request, $response, $tempLocation);
+ }
+
+ return;
+ }
+
+ /**
+ * This method is invoked if some subsystem creates a new file.
+ *
+ * This is used to deal with HTTP LOCK requests which create a new
+ * file.
+ *
+ * @param string $uri
+ * @param resource $data
+ * @param bool $modified should be set to true, if this event handler
+ * changed &$data
+ *
+ * @return bool
+ */
+ public function beforeCreateFile($uri, $data, ICollection $parent, $modified)
+ {
+ if ($tempPath = $this->isTempFile($uri)) {
+ $hR = $this->server->httpResponse;
+ $hR->setHeader('X-Sabre-Temp', 'true');
+ file_put_contents($tempPath, $data);
+
+ return false;
+ }
+
+ return;
+ }
+
+ /**
+ * This method will check if the url matches the temporary file pattern
+ * if it does, it will return an path based on $this->dataDir for the
+ * temporary file storage.
+ *
+ * @param string $path
+ *
+ * @return bool|string
+ */
+ protected function isTempFile($path)
+ {
+ // We're only interested in the basename.
+ list(, $tempPath) = Uri\split($path);
+
+ if (null === $tempPath) {
+ return false;
+ }
+
+ foreach ($this->temporaryFilePatterns as $tempFile) {
+ if (preg_match($tempFile, $tempPath)) {
+ return $this->getDataDir().'/sabredav_'.md5($path).'.tempfile';
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * This method handles the GET method for temporary files.
+ * If the file doesn't exist, it will return false which will kick in
+ * the regular system for the GET method.
+ *
+ * @param string $tempLocation
+ *
+ * @return bool
+ */
+ public function httpGet(RequestInterface $request, ResponseInterface $hR, $tempLocation)
+ {
+ if (!file_exists($tempLocation)) {
+ return;
+ }
+
+ $hR->setHeader('Content-Type', 'application/octet-stream');
+ $hR->setHeader('Content-Length', filesize($tempLocation));
+ $hR->setHeader('X-Sabre-Temp', 'true');
+ $hR->setStatus(200);
+ $hR->setBody(fopen($tempLocation, 'r'));
+
+ return false;
+ }
+
+ /**
+ * This method handles the PUT method.
+ *
+ * @param string $tempLocation
+ *
+ * @return bool
+ */
+ public function httpPut(RequestInterface $request, ResponseInterface $hR, $tempLocation)
+ {
+ $hR->setHeader('X-Sabre-Temp', 'true');
+
+ $newFile = !file_exists($tempLocation);
+
+ if (!$newFile && ($this->server->httpRequest->getHeader('If-None-Match'))) {
+ throw new Exception\PreconditionFailed('The resource already exists, and an If-None-Match header was supplied');
+ }
+
+ file_put_contents($tempLocation, $this->server->httpRequest->getBody());
+ $hR->setStatus($newFile ? 201 : 200);
+
+ return false;
+ }
+
+ /**
+ * This method handles the DELETE method.
+ *
+ * If the file didn't exist, it will return false, which will make the
+ * standard HTTP DELETE handler kick in.
+ *
+ * @param string $tempLocation
+ *
+ * @return bool
+ */
+ public function httpDelete(RequestInterface $request, ResponseInterface $hR, $tempLocation)
+ {
+ if (!file_exists($tempLocation)) {
+ return;
+ }
+
+ unlink($tempLocation);
+ $hR->setHeader('X-Sabre-Temp', 'true');
+ $hR->setStatus(204);
+
+ return false;
+ }
+
+ /**
+ * This method handles the PROPFIND method.
+ *
+ * It's a very lazy method, it won't bother checking the request body
+ * for which properties were requested, and just sends back a default
+ * set of properties.
+ *
+ * @param string $tempLocation
+ *
+ * @return bool
+ */
+ public function httpPropfind(RequestInterface $request, ResponseInterface $hR, $tempLocation)
+ {
+ if (!file_exists($tempLocation)) {
+ return;
+ }
+
+ $hR->setHeader('X-Sabre-Temp', 'true');
+ $hR->setStatus(207);
+ $hR->setHeader('Content-Type', 'application/xml; charset=utf-8');
+
+ $properties = [
+ 'href' => $request->getPath(),
+ 200 => [
+ '{DAV:}getlastmodified' => new Xml\Property\GetLastModified(filemtime($tempLocation)),
+ '{DAV:}getcontentlength' => filesize($tempLocation),
+ '{DAV:}resourcetype' => new Xml\Property\ResourceType(null),
+ '{'.Server::NS_SABREDAV.'}tempFile' => true,
+ ],
+ ];
+
+ $data = $this->server->generateMultiStatus([$properties]);
+ $hR->setBody($data);
+
+ return false;
+ }
+
+ /**
+ * This method returns the directory where the temporary files should be stored.
+ *
+ * @return string
+ */
+ protected function getDataDir()
+ {
+ return $this->dataDir;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Tree.php b/lib/composer/vendor/sabre/dav/lib/DAV/Tree.php
new file mode 100644
index 0000000..1483e1b
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Tree.php
@@ -0,0 +1,342 @@
+rootNode = $rootNode;
+ }
+
+ /**
+ * Returns the INode object for the requested path.
+ *
+ * @param string $path
+ *
+ * @return INode
+ */
+ public function getNodeForPath($path)
+ {
+ $path = trim($path, '/');
+ if (isset($this->cache[$path])) {
+ return $this->cache[$path];
+ }
+
+ // Is it the root node?
+ if (!strlen($path)) {
+ return $this->rootNode;
+ }
+
+ $node = $this->rootNode;
+
+ // look for any cached parent and collect the parts below the parent
+ $parts = [];
+ $remainingPath = $path;
+ do {
+ list($remainingPath, $baseName) = Uri\split($remainingPath);
+ array_unshift($parts, $baseName);
+
+ if (isset($this->cache[$remainingPath])) {
+ $node = $this->cache[$remainingPath];
+ break;
+ }
+ } while ('' !== $remainingPath);
+
+ while (count($parts)) {
+ if (!($node instanceof ICollection)) {
+ throw new Exception\NotFound('Could not find node at path: '.$path);
+ }
+
+ if ($node instanceof INodeByPath) {
+ $targetNode = $node->getNodeForPath(implode('/', $parts));
+ if ($targetNode instanceof Node) {
+ $node = $targetNode;
+ break;
+ }
+ }
+
+ $part = array_shift($parts);
+ if ('' !== $part) {
+ $node = $node->getChild($part);
+ }
+ }
+
+ $this->cache[$path] = $node;
+
+ return $node;
+ }
+
+ /**
+ * This function allows you to check if a node exists.
+ *
+ * Implementors of this class should override this method to make
+ * it cheaper.
+ *
+ * @param string $path
+ *
+ * @return bool
+ */
+ public function nodeExists($path)
+ {
+ try {
+ // The root always exists
+ if ('' === $path) {
+ return true;
+ }
+
+ list($parent, $base) = Uri\split($path);
+
+ $parentNode = $this->getNodeForPath($parent);
+ if (!$parentNode instanceof ICollection) {
+ return false;
+ }
+
+ return $parentNode->childExists($base);
+ } catch (Exception\NotFound $e) {
+ return false;
+ }
+ }
+
+ /**
+ * Copies a file from path to another.
+ *
+ * @param string $sourcePath The source location
+ * @param string $destinationPath The full destination path
+ */
+ public function copy($sourcePath, $destinationPath)
+ {
+ $sourceNode = $this->getNodeForPath($sourcePath);
+
+ // grab the dirname and basename components
+ list($destinationDir, $destinationName) = Uri\split($destinationPath);
+
+ $destinationParent = $this->getNodeForPath($destinationDir);
+ // Check if the target can handle the copy itself. If not, we do it ourselves.
+ if (!$destinationParent instanceof ICopyTarget || !$destinationParent->copyInto($destinationName, $sourcePath, $sourceNode)) {
+ $this->copyNode($sourceNode, $destinationParent, $destinationName);
+ }
+
+ $this->markDirty($destinationDir);
+ }
+
+ /**
+ * Moves a file from one location to another.
+ *
+ * @param string $sourcePath The path to the file which should be moved
+ * @param string $destinationPath The full destination path, so not just the destination parent node
+ */
+ public function move($sourcePath, $destinationPath)
+ {
+ list($sourceDir) = Uri\split($sourcePath);
+ list($destinationDir, $destinationName) = Uri\split($destinationPath);
+
+ if ($sourceDir === $destinationDir) {
+ // If this is a 'local' rename, it means we can just trigger a rename.
+ $sourceNode = $this->getNodeForPath($sourcePath);
+ $sourceNode->setName($destinationName);
+ } else {
+ $newParentNode = $this->getNodeForPath($destinationDir);
+ $moveSuccess = false;
+ if ($newParentNode instanceof IMoveTarget) {
+ // The target collection may be able to handle the move
+ $sourceNode = $this->getNodeForPath($sourcePath);
+ $moveSuccess = $newParentNode->moveInto($destinationName, $sourcePath, $sourceNode);
+ }
+ if (!$moveSuccess) {
+ $this->copy($sourcePath, $destinationPath);
+ $this->getNodeForPath($sourcePath)->delete();
+ }
+ }
+ $this->markDirty($sourceDir);
+ $this->markDirty($destinationDir);
+ }
+
+ /**
+ * Deletes a node from the tree.
+ *
+ * @param string $path
+ */
+ public function delete($path)
+ {
+ $node = $this->getNodeForPath($path);
+ $node->delete();
+
+ list($parent) = Uri\split($path);
+ $this->markDirty($parent);
+ }
+
+ /**
+ * Returns a list of childnodes for a given path.
+ *
+ * @param string $path
+ *
+ * @return \Traversable
+ */
+ public function getChildren($path)
+ {
+ $node = $this->getNodeForPath($path);
+ $basePath = trim($path, '/');
+ if ('' !== $basePath) {
+ $basePath .= '/';
+ }
+
+ foreach ($node->getChildren() as $child) {
+ $this->cache[$basePath.$child->getName()] = $child;
+ yield $child;
+ }
+ }
+
+ /**
+ * This method is called with every tree update.
+ *
+ * Examples of tree updates are:
+ * * node deletions
+ * * node creations
+ * * copy
+ * * move
+ * * renaming nodes
+ *
+ * If Tree classes implement a form of caching, this will allow
+ * them to make sure caches will be expired.
+ *
+ * If a path is passed, it is assumed that the entire subtree is dirty
+ *
+ * @param string $path
+ */
+ public function markDirty($path)
+ {
+ // We don't care enough about sub-paths
+ // flushing the entire cache
+ $path = trim($path, '/');
+ foreach ($this->cache as $nodePath => $node) {
+ if ('' === $path || $nodePath == $path || 0 === strpos((string) $nodePath, $path.'/')) {
+ unset($this->cache[$nodePath]);
+ }
+ }
+ }
+
+ /**
+ * This method tells the tree system to pre-fetch and cache a list of
+ * children of a single parent.
+ *
+ * There are a bunch of operations in the WebDAV stack that request many
+ * children (based on uris), and sometimes fetching many at once can
+ * optimize this.
+ *
+ * This method returns an array with the found nodes. It's keys are the
+ * original paths. The result may be out of order.
+ *
+ * @param array $paths list of nodes that must be fetched
+ *
+ * @return array
+ */
+ public function getMultipleNodes($paths)
+ {
+ // Finding common parents
+ $parents = [];
+ foreach ($paths as $path) {
+ list($parent, $node) = Uri\split($path);
+ if (!isset($parents[$parent])) {
+ $parents[$parent] = [$node];
+ } else {
+ $parents[$parent][] = $node;
+ }
+ }
+
+ $result = [];
+
+ foreach ($parents as $parent => $children) {
+ $parentNode = $this->getNodeForPath($parent);
+ if ($parentNode instanceof IMultiGet) {
+ foreach ($parentNode->getMultipleChildren($children) as $childNode) {
+ $fullPath = $parent.'/'.$childNode->getName();
+ $result[$fullPath] = $childNode;
+ $this->cache[$fullPath] = $childNode;
+ }
+ } else {
+ foreach ($children as $child) {
+ $fullPath = $parent.'/'.$child;
+ $result[$fullPath] = $this->getNodeForPath($fullPath);
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * copyNode.
+ *
+ * @param string $destinationName
+ */
+ protected function copyNode(INode $source, ICollection $destinationParent, $destinationName = null)
+ {
+ if ('' === (string) $destinationName) {
+ $destinationName = $source->getName();
+ }
+
+ $destination = null;
+
+ if ($source instanceof IFile) {
+ $data = $source->get();
+
+ // If the body was a string, we need to convert it to a stream
+ if (is_string($data)) {
+ $stream = fopen('php://temp', 'r+');
+ fwrite($stream, $data);
+ rewind($stream);
+ $data = $stream;
+ }
+ $destinationParent->createFile($destinationName, $data);
+ $destination = $destinationParent->getChild($destinationName);
+ } elseif ($source instanceof ICollection) {
+ $destinationParent->createDirectory($destinationName);
+
+ $destination = $destinationParent->getChild($destinationName);
+ foreach ($source->getChildren() as $child) {
+ $this->copyNode($child, $destination);
+ }
+ }
+ if ($source instanceof IProperties && $destination instanceof IProperties) {
+ $props = $source->getProperties([]);
+ $propPatch = new PropPatch($props);
+ $destination->propPatch($propPatch);
+ $propPatch->commit();
+ }
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/UUIDUtil.php b/lib/composer/vendor/sabre/dav/lib/DAV/UUIDUtil.php
new file mode 100644
index 0000000..8c36e1b
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/UUIDUtil.php
@@ -0,0 +1,66 @@
+value array.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Prop implements XmlDeserializable
+{
+ /**
+ * 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.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ // If there's no children, we don't do anything.
+ if ($reader->isEmptyElement) {
+ $reader->next();
+
+ return [];
+ }
+
+ $values = [];
+
+ $reader->read();
+ do {
+ if (Reader::ELEMENT === $reader->nodeType) {
+ $clark = $reader->getClark();
+ $values[$clark] = self::parseCurrentElement($reader)['value'];
+ } else {
+ $reader->read();
+ }
+ } while (Reader::END_ELEMENT !== $reader->nodeType);
+
+ $reader->read();
+
+ return $values;
+ }
+
+ /**
+ * This function behaves similar to Sabre\Xml\Reader::parseCurrentElement,
+ * but instead of creating deep xml array structures, it will turn any
+ * top-level element it doesn't recognize into either a string, or an
+ * XmlFragment class.
+ *
+ * This method returns arn array with 2 properties:
+ * * name - A clark-notation XML element name.
+ * * value - The parsed value.
+ *
+ * @return array
+ */
+ private static function parseCurrentElement(Reader $reader)
+ {
+ $name = $reader->getClark();
+
+ if (array_key_exists($name, $reader->elementMap)) {
+ $deserializer = $reader->elementMap[$name];
+ if (is_subclass_of($deserializer, 'Sabre\\Xml\\XmlDeserializable')) {
+ $value = call_user_func([$deserializer, 'xmlDeserialize'], $reader);
+ } elseif (is_callable($deserializer)) {
+ $value = call_user_func($deserializer, $reader);
+ } else {
+ $type = gettype($deserializer);
+ if ('string' === $type) {
+ $type .= ' ('.$deserializer.')';
+ } elseif ('object' === $type) {
+ $type .= ' ('.get_class($deserializer).')';
+ }
+ throw new \LogicException('Could not use this type as a deserializer: '.$type);
+ }
+ } else {
+ $value = Complex::xmlDeserialize($reader);
+ }
+
+ return [
+ 'name' => $name,
+ 'value' => $value,
+ ];
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Element/Response.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Element/Response.php
new file mode 100644
index 0000000..df92914
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Element/Response.php
@@ -0,0 +1,267 @@
+href = $href;
+ $this->responseProperties = $responseProperties;
+ $this->httpStatus = $httpStatus;
+ }
+
+ /**
+ * Returns the url.
+ *
+ * @return string
+ */
+ public function getHref()
+ {
+ return $this->href;
+ }
+
+ /**
+ * Returns the httpStatus value.
+ *
+ * @return string
+ */
+ public function getHttpStatus()
+ {
+ return $this->httpStatus;
+ }
+
+ /**
+ * Returns the property list.
+ *
+ * @return array
+ */
+ public function getResponseProperties()
+ {
+ return $this->responseProperties;
+ }
+
+ /**
+ * The serialize method is called during xml writing.
+ *
+ * It should use the $writer argument to encode this object into XML.
+ *
+ * Important note: it is not needed to create the parent element. The
+ * parent element is already created, and we only have to worry about
+ * attributes, child elements and text (if any).
+ *
+ * Important note 2: If you are writing any new elements, you are also
+ * responsible for closing them.
+ */
+ public function xmlSerialize(Writer $writer)
+ {
+ /*
+ * Accordingly to the RFC the element looks like:
+ *
+ *
+ * So the response
+ * - MUST contain a href and
+ * - EITHER a status and additional href(s)
+ * OR one or more propstat(s)
+ */
+ $writer->writeElement('{DAV:}href', $writer->contextUri.\Sabre\HTTP\encodePath($this->getHref()));
+
+ $empty = true;
+ $httpStatus = $this->getHTTPStatus();
+
+ // Add propstat elements
+ foreach ($this->getResponseProperties() as $status => $properties) {
+ // Skipping empty lists
+ if (!$properties || (!is_int($status) && !ctype_digit($status))) {
+ continue;
+ }
+ $empty = false;
+ $writer->startElement('{DAV:}propstat');
+ $writer->writeElement('{DAV:}prop', $properties);
+ $writer->writeElement('{DAV:}status', 'HTTP/1.1 '.$status.' '.\Sabre\HTTP\Response::$statusCodes[$status]);
+ $writer->endElement(); // {DAV:}propstat
+ }
+
+ // The WebDAV spec only allows the status element on responses _without_ a propstat
+ if ($empty) {
+ if (null !== $httpStatus) {
+ $writer->writeElement('{DAV:}status', 'HTTP/1.1 '.$httpStatus.' '.\Sabre\HTTP\Response::$statusCodes[$httpStatus]);
+ } else {
+ /*
+ * The WebDAV spec _requires_ at least one DAV:propstat to appear for
+ * every DAV:response if there is no status.
+ * In some circumstances however, there are no properties to encode.
+ *
+ * In those cases we MUST specify at least one DAV:propstat anyway, with
+ * no properties.
+ */
+ $writer->writeElement('{DAV:}propstat', [
+ '{DAV:}prop' => [],
+ '{DAV:}status' => 'HTTP/1.1 418 '.\Sabre\HTTP\Response::$statusCodes[418],
+ ]);
+ }
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $reader->pushContext();
+
+ $reader->elementMap['{DAV:}propstat'] = 'Sabre\\Xml\\Element\\KeyValue';
+
+ // We are overriding the parser for {DAV:}prop. This deserializer is
+ // almost identical to the one for Sabre\Xml\Element\KeyValue.
+ //
+ // The difference is that if there are any child-elements inside of
+ // {DAV:}prop, that have no value, normally any deserializers are
+ // called. But we don't want this, because a singular element without
+ // child-elements implies 'no value' in {DAV:}prop, so we want to skip
+ // deserializers and just set null for those.
+ $reader->elementMap['{DAV:}prop'] = function (Reader $reader) {
+ 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) {
+ $clark = $reader->getClark();
+
+ if ($reader->isEmptyElement) {
+ $values[$clark] = null;
+ $reader->next();
+ } else {
+ $values[$clark] = $reader->parseCurrentElement()['value'];
+ }
+ } else {
+ if (!$reader->read()) {
+ break;
+ }
+ }
+ } while (Reader::END_ELEMENT !== $reader->nodeType);
+
+ $reader->read();
+
+ return $values;
+ };
+ $elems = $reader->parseInnerTree();
+ $reader->popContext();
+
+ $href = null;
+ $propertyLists = [];
+ $statusCode = null;
+
+ foreach ($elems as $elem) {
+ switch ($elem['name']) {
+ case '{DAV:}href':
+ $href = $elem['value'];
+ break;
+ case '{DAV:}propstat':
+ $status = $elem['value']['{DAV:}status'];
+ list(, $status) = explode(' ', $status, 3);
+ $properties = isset($elem['value']['{DAV:}prop']) ? $elem['value']['{DAV:}prop'] : [];
+ if ($properties) {
+ $propertyLists[$status] = $properties;
+ }
+ break;
+ case '{DAV:}status':
+ list(, $statusCode) = explode(' ', $elem['value'], 3);
+ break;
+ }
+ }
+
+ return new self($href, $propertyLists, $statusCode);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Element/Sharee.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Element/Sharee.php
new file mode 100644
index 0000000..33564d8
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Element/Sharee.php
@@ -0,0 +1,189 @@
+ $v) {
+ if (property_exists($this, $k)) {
+ $this->$k = $v;
+ } else {
+ throw new \InvalidArgumentException('Unknown property: '.$k);
+ }
+ }
+ }
+
+ /**
+ * 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)
+ {
+ $writer->write([
+ new Href($this->href),
+ '{DAV:}prop' => $this->properties,
+ '{DAV:}share-access' => new ShareAccess($this->access),
+ ]);
+ switch ($this->inviteStatus) {
+ case Plugin::INVITE_NORESPONSE:
+ $writer->writeElement('{DAV:}invite-noresponse');
+ break;
+ case Plugin::INVITE_ACCEPTED:
+ $writer->writeElement('{DAV:}invite-accepted');
+ break;
+ case Plugin::INVITE_DECLINED:
+ $writer->writeElement('{DAV:}invite-declined');
+ break;
+ case Plugin::INVITE_INVALID:
+ $writer->writeElement('{DAV:}invite-invalid');
+ 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.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ // Temporarily override configuration
+ $reader->pushContext();
+ $reader->elementMap['{DAV:}share-access'] = 'Sabre\DAV\Xml\Property\ShareAccess';
+ $reader->elementMap['{DAV:}prop'] = 'Sabre\Xml\Deserializer\keyValue';
+
+ $elems = Deserializer\keyValue($reader, 'DAV:');
+
+ // Restore previous configuration
+ $reader->popContext();
+
+ $sharee = new self();
+ if (!isset($elems['href'])) {
+ throw new BadRequest('Every {DAV:}sharee must have a {DAV:}href child-element');
+ }
+ $sharee->href = $elems['href'];
+
+ if (isset($elems['prop'])) {
+ $sharee->properties = $elems['prop'];
+ }
+ if (isset($elems['comment'])) {
+ $sharee->comment = $elems['comment'];
+ }
+ if (!isset($elems['share-access'])) {
+ throw new BadRequest('Every {DAV:}sharee must have a {DAV:}share-access child element');
+ }
+ $sharee->access = $elems['share-access']->getValue();
+
+ return $sharee;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/Complex.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/Complex.php
new file mode 100644
index 0000000..787d30d
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/Complex.php
@@ -0,0 +1,87 @@
+next();
+ *
+ * $reader->parseInnerTree() will parse the entire sub-tree, and advance to
+ * the next element.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $xml = $reader->readInnerXml();
+
+ if (Reader::ELEMENT === $reader->nodeType && $reader->isEmptyElement) {
+ // Easy!
+ $reader->next();
+
+ return null;
+ }
+ // Now we have a copy of the inner xml, we need to traverse it to get
+ // all the strings. If there's no non-string data, we just return the
+ // string, otherwise we return an instance of this class.
+ $reader->read();
+
+ $nonText = false;
+ $text = '';
+
+ while (true) {
+ switch ($reader->nodeType) {
+ case Reader::ELEMENT:
+ $nonText = true;
+ $reader->next();
+ continue 2;
+ case Reader::TEXT:
+ case Reader::CDATA:
+ $text .= $reader->value;
+ break;
+ case Reader::END_ELEMENT:
+ break 2;
+ }
+ $reader->read();
+ }
+
+ // Make sure we advance the cursor one step further.
+ $reader->read();
+
+ if ($nonText) {
+ $new = new self($xml);
+
+ return $new;
+ } else {
+ return $text;
+ }
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/GetLastModified.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/GetLastModified.php
new file mode 100644
index 0000000..efc15c2
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/GetLastModified.php
@@ -0,0 +1,103 @@
+time = clone $time;
+ } else {
+ $this->time = new DateTime('@'.$time);
+ }
+
+ // Setting timezone to UTC
+ $this->time->setTimezone(new DateTimeZone('UTC'));
+ }
+
+ /**
+ * getTime.
+ *
+ * @return DateTime
+ */
+ public function getTime()
+ {
+ return $this->time;
+ }
+
+ /**
+ * The serialize method is called during xml writing.
+ *
+ * It should use the $writer argument to encode this object into XML.
+ *
+ * Important note: it is not needed to create the parent element. The
+ * parent element is already created, and we only have to worry about
+ * attributes, child elements and text (if any).
+ *
+ * Important note 2: If you are writing any new elements, you are also
+ * responsible for closing them.
+ */
+ public function xmlSerialize(Writer $writer)
+ {
+ $writer->write(
+ HTTP\toDate($this->time)
+ );
+ }
+
+ /**
+ * 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 mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ return new self(new DateTime($reader->parseInnerTree()));
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/Href.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/Href.php
new file mode 100644
index 0000000..d4e43da
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/Href.php
@@ -0,0 +1,166 @@
+hrefs = $hrefs;
+ }
+
+ /**
+ * Returns the first Href.
+ *
+ * @return string|null
+ */
+ public function getHref()
+ {
+ return $this->hrefs[0] ?? null;
+ }
+
+ /**
+ * Returns the hrefs as an array.
+ *
+ * @return array
+ */
+ public function getHrefs()
+ {
+ return $this->hrefs;
+ }
+
+ /**
+ * 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)
+ {
+ foreach ($this->getHrefs() as $href) {
+ $href = Uri\resolve($writer->contextUri, $href);
+ $writer->writeElement('{DAV:}href', $href);
+ }
+ }
+
+ /**
+ * Generate html representation for this value.
+ *
+ * The html output is 100% trusted, and no effort is being made to sanitize
+ * it. It's up to the implementor to sanitize user provided values.
+ *
+ * The output must be in UTF-8.
+ *
+ * The baseUri parameter is a url to the root of the application, and can
+ * be used to construct local links.
+ *
+ * @return string
+ */
+ public function toHtml(HtmlOutputHelper $html)
+ {
+ $links = [];
+ foreach ($this->getHrefs() as $href) {
+ $links[] = $html->link($href);
+ }
+
+ return implode(' ', $links);
+ }
+
+ /**
+ * 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.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $hrefs = [];
+ foreach ((array) $reader->parseInnerTree() as $elem) {
+ if ('{DAV:}href' !== $elem['name']) {
+ continue;
+ }
+
+ $hrefs[] = $elem['value'];
+ }
+ if ($hrefs) {
+ return new self($hrefs);
+ }
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/Invite.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/Invite.php
new file mode 100644
index 0000000..e3f0a61
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/Invite.php
@@ -0,0 +1,66 @@
+sharees = $sharees;
+ }
+
+ /**
+ * 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)
+ {
+ foreach ($this->sharees as $sharee) {
+ $writer->writeElement('{DAV:}sharee', $sharee);
+ }
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/LocalHref.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/LocalHref.php
new file mode 100644
index 0000000..cb79497
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/LocalHref.php
@@ -0,0 +1,48 @@
+locks = $locks;
+ }
+
+ /**
+ * The serialize method is called during xml writing.
+ *
+ * It should use the $writer argument to encode this object into XML.
+ *
+ * Important note: it is not needed to create the parent element. The
+ * parent element is already created, and we only have to worry about
+ * attributes, child elements and text (if any).
+ *
+ * Important note 2: If you are writing any new elements, you are also
+ * responsible for closing them.
+ */
+ public function xmlSerialize(Writer $writer)
+ {
+ foreach ($this->locks as $lock) {
+ $writer->startElement('{DAV:}activelock');
+
+ $writer->startElement('{DAV:}lockscope');
+ if (LockInfo::SHARED === $lock->scope) {
+ $writer->writeElement('{DAV:}shared');
+ } else {
+ $writer->writeElement('{DAV:}exclusive');
+ }
+
+ $writer->endElement(); // {DAV:}lockscope
+
+ $writer->startElement('{DAV:}locktype');
+ $writer->writeElement('{DAV:}write');
+ $writer->endElement(); // {DAV:}locktype
+
+ if (!self::$hideLockRoot) {
+ $writer->startElement('{DAV:}lockroot');
+ $writer->writeElement('{DAV:}href', $writer->contextUri.$lock->uri);
+ $writer->endElement(); // {DAV:}lockroot
+ }
+ $writer->writeElement('{DAV:}depth', (DAV\Server::DEPTH_INFINITY == $lock->depth ? 'infinity' : $lock->depth));
+ $writer->writeElement('{DAV:}timeout', (LockInfo::TIMEOUT_INFINITE === $lock->timeout ? 'Infinite' : 'Second-'.$lock->timeout));
+
+ // optional according to https://tools.ietf.org/html/rfc4918#section-6.5
+ if (null !== $lock->token && '' !== $lock->token) {
+ $writer->startElement('{DAV:}locktoken');
+ $writer->writeElement('{DAV:}href', 'opaquelocktoken:'.$lock->token);
+ $writer->endElement(); // {DAV:}locktoken
+ }
+
+ if ($lock->owner) {
+ $writer->writeElement('{DAV:}owner', new XmlFragment($lock->owner));
+ }
+ $writer->endElement(); // {DAV:}activelock
+ }
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/ResourceType.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/ResourceType.php
new file mode 100644
index 0000000..75ddcba
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/ResourceType.php
@@ -0,0 +1,120 @@
+value;
+ }
+
+ /**
+ * Checks if the principal contains a certain value.
+ *
+ * @param string $type
+ *
+ * @return bool
+ */
+ public function is($type)
+ {
+ return in_array($type, $this->value);
+ }
+
+ /**
+ * Adds a resourcetype value to this property.
+ *
+ * @param string $type
+ */
+ public function add($type)
+ {
+ $this->value[] = $type;
+ $this->value = array_unique($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 mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ return new self(parent::xmlDeserialize($reader));
+ }
+
+ /**
+ * Generate html representation for this value.
+ *
+ * The html output is 100% trusted, and no effort is being made to sanitize
+ * it. It's up to the implementor to sanitize user provided values.
+ *
+ * The output must be in UTF-8.
+ *
+ * The baseUri parameter is a url to the root of the application, and can
+ * be used to construct local links.
+ *
+ * @return string
+ */
+ public function toHtml(HtmlOutputHelper $html)
+ {
+ return implode(
+ ', ',
+ array_map([$html, 'xmlName'], $this->getValue())
+ );
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/ShareAccess.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/ShareAccess.php
new file mode 100644
index 0000000..fdd5555
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/ShareAccess.php
@@ -0,0 +1,135 @@
+value = $shareAccess;
+ }
+
+ /**
+ * Returns the current value.
+ *
+ * @return int
+ */
+ public function getValue()
+ {
+ return $this->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(Writer $writer)
+ {
+ switch ($this->value) {
+ case SharingPlugin::ACCESS_NOTSHARED:
+ $writer->writeElement('{DAV:}not-shared');
+ break;
+ case SharingPlugin::ACCESS_SHAREDOWNER:
+ $writer->writeElement('{DAV:}shared-owner');
+ break;
+ case SharingPlugin::ACCESS_READ:
+ $writer->writeElement('{DAV:}read');
+ break;
+ case SharingPlugin::ACCESS_READWRITE:
+ $writer->writeElement('{DAV:}read-write');
+ break;
+ case SharingPlugin::ACCESS_NOACCESS:
+ $writer->writeElement('{DAV:}no-access');
+ 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.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $elems = $reader->parseInnerTree();
+ foreach ($elems as $elem) {
+ switch ($elem['name']) {
+ case '{DAV:}not-shared':
+ return new self(SharingPlugin::ACCESS_NOTSHARED);
+ case '{DAV:}shared-owner':
+ return new self(SharingPlugin::ACCESS_SHAREDOWNER);
+ case '{DAV:}read':
+ return new self(SharingPlugin::ACCESS_READ);
+ case '{DAV:}read-write':
+ return new self(SharingPlugin::ACCESS_READWRITE);
+ case '{DAV:}no-access':
+ return new self(SharingPlugin::ACCESS_NOACCESS);
+ }
+ }
+ throw new BadRequest('Invalid value for {DAV:}share-access element');
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedLock.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedLock.php
new file mode 100644
index 0000000..100829c
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedLock.php
@@ -0,0 +1,52 @@
+writeElement('{DAV:}lockentry', [
+ '{DAV:}lockscope' => ['{DAV:}exclusive' => null],
+ '{DAV:}locktype' => ['{DAV:}write' => null],
+ ]);
+ $writer->writeElement('{DAV:}lockentry', [
+ '{DAV:}lockscope' => ['{DAV:}shared' => null],
+ '{DAV:}locktype' => ['{DAV:}write' => null],
+ ]);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedMethodSet.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedMethodSet.php
new file mode 100644
index 0000000..6344010
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedMethodSet.php
@@ -0,0 +1,114 @@
+methods = $methods;
+ }
+
+ /**
+ * Returns the list of supported http methods.
+ *
+ * @return string[]
+ */
+ public function getValue()
+ {
+ return $this->methods;
+ }
+
+ /**
+ * Returns true or false if the property contains a specific method.
+ *
+ * @param string $methodName
+ *
+ * @return bool
+ */
+ public function has($methodName)
+ {
+ return in_array(
+ $methodName,
+ $this->methods
+ );
+ }
+
+ /**
+ * 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)
+ {
+ foreach ($this->getValue() as $val) {
+ $writer->startElement('{DAV:}supported-method');
+ $writer->writeAttribute('name', $val);
+ $writer->endElement();
+ }
+ }
+
+ /**
+ * Generate html representation for this value.
+ *
+ * The html output is 100% trusted, and no effort is being made to sanitize
+ * it. It's up to the implementor to sanitize user provided values.
+ *
+ * The output must be in UTF-8.
+ *
+ * The baseUri parameter is a url to the root of the application, and can
+ * be used to construct local links.
+ *
+ * @return string
+ */
+ public function toHtml(HtmlOutputHelper $html)
+ {
+ return implode(
+ ', ',
+ array_map([$html, 'h'], $this->getValue())
+ );
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedReportSet.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedReportSet.php
new file mode 100644
index 0000000..0b4990e
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedReportSet.php
@@ -0,0 +1,144 @@
+addReport($reports);
+ }
+ }
+
+ /**
+ * Adds a report to this property.
+ *
+ * The report must be a string in clark-notation.
+ * Multiple reports can be specified as an array.
+ *
+ * @param mixed $report
+ */
+ public function addReport($report)
+ {
+ $report = (array) $report;
+
+ foreach ($report as $r) {
+ if (!preg_match('/^{([^}]*)}(.*)$/', $r)) {
+ throw new DAV\Exception('Reportname must be in clark-notation');
+ }
+ $this->reports[] = $r;
+ }
+ }
+
+ /**
+ * Returns the list of supported reports.
+ *
+ * @return string[]
+ */
+ public function getValue()
+ {
+ return $this->reports;
+ }
+
+ /**
+ * Returns true or false if the property contains a specific report.
+ *
+ * @param string $reportName
+ *
+ * @return bool
+ */
+ public function has($reportName)
+ {
+ return in_array(
+ $reportName,
+ $this->reports
+ );
+ }
+
+ /**
+ * 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)
+ {
+ foreach ($this->getValue() as $val) {
+ $writer->startElement('{DAV:}supported-report');
+ $writer->startElement('{DAV:}report');
+ $writer->writeElement($val);
+ $writer->endElement();
+ $writer->endElement();
+ }
+ }
+
+ /**
+ * Generate html representation for this value.
+ *
+ * The html output is 100% trusted, and no effort is being made to sanitize
+ * it. It's up to the implementor to sanitize user provided values.
+ *
+ * The output must be in UTF-8.
+ *
+ * The baseUri parameter is a url to the root of the application, and can
+ * be used to construct local links.
+ *
+ * @return string
+ */
+ public function toHtml(HtmlOutputHelper $html)
+ {
+ return implode(
+ ', ',
+ array_map([$html, 'xmlName'], $this->getValue())
+ );
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/Lock.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/Lock.php
new file mode 100644
index 0000000..57d12ef
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/Lock.php
@@ -0,0 +1,84 @@
+next();
+ *
+ * $reader->parseInnerTree() will parse the entire sub-tree, and advance to
+ * the next element.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $reader->pushContext();
+ $reader->elementMap['{DAV:}owner'] = 'Sabre\\Xml\\Element\\XmlFragment';
+
+ $values = KeyValue::xmlDeserialize($reader);
+
+ $reader->popContext();
+
+ $new = new self();
+ $new->owner = !empty($values['{DAV:}owner']) ? $values['{DAV:}owner']->getXml() : null;
+ $new->scope = LockInfo::SHARED;
+
+ if (isset($values['{DAV:}lockscope'])) {
+ foreach ($values['{DAV:}lockscope'] as $elem) {
+ if ('{DAV:}exclusive' === $elem['name']) {
+ $new->scope = LockInfo::EXCLUSIVE;
+ }
+ }
+ }
+
+ return $new;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/MkCol.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/MkCol.php
new file mode 100644
index 0000000..e0d7e90
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/MkCol.php
@@ -0,0 +1,80 @@
+value array with properties that are supposed to get set
+ * during creation of the new collection.
+ *
+ * @return array
+ */
+ public function getProperties()
+ {
+ return $this->properties;
+ }
+
+ /**
+ * 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.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $self = new self();
+
+ $elementMap = $reader->elementMap;
+ $elementMap['{DAV:}prop'] = 'Sabre\DAV\Xml\Element\Prop';
+ $elementMap['{DAV:}set'] = 'Sabre\Xml\Element\KeyValue';
+ $elementMap['{DAV:}remove'] = 'Sabre\Xml\Element\KeyValue';
+
+ $elems = $reader->parseInnerTree($elementMap);
+
+ foreach ($elems as $elem) {
+ if ('{DAV:}set' === $elem['name']) {
+ $self->properties = array_merge($self->properties, $elem['value']['{DAV:}prop']);
+ }
+ }
+
+ return $self;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/PropFind.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/PropFind.php
new file mode 100644
index 0000000..505e7c7
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/PropFind.php
@@ -0,0 +1,79 @@
+next();
+ *
+ * $reader->parseInnerTree() will parse the entire sub-tree, and advance to
+ * the next element.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $self = new self();
+
+ $reader->pushContext();
+ $reader->elementMap['{DAV:}prop'] = 'Sabre\Xml\Element\Elements';
+
+ foreach (KeyValue::xmlDeserialize($reader) as $k => $v) {
+ switch ($k) {
+ case '{DAV:}prop':
+ $self->properties = $v;
+ break;
+ case '{DAV:}allprop':
+ $self->allProp = true;
+ }
+ }
+
+ $reader->popContext();
+
+ return $self;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/PropPatch.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/PropPatch.php
new file mode 100644
index 0000000..4a27095
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/PropPatch.php
@@ -0,0 +1,109 @@
+properties as $propertyName => $propertyValue) {
+ if (is_null($propertyValue)) {
+ $writer->startElement('{DAV:}remove');
+ $writer->write(['{DAV:}prop' => [$propertyName => $propertyValue]]);
+ $writer->endElement();
+ } else {
+ $writer->startElement('{DAV:}set');
+ $writer->write(['{DAV:}prop' => [$propertyName => $propertyValue]]);
+ $writer->endElement();
+ }
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $self = new self();
+
+ $elementMap = $reader->elementMap;
+ $elementMap['{DAV:}prop'] = 'Sabre\DAV\Xml\Element\Prop';
+ $elementMap['{DAV:}set'] = 'Sabre\Xml\Element\KeyValue';
+ $elementMap['{DAV:}remove'] = 'Sabre\Xml\Element\KeyValue';
+
+ $elems = $reader->parseInnerTree($elementMap);
+
+ foreach ($elems as $elem) {
+ if ('{DAV:}set' === $elem['name']) {
+ $self->properties = array_merge($self->properties, $elem['value']['{DAV:}prop']);
+ }
+ if ('{DAV:}remove' === $elem['name']) {
+ // Ensuring there are no values.
+ foreach ($elem['value']['{DAV:}prop'] as $remove => $value) {
+ $self->properties[$remove] = null;
+ }
+ }
+ }
+
+ return $self;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/ShareResource.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/ShareResource.php
new file mode 100644
index 0000000..79d7dc8
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/ShareResource.php
@@ -0,0 +1,80 @@
+sharees = $sharees;
+ }
+
+ /**
+ * 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.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $elems = $reader->parseInnerTree([
+ '{DAV:}sharee' => 'Sabre\DAV\Xml\Element\Sharee',
+ '{DAV:}share-access' => 'Sabre\DAV\Xml\Property\ShareAccess',
+ '{DAV:}prop' => 'Sabre\Xml\Deserializer\keyValue',
+ ]);
+
+ $sharees = [];
+
+ foreach ($elems as $elem) {
+ if ('{DAV:}sharee' !== $elem['name']) {
+ continue;
+ }
+ $sharees[] = $elem['value'];
+ }
+
+ return new self($sharees);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/SyncCollectionReport.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/SyncCollectionReport.php
new file mode 100644
index 0000000..8dd9576
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/SyncCollectionReport.php
@@ -0,0 +1,118 @@
+next();
+ *
+ * $reader->parseInnerTree() will parse the entire sub-tree, and advance to
+ * the next element.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $self = new self();
+
+ $reader->pushContext();
+
+ $reader->elementMap['{DAV:}prop'] = 'Sabre\Xml\Element\Elements';
+ $elems = KeyValue::xmlDeserialize($reader);
+
+ $reader->popContext();
+
+ $required = [
+ '{DAV:}sync-token',
+ '{DAV:}prop',
+ ];
+
+ foreach ($required as $elem) {
+ if (!array_key_exists($elem, $elems)) {
+ throw new BadRequest('The '.$elem.' element in the {DAV:}sync-collection report is required');
+ }
+ }
+
+ $self->properties = $elems['{DAV:}prop'];
+ $self->syncToken = $elems['{DAV:}sync-token'];
+
+ if (isset($elems['{DAV:}limit'])) {
+ $nresults = null;
+ foreach ($elems['{DAV:}limit'] as $child) {
+ if ('{DAV:}nresults' === $child['name']) {
+ $nresults = (int) $child['value'];
+ }
+ }
+ $self->limit = $nresults;
+ }
+
+ if (isset($elems['{DAV:}sync-level'])) {
+ $value = $elems['{DAV:}sync-level'];
+ if ('infinity' === $value) {
+ $value = \Sabre\DAV\Server::DEPTH_INFINITY;
+ }
+ $self->syncLevel = $value;
+ }
+
+ return $self;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Response/MultiStatus.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Response/MultiStatus.php
new file mode 100644
index 0000000..e824cda
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Response/MultiStatus.php
@@ -0,0 +1,136 @@
+responses = $responses;
+ $this->syncToken = $syncToken;
+ }
+
+ /**
+ * Returns the response list.
+ *
+ * @return \Sabre\DAV\Xml\Element\Response[]
+ */
+ public function getResponses()
+ {
+ return $this->responses;
+ }
+
+ /**
+ * Returns the sync-token, if available.
+ *
+ * @return string|null
+ */
+ public function getSyncToken()
+ {
+ return $this->syncToken;
+ }
+
+ /**
+ * The serialize method is called during xml writing.
+ *
+ * It should use the $writer argument to encode this object into XML.
+ *
+ * Important note: it is not needed to create the parent element. The
+ * parent element is already created, and we only have to worry about
+ * attributes, child elements and text (if any).
+ *
+ * Important note 2: If you are writing any new elements, you are also
+ * responsible for closing them.
+ */
+ public function xmlSerialize(Writer $writer)
+ {
+ foreach ($this->getResponses() as $response) {
+ $writer->writeElement('{DAV:}response', $response);
+ }
+ if ($syncToken = $this->getSyncToken()) {
+ $writer->writeElement('{DAV:}sync-token', $syncToken);
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $elementMap = $reader->elementMap;
+ $elementMap['{DAV:}prop'] = 'Sabre\\DAV\\Xml\\Element\\Prop';
+ $elements = $reader->parseInnerTree($elementMap);
+
+ $responses = [];
+ $syncToken = null;
+
+ if ($elements) {
+ foreach ($elements as $elem) {
+ if ('{DAV:}response' === $elem['name']) {
+ $responses[] = $elem['value'];
+ }
+ if ('{DAV:}sync-token' === $elem['name']) {
+ $syncToken = $elem['value'];
+ }
+ }
+ }
+
+ return new self($responses, $syncToken);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Service.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Service.php
new file mode 100644
index 0000000..4406b02
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Service.php
@@ -0,0 +1,47 @@
+ 'Sabre\\DAV\\Xml\\Response\\MultiStatus',
+ '{DAV:}response' => 'Sabre\\DAV\\Xml\\Element\\Response',
+
+ // Requests
+ '{DAV:}propfind' => 'Sabre\\DAV\\Xml\\Request\\PropFind',
+ '{DAV:}propertyupdate' => 'Sabre\\DAV\\Xml\\Request\\PropPatch',
+ '{DAV:}mkcol' => 'Sabre\\DAV\\Xml\\Request\\MkCol',
+
+ // Properties
+ '{DAV:}resourcetype' => 'Sabre\\DAV\\Xml\\Property\\ResourceType',
+ ];
+
+ /**
+ * This is a default list of namespaces.
+ *
+ * If you are defining your own custom namespace, add it here to reduce
+ * bandwidth and improve legibility of xml bodies.
+ *
+ * @var array
+ */
+ public $namespaceMap = [
+ 'DAV:' => 'd',
+ 'http://sabredav.org/ns' => 's',
+ ];
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/ACLTrait.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/ACLTrait.php
new file mode 100644
index 0000000..98c1ce3
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/ACLTrait.php
@@ -0,0 +1,94 @@
+ '{DAV:}all',
+ 'principal' => '{DAV:}owner',
+ 'protected' => true,
+ ],
+ ];
+ }
+
+ /**
+ * Updates the ACL.
+ *
+ * This method will receive a list of new ACE's as an array argument.
+ */
+ public function setACL(array $acl)
+ {
+ throw new \Sabre\DAV\Exception\Forbidden('Setting ACL is not supported on this node');
+ }
+
+ /**
+ * Returns the list of supported privileges for this node.
+ *
+ * The returned data structure is a list of nested privileges.
+ * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
+ * standard structure.
+ *
+ * If null is returned from this method, the default privilege set is used,
+ * which is fine for most common usecases.
+ *
+ * @return array|null
+ */
+ public function getSupportedPrivilegeSet()
+ {
+ return null;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/AbstractPrincipalCollection.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/AbstractPrincipalCollection.php
new file mode 100644
index 0000000..d26f7d2
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/AbstractPrincipalCollection.php
@@ -0,0 +1,178 @@
+principalPrefix = $principalPrefix;
+ $this->principalBackend = $principalBackend;
+ }
+
+ /**
+ * This method returns a node for a principal.
+ *
+ * The passed array contains principal information, and is guaranteed to
+ * at least contain a uri item. Other properties may or may not be
+ * supplied by the authentication backend.
+ *
+ * @return DAV\INode
+ */
+ abstract public function getChildForPrincipal(array $principalInfo);
+
+ /**
+ * Returns the name of this collection.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ list(, $name) = Uri\split($this->principalPrefix);
+
+ return $name;
+ }
+
+ /**
+ * Return the list of users.
+ *
+ * @return array
+ */
+ public function getChildren()
+ {
+ if ($this->disableListing) {
+ throw new DAV\Exception\MethodNotAllowed('Listing members of this collection is disabled');
+ }
+ $children = [];
+ foreach ($this->principalBackend->getPrincipalsByPrefix($this->principalPrefix) as $principalInfo) {
+ $children[] = $this->getChildForPrincipal($principalInfo);
+ }
+
+ return $children;
+ }
+
+ /**
+ * Returns a child object, by its name.
+ *
+ * @param string $name
+ *
+ * @throws DAV\Exception\NotFound
+ *
+ * @return DAV\INode
+ */
+ public function getChild($name)
+ {
+ $principalInfo = $this->principalBackend->getPrincipalByPath($this->principalPrefix.'/'.$name);
+ if (!$principalInfo) {
+ throw new DAV\Exception\NotFound('Principal with name '.$name.' not found');
+ }
+
+ return $this->getChildForPrincipal($principalInfo);
+ }
+
+ /**
+ * This method is used to search for principals matching a set of
+ * properties.
+ *
+ * This search is specifically used by RFC3744's principal-property-search
+ * REPORT. You should at least allow searching on
+ * http://sabredav.org/ns}email-address.
+ *
+ * The actual search should be a unicode-non-case-sensitive search. The
+ * keys in searchProperties are the WebDAV property names, while the values
+ * are the property values to search on.
+ *
+ * By default, if multiple properties are submitted to this method, the
+ * various properties should be combined with 'AND'. If $test is set to
+ * 'anyof', it should be combined using 'OR'.
+ *
+ * This method should simply return a list of 'child names', which may be
+ * used to call $this->getChild in the future.
+ *
+ * @param string $test
+ *
+ * @return array
+ */
+ public function searchPrincipals(array $searchProperties, $test = 'allof')
+ {
+ $result = $this->principalBackend->searchPrincipals($this->principalPrefix, $searchProperties, $test);
+ $r = [];
+
+ foreach ($result as $row) {
+ list(, $r[]) = Uri\split($row);
+ }
+
+ return $r;
+ }
+
+ /**
+ * Finds a principal by its URI.
+ *
+ * This method may receive any type of uri, but mailto: addresses will be
+ * the most common.
+ *
+ * Implementation of this API is optional. It is currently used by the
+ * CalDAV system to find principals based on their email addresses. If this
+ * API is not implemented, some features may not work correctly.
+ *
+ * This method must return a relative principal path, or null, if the
+ * principal was not found or you refuse to find it.
+ *
+ * @param string $uri
+ *
+ * @return string
+ */
+ public function findByUri($uri)
+ {
+ return $this->principalBackend->findByUri($uri, $this->principalPrefix);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/AceConflict.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/AceConflict.php
new file mode 100644
index 0000000..0fc3f77
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/AceConflict.php
@@ -0,0 +1,31 @@
+ownerDocument;
+
+ $np = $doc->createElementNS('DAV:', 'd:no-ace-conflict');
+ $errorNode->appendChild($np);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/NeedPrivileges.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/NeedPrivileges.php
new file mode 100644
index 0000000..af1f01c
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/NeedPrivileges.php
@@ -0,0 +1,73 @@
+uri = $uri;
+ $this->privileges = $privileges;
+
+ parent::__construct('User did not have the required privileges ('.implode(',', $privileges).') for path "'.$uri.'"');
+ }
+
+ /**
+ * Adds in extra information in the xml response.
+ *
+ * This method adds the {DAV:}need-privileges element as defined in rfc3744
+ */
+ public function serialize(DAV\Server $server, \DOMElement $errorNode)
+ {
+ $doc = $errorNode->ownerDocument;
+
+ $np = $doc->createElementNS('DAV:', 'd:need-privileges');
+ $errorNode->appendChild($np);
+
+ foreach ($this->privileges as $privilege) {
+ $resource = $doc->createElementNS('DAV:', 'd:resource');
+ $np->appendChild($resource);
+
+ $resource->appendChild($doc->createElementNS('DAV:', 'd:href', $server->getBaseUri().$this->uri));
+
+ $priv = $doc->createElementNS('DAV:', 'd:privilege');
+ $resource->appendChild($priv);
+
+ preg_match('/^{([^}]*)}(.*)$/', $privilege, $privilegeParts);
+ $priv->appendChild($doc->createElementNS($privilegeParts[1], 'd:'.$privilegeParts[2]));
+ }
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/NoAbstract.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/NoAbstract.php
new file mode 100644
index 0000000..b9c6616
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/NoAbstract.php
@@ -0,0 +1,31 @@
+ownerDocument;
+
+ $np = $doc->createElementNS('DAV:', 'd:no-abstract');
+ $errorNode->appendChild($np);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/NotRecognizedPrincipal.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/NotRecognizedPrincipal.php
new file mode 100644
index 0000000..d4e7284
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/NotRecognizedPrincipal.php
@@ -0,0 +1,31 @@
+ownerDocument;
+
+ $np = $doc->createElementNS('DAV:', 'd:recognized-principal');
+ $errorNode->appendChild($np);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/NotSupportedPrivilege.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/NotSupportedPrivilege.php
new file mode 100644
index 0000000..c04c5fa
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/NotSupportedPrivilege.php
@@ -0,0 +1,31 @@
+ownerDocument;
+
+ $np = $doc->createElementNS('DAV:', 'd:not-supported-privilege');
+ $errorNode->appendChild($np);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/FS/Collection.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/FS/Collection.php
new file mode 100644
index 0000000..85b04e2
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/FS/Collection.php
@@ -0,0 +1,109 @@
+acl = $acl;
+ $this->owner = $owner;
+ }
+
+ /**
+ * Returns a specific child node, referenced by its name.
+ *
+ * This method must throw Sabre\DAV\Exception\NotFound if the node does not
+ * exist.
+ *
+ * @param string $name
+ *
+ * @throws NotFound
+ *
+ * @return \Sabre\DAV\INode
+ */
+ public function getChild($name)
+ {
+ $path = $this->path.'/'.$name;
+
+ if (!file_exists($path)) {
+ throw new NotFound('File could not be located');
+ }
+ if ('.' == $name || '..' == $name) {
+ throw new Forbidden('Permission denied to . and ..');
+ }
+ if (is_dir($path)) {
+ return new self($path, $this->acl, $this->owner);
+ } else {
+ return new File($path, $this->acl, $this->owner);
+ }
+ }
+
+ /**
+ * Returns the owner principal.
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner()
+ {
+ return $this->owner;
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ public function getACL()
+ {
+ return $this->acl;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/FS/File.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/FS/File.php
new file mode 100644
index 0000000..5506aa2
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/FS/File.php
@@ -0,0 +1,78 @@
+acl = $acl;
+ $this->owner = $owner;
+ }
+
+ /**
+ * Returns the owner principal.
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner()
+ {
+ return $this->owner;
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ public function getACL()
+ {
+ return $this->acl;
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/FS/HomeCollection.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/FS/HomeCollection.php
new file mode 100644
index 0000000..fa476e0
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/FS/HomeCollection.php
@@ -0,0 +1,123 @@
+storagePath = $storagePath;
+ }
+
+ /**
+ * Returns the name of the node.
+ *
+ * This is used to generate the url.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->collectionName;
+ }
+
+ /**
+ * Returns a principals' collection of files.
+ *
+ * The passed array contains principal information, and is guaranteed to
+ * at least contain a uri item. Other properties may or may not be
+ * supplied by the authentication backend.
+ *
+ * @return \Sabre\DAV\INode
+ */
+ public function getChildForPrincipal(array $principalInfo)
+ {
+ $owner = $principalInfo['uri'];
+ $acl = [
+ [
+ 'privilege' => '{DAV:}all',
+ 'principal' => '{DAV:}owner',
+ 'protected' => true,
+ ],
+ ];
+
+ list(, $principalBaseName) = Uri\split($owner);
+
+ $path = $this->storagePath.'/'.$principalBaseName;
+
+ if (!is_dir($path)) {
+ mkdir($path, 0777, true);
+ }
+
+ return new Collection(
+ $path,
+ $acl,
+ $owner
+ );
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ public function getACL()
+ {
+ return [
+ [
+ 'principal' => '{DAV:}authenticated',
+ 'privilege' => '{DAV:}read',
+ 'protected' => true,
+ ],
+ ];
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/IACL.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/IACL.php
new file mode 100644
index 0000000..291fb24
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/IACL.php
@@ -0,0 +1,72 @@
+getChild in the future.
+ *
+ * @param string $test
+ *
+ * @return array
+ */
+ public function searchPrincipals(array $searchProperties, $test = 'allof');
+
+ /**
+ * Finds a principal by its URI.
+ *
+ * This method may receive any type of uri, but mailto: addresses will be
+ * the most common.
+ *
+ * Implementation of this API is optional. It is currently used by the
+ * CalDAV system to find principals based on their email addresses. If this
+ * API is not implemented, some features may not work correctly.
+ *
+ * This method must return a relative principal path, or null, if the
+ * principal was not found or you refuse to find it.
+ *
+ * @param string $uri
+ *
+ * @return string
+ */
+ public function findByUri($uri);
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/Plugin.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Plugin.php
new file mode 100644
index 0000000..f049784
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/Plugin.php
@@ -0,0 +1,1545 @@
+ 'Display name',
+ '{http://sabredav.org/ns}email-address' => 'Email address',
+ ];
+
+ /**
+ * Any principal uri's added here, will automatically be added to the list
+ * of ACL's. They will effectively receive {DAV:}all privileges, as a
+ * protected privilege.
+ *
+ * @var array
+ */
+ public $adminPrincipals = [];
+
+ /**
+ * The ACL plugin allows privileges to be assigned to users that are not
+ * logged in. To facilitate that, it modifies the auth plugin's behavior
+ * to only require login when a privileged operation was denied.
+ *
+ * Unauthenticated access can be considered a security concern, so it's
+ * possible to turn this feature off to harden the server's security.
+ *
+ * @var bool
+ */
+ public $allowUnauthenticatedAccess = true;
+
+ /**
+ * Returns a list of features added by this plugin.
+ *
+ * This list is used in the response of a HTTP OPTIONS request.
+ *
+ * @return array
+ */
+ public function getFeatures()
+ {
+ return ['access-control', 'calendarserver-principal-property-search'];
+ }
+
+ /**
+ * Returns a list of available methods for a given url.
+ *
+ * @param string $uri
+ *
+ * @return array
+ */
+ public function getMethods($uri)
+ {
+ return ['ACL'];
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using Sabre\DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ public function getPluginName()
+ {
+ return 'acl';
+ }
+
+ /**
+ * Returns a list of reports this plugin supports.
+ *
+ * This will be used in the {DAV:}supported-report-set property.
+ * Note that you still need to subscribe to the 'report' event to actually
+ * implement them
+ *
+ * @param string $uri
+ *
+ * @return array
+ */
+ public function getSupportedReportSet($uri)
+ {
+ return [
+ '{DAV:}expand-property',
+ '{DAV:}principal-match',
+ '{DAV:}principal-property-search',
+ '{DAV:}principal-search-property-set',
+ ];
+ }
+
+ /**
+ * Checks if the current user has the specified privilege(s).
+ *
+ * You can specify a single privilege, or a list of privileges.
+ * This method will throw an exception if the privilege is not available
+ * and return true otherwise.
+ *
+ * @param string $uri
+ * @param array|string $privileges
+ * @param int $recursion
+ * @param bool $throwExceptions if set to false, this method won't throw exceptions
+ *
+ * @throws NeedPrivileges
+ * @throws NotAuthenticated
+ *
+ * @return bool
+ */
+ public function checkPrivileges($uri, $privileges, $recursion = self::R_PARENT, $throwExceptions = true)
+ {
+ if (!is_array($privileges)) {
+ $privileges = [$privileges];
+ }
+
+ $acl = $this->getCurrentUserPrivilegeSet($uri);
+
+ $failed = [];
+ foreach ($privileges as $priv) {
+ if (!in_array($priv, $acl)) {
+ $failed[] = $priv;
+ }
+ }
+
+ if ($failed) {
+ if ($this->allowUnauthenticatedAccess && is_null($this->getCurrentUserPrincipal())) {
+ // We are not authenticated. Kicking in the Auth plugin.
+ $authPlugin = $this->server->getPlugin('auth');
+ $reasons = $authPlugin->getLoginFailedReasons();
+ $authPlugin->challenge(
+ $this->server->httpRequest,
+ $this->server->httpResponse
+ );
+ throw new NotAuthenticated(implode(', ', $reasons).'. Login was needed for privilege: '.implode(', ', $failed).' on '.$uri);
+ }
+ if ($throwExceptions) {
+ throw new NeedPrivileges($uri, $failed);
+ } else {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the standard users' principal.
+ *
+ * This is one authoritative principal url for the current user.
+ * This method will return null if the user wasn't logged in.
+ *
+ * @return string|null
+ */
+ public function getCurrentUserPrincipal()
+ {
+ /** @var $authPlugin \Sabre\DAV\Auth\Plugin */
+ $authPlugin = $this->server->getPlugin('auth');
+ if (!$authPlugin) {
+ return null;
+ }
+
+ return $authPlugin->getCurrentPrincipal();
+ }
+
+ /**
+ * Returns a list of principals that's associated to the current
+ * user, either directly or through group membership.
+ *
+ * @return array
+ */
+ public function getCurrentUserPrincipals()
+ {
+ $currentUser = $this->getCurrentUserPrincipal();
+
+ if (is_null($currentUser)) {
+ return [];
+ }
+
+ return array_merge(
+ [$currentUser],
+ $this->getPrincipalMembership($currentUser)
+ );
+ }
+
+ /**
+ * Sets the default ACL rules.
+ *
+ * These rules are used for all nodes that don't implement the IACL interface.
+ */
+ public function setDefaultAcl(array $acl)
+ {
+ $this->defaultAcl = $acl;
+ }
+
+ /**
+ * Returns the default ACL rules.
+ *
+ * These rules are used for all nodes that don't implement the IACL interface.
+ *
+ * @return array
+ */
+ public function getDefaultAcl()
+ {
+ return $this->defaultAcl;
+ }
+
+ /**
+ * The default ACL rules.
+ *
+ * These rules are used for nodes that don't implement IACL. These default
+ * set of rules allow anyone to do anything, as long as they are
+ * authenticated.
+ *
+ * @var array
+ */
+ protected $defaultAcl = [
+ [
+ 'principal' => '{DAV:}authenticated',
+ 'protected' => true,
+ 'privilege' => '{DAV:}all',
+ ],
+ ];
+
+ /**
+ * This array holds a cache for all the principals that are associated with
+ * a single principal.
+ *
+ * @var array
+ */
+ protected $principalMembershipCache = [];
+
+ /**
+ * Returns all the principal groups the specified principal is a member of.
+ *
+ * @param string $mainPrincipal
+ *
+ * @return array
+ */
+ public function getPrincipalMembership($mainPrincipal)
+ {
+ // First check our cache
+ if (isset($this->principalMembershipCache[$mainPrincipal])) {
+ return $this->principalMembershipCache[$mainPrincipal];
+ }
+
+ $check = [$mainPrincipal];
+ $principals = [];
+
+ while (count($check)) {
+ $principal = array_shift($check);
+
+ $node = $this->server->tree->getNodeForPath($principal);
+ if ($node instanceof IPrincipal) {
+ foreach ($node->getGroupMembership() as $groupMember) {
+ if (!in_array($groupMember, $principals)) {
+ $check[] = $groupMember;
+ $principals[] = $groupMember;
+ }
+ }
+ }
+ }
+
+ // Store the result in the cache
+ $this->principalMembershipCache[$mainPrincipal] = $principals;
+
+ return $principals;
+ }
+
+ /**
+ * Find out of a principal equals another principal.
+ *
+ * This is a quick way to find out whether a principal URI is part of a
+ * group, or any subgroups.
+ *
+ * The first argument is the principal URI you want to check against. For
+ * example the principal group, and the second argument is the principal of
+ * which you want to find out of it is the same as the first principal, or
+ * in a member of the first principal's group or subgroups.
+ *
+ * So the arguments are not interchangeable. If principal A is in group B,
+ * passing 'B', 'A' will yield true, but 'A', 'B' is false.
+ *
+ * If the second argument is not passed, we will use the current user
+ * principal.
+ *
+ * @param string $checkPrincipal
+ * @param string $currentPrincipal
+ *
+ * @return bool
+ */
+ public function principalMatchesPrincipal($checkPrincipal, $currentPrincipal = null)
+ {
+ if (is_null($currentPrincipal)) {
+ $currentPrincipal = $this->getCurrentUserPrincipal();
+ }
+ if ($currentPrincipal === $checkPrincipal) {
+ return true;
+ }
+ if (is_null($currentPrincipal)) {
+ return false;
+ }
+
+ return in_array(
+ $checkPrincipal,
+ $this->getPrincipalMembership($currentPrincipal)
+ );
+ }
+
+ /**
+ * Returns a tree of supported privileges for a resource.
+ *
+ * The returned array structure should be in this form:
+ *
+ * [
+ * [
+ * 'privilege' => '{DAV:}read',
+ * 'abstract' => false,
+ * 'aggregates' => []
+ * ]
+ * ]
+ *
+ * Privileges can be nested using "aggregates". Doing so means that
+ * if you assign someone the aggregating privilege, all the
+ * sub-privileges will automatically be granted.
+ *
+ * Marking a privilege as abstract means that the privilege cannot be
+ * directly assigned, but must be assigned via the parent privilege.
+ *
+ * So a more complex version might look like this:
+ *
+ * [
+ * [
+ * 'privilege' => '{DAV:}read',
+ * 'abstract' => false,
+ * 'aggregates' => [
+ * [
+ * 'privilege' => '{DAV:}read-acl',
+ * 'abstract' => false,
+ * 'aggregates' => [],
+ * ]
+ * ]
+ * ]
+ * ]
+ *
+ * @param string|INode $node
+ *
+ * @return array
+ */
+ public function getSupportedPrivilegeSet($node)
+ {
+ if (is_string($node)) {
+ $node = $this->server->tree->getNodeForPath($node);
+ }
+
+ $supportedPrivileges = null;
+ if ($node instanceof IACL) {
+ $supportedPrivileges = $node->getSupportedPrivilegeSet();
+ }
+
+ if (is_null($supportedPrivileges)) {
+ // Default
+ $supportedPrivileges = [
+ '{DAV:}read' => [
+ 'abstract' => false,
+ 'aggregates' => [
+ '{DAV:}read-acl' => [
+ 'abstract' => false,
+ 'aggregates' => [],
+ ],
+ '{DAV:}read-current-user-privilege-set' => [
+ 'abstract' => false,
+ 'aggregates' => [],
+ ],
+ ],
+ ],
+ '{DAV:}write' => [
+ 'abstract' => false,
+ 'aggregates' => [
+ '{DAV:}write-properties' => [
+ 'abstract' => false,
+ 'aggregates' => [],
+ ],
+ '{DAV:}write-content' => [
+ 'abstract' => false,
+ 'aggregates' => [],
+ ],
+ '{DAV:}unlock' => [
+ 'abstract' => false,
+ 'aggregates' => [],
+ ],
+ ],
+ ],
+ ];
+ if ($node instanceof DAV\ICollection) {
+ $supportedPrivileges['{DAV:}write']['aggregates']['{DAV:}bind'] = [
+ 'abstract' => false,
+ 'aggregates' => [],
+ ];
+ $supportedPrivileges['{DAV:}write']['aggregates']['{DAV:}unbind'] = [
+ 'abstract' => false,
+ 'aggregates' => [],
+ ];
+ }
+ if ($node instanceof IACL) {
+ $supportedPrivileges['{DAV:}write']['aggregates']['{DAV:}write-acl'] = [
+ 'abstract' => false,
+ 'aggregates' => [],
+ ];
+ }
+ }
+
+ $this->server->emit(
+ 'getSupportedPrivilegeSet',
+ [$node, &$supportedPrivileges]
+ );
+
+ return $supportedPrivileges;
+ }
+
+ /**
+ * Returns the supported privilege set as a flat list.
+ *
+ * This is much easier to parse.
+ *
+ * The returned list will be index by privilege name.
+ * The value is a struct containing the following properties:
+ * - aggregates
+ * - abstract
+ * - concrete
+ *
+ * @param string|INode $node
+ *
+ * @return array
+ */
+ final public function getFlatPrivilegeSet($node)
+ {
+ $privs = [
+ 'abstract' => false,
+ 'aggregates' => $this->getSupportedPrivilegeSet($node),
+ ];
+
+ $fpsTraverse = null;
+ $fpsTraverse = function ($privName, $privInfo, $concrete, &$flat) use (&$fpsTraverse) {
+ $myPriv = [
+ 'privilege' => $privName,
+ 'abstract' => isset($privInfo['abstract']) && $privInfo['abstract'],
+ 'aggregates' => [],
+ 'concrete' => isset($privInfo['abstract']) && $privInfo['abstract'] ? $concrete : $privName,
+ ];
+
+ if (isset($privInfo['aggregates'])) {
+ foreach ($privInfo['aggregates'] as $subPrivName => $subPrivInfo) {
+ $myPriv['aggregates'][] = $subPrivName;
+ }
+ }
+
+ $flat[$privName] = $myPriv;
+
+ if (isset($privInfo['aggregates'])) {
+ foreach ($privInfo['aggregates'] as $subPrivName => $subPrivInfo) {
+ $fpsTraverse($subPrivName, $subPrivInfo, $myPriv['concrete'], $flat);
+ }
+ }
+ };
+
+ $flat = [];
+ $fpsTraverse('{DAV:}all', $privs, null, $flat);
+
+ return $flat;
+ }
+
+ /**
+ * Returns the full ACL list.
+ *
+ * Either a uri or a INode may be passed.
+ *
+ * null will be returned if the node doesn't support ACLs.
+ *
+ * @param string|DAV\INode $node
+ *
+ * @return array
+ */
+ public function getAcl($node)
+ {
+ if (is_string($node)) {
+ $node = $this->server->tree->getNodeForPath($node);
+ }
+ if (!$node instanceof IACL) {
+ return $this->getDefaultAcl();
+ }
+ $acl = $node->getACL();
+ foreach ($this->adminPrincipals as $adminPrincipal) {
+ $acl[] = [
+ 'principal' => $adminPrincipal,
+ 'privilege' => '{DAV:}all',
+ 'protected' => true,
+ ];
+ }
+
+ return $acl;
+ }
+
+ /**
+ * Returns a list of privileges the current user has
+ * on a particular node.
+ *
+ * Either a uri or a DAV\INode may be passed.
+ *
+ * null will be returned if the node doesn't support ACLs.
+ *
+ * @param string|DAV\INode $node
+ *
+ * @return array
+ */
+ public function getCurrentUserPrivilegeSet($node)
+ {
+ if (is_string($node)) {
+ $node = $this->server->tree->getNodeForPath($node);
+ }
+
+ $acl = $this->getACL($node);
+
+ $collected = [];
+
+ $isAuthenticated = null !== $this->getCurrentUserPrincipal();
+
+ foreach ($acl as $ace) {
+ $principal = $ace['principal'];
+
+ switch ($principal) {
+ case '{DAV:}owner':
+ $owner = $node->getOwner();
+ if ($owner && $this->principalMatchesPrincipal($owner)) {
+ $collected[] = $ace;
+ }
+ break;
+
+ // 'all' matches for every user
+ case '{DAV:}all':
+ $collected[] = $ace;
+ break;
+
+ case '{DAV:}authenticated':
+ // Authenticated users only
+ if ($isAuthenticated) {
+ $collected[] = $ace;
+ }
+ break;
+
+ case '{DAV:}unauthenticated':
+ // Unauthenticated users only
+ if (!$isAuthenticated) {
+ $collected[] = $ace;
+ }
+ break;
+
+ default:
+ if ($this->principalMatchesPrincipal($ace['principal'])) {
+ $collected[] = $ace;
+ }
+ break;
+ }
+ }
+
+ // Now we deduct all aggregated privileges.
+ $flat = $this->getFlatPrivilegeSet($node);
+
+ $collected2 = [];
+ while (count($collected)) {
+ $current = array_pop($collected);
+ $collected2[] = $current['privilege'];
+
+ if (!isset($flat[$current['privilege']])) {
+ // Ignoring privileges that are not in the supported-privileges list.
+ $this->server->getLogger()->debug('A node has the "'.$current['privilege'].'" in its ACL list, but this privilege was not reported in the supportedPrivilegeSet list. This will be ignored.');
+ continue;
+ }
+ foreach ($flat[$current['privilege']]['aggregates'] as $subPriv) {
+ $collected2[] = $subPriv;
+ $collected[] = $flat[$subPriv];
+ }
+ }
+
+ return array_values(array_unique($collected2));
+ }
+
+ /**
+ * Returns a principal based on its uri.
+ *
+ * Returns null if the principal could not be found.
+ *
+ * @param string $uri
+ *
+ * @return string|null
+ */
+ public function getPrincipalByUri($uri)
+ {
+ $result = null;
+ $collections = $this->principalCollectionSet;
+ foreach ($collections as $collection) {
+ try {
+ $principalCollection = $this->server->tree->getNodeForPath($collection);
+ } catch (NotFound $e) {
+ // Ignore and move on
+ continue;
+ }
+
+ if (!$principalCollection instanceof IPrincipalCollection) {
+ // Not a principal collection, we're simply going to ignore
+ // this.
+ continue;
+ }
+
+ $result = $principalCollection->findByUri($uri);
+ if ($result) {
+ return $result;
+ }
+ }
+ }
+
+ /**
+ * Principal property search.
+ *
+ * This method can search for principals matching certain values in
+ * properties.
+ *
+ * This method will return a list of properties for the matched properties.
+ *
+ * @param array $searchProperties The properties to search on. This is a
+ * key-value list. The keys are property
+ * names, and the values the strings to
+ * match them on.
+ * @param array $requestedProperties this is the list of properties to
+ * return for every match
+ * @param string $collectionUri the principal collection to search on.
+ * If this is omitted, the standard
+ * principal collection-set will be used
+ * @param string $test "allof" to use AND to search the
+ * properties. 'anyof' for OR.
+ *
+ * @return array This method returns an array structure similar to
+ * Sabre\DAV\Server::getPropertiesForPath. Returned
+ * properties are index by a HTTP status code.
+ */
+ public function principalSearch(array $searchProperties, array $requestedProperties, $collectionUri = null, $test = 'allof')
+ {
+ if (!is_null($collectionUri)) {
+ $uris = [$collectionUri];
+ } else {
+ $uris = $this->principalCollectionSet;
+ }
+
+ $lookupResults = [];
+ foreach ($uris as $uri) {
+ $principalCollection = $this->server->tree->getNodeForPath($uri);
+ if (!$principalCollection instanceof IPrincipalCollection) {
+ // Not a principal collection, we're simply going to ignore
+ // this.
+ continue;
+ }
+
+ $results = $principalCollection->searchPrincipals($searchProperties, $test);
+ foreach ($results as $result) {
+ $lookupResults[] = rtrim($uri, '/').'/'.$result;
+ }
+ }
+
+ $matches = [];
+
+ foreach ($lookupResults as $lookupResult) {
+ list($matches[]) = $this->server->getPropertiesForPath($lookupResult, $requestedProperties, 0);
+ }
+
+ return $matches;
+ }
+
+ /**
+ * Sets up the plugin.
+ *
+ * This method is automatically called by the server class.
+ */
+ public function initialize(DAV\Server $server)
+ {
+ if ($this->allowUnauthenticatedAccess) {
+ $authPlugin = $server->getPlugin('auth');
+ if (!$authPlugin) {
+ throw new \Exception('The Auth plugin must be loaded before the ACL plugin if you want to allow unauthenticated access.');
+ }
+ $authPlugin->autoRequireLogin = false;
+ }
+
+ $this->server = $server;
+ $server->on('propFind', [$this, 'propFind'], 20);
+ $server->on('beforeMethod:*', [$this, 'beforeMethod'], 20);
+ $server->on('beforeBind', [$this, 'beforeBind'], 20);
+ $server->on('beforeUnbind', [$this, 'beforeUnbind'], 20);
+ $server->on('propPatch', [$this, 'propPatch']);
+ $server->on('beforeUnlock', [$this, 'beforeUnlock'], 20);
+ $server->on('report', [$this, 'report']);
+ $server->on('method:ACL', [$this, 'httpAcl']);
+ $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']);
+ $server->on('getPrincipalByUri', function ($principal, &$uri) {
+ $uri = $this->getPrincipalByUri($principal);
+
+ // Break event chain
+ if ($uri) {
+ return false;
+ }
+ });
+
+ array_push($server->protectedProperties,
+ '{DAV:}alternate-URI-set',
+ '{DAV:}principal-URL',
+ '{DAV:}group-membership',
+ '{DAV:}principal-collection-set',
+ '{DAV:}current-user-principal',
+ '{DAV:}supported-privilege-set',
+ '{DAV:}current-user-privilege-set',
+ '{DAV:}acl',
+ '{DAV:}acl-restrictions',
+ '{DAV:}inherited-acl-set',
+ '{DAV:}owner',
+ '{DAV:}group'
+ );
+
+ // Automatically mapping nodes implementing IPrincipal to the
+ // {DAV:}principal resourcetype.
+ $server->resourceTypeMapping['Sabre\\DAVACL\\IPrincipal'] = '{DAV:}principal';
+
+ // Mapping the group-member-set property to the HrefList property
+ // class.
+ $server->xml->elementMap['{DAV:}group-member-set'] = 'Sabre\\DAV\\Xml\\Property\\Href';
+ $server->xml->elementMap['{DAV:}acl'] = 'Sabre\\DAVACL\\Xml\\Property\\Acl';
+ $server->xml->elementMap['{DAV:}acl-principal-prop-set'] = 'Sabre\\DAVACL\\Xml\\Request\\AclPrincipalPropSetReport';
+ $server->xml->elementMap['{DAV:}expand-property'] = 'Sabre\\DAVACL\\Xml\\Request\\ExpandPropertyReport';
+ $server->xml->elementMap['{DAV:}principal-property-search'] = 'Sabre\\DAVACL\\Xml\\Request\\PrincipalPropertySearchReport';
+ $server->xml->elementMap['{DAV:}principal-search-property-set'] = 'Sabre\\DAVACL\\Xml\\Request\\PrincipalSearchPropertySetReport';
+ $server->xml->elementMap['{DAV:}principal-match'] = 'Sabre\\DAVACL\\Xml\\Request\\PrincipalMatchReport';
+ }
+
+ /* {{{ Event handlers */
+
+ /**
+ * Triggered before any method is handled.
+ */
+ public function beforeMethod(RequestInterface $request, ResponseInterface $response)
+ {
+ $method = $request->getMethod();
+ $path = $request->getPath();
+
+ $exists = $this->server->tree->nodeExists($path);
+
+ // If the node doesn't exists, none of these checks apply
+ if (!$exists) {
+ return;
+ }
+
+ switch ($method) {
+ case 'GET':
+ case 'HEAD':
+ case 'OPTIONS':
+ // For these 3 we only need to know if the node is readable.
+ $this->checkPrivileges($path, '{DAV:}read');
+ break;
+
+ case 'PUT':
+ case 'LOCK':
+ // This method requires the write-content priv if the node
+ // already exists, and bind on the parent if the node is being
+ // created.
+ // The bind privilege is handled in the beforeBind event.
+ $this->checkPrivileges($path, '{DAV:}write-content');
+ break;
+
+ case 'UNLOCK':
+ // Unlock is always allowed at the moment.
+ break;
+
+ case 'PROPPATCH':
+ $this->checkPrivileges($path, '{DAV:}write-properties');
+ break;
+
+ case 'ACL':
+ $this->checkPrivileges($path, '{DAV:}write-acl');
+ break;
+
+ case 'COPY':
+ case 'MOVE':
+ // Copy requires read privileges on the entire source tree.
+ // If the target exists write-content normally needs to be
+ // checked, however, we're deleting the node beforehand and
+ // creating a new one after, so this is handled by the
+ // beforeUnbind event.
+ //
+ // The creation of the new node is handled by the beforeBind
+ // event.
+ //
+ // If MOVE is used beforeUnbind will also be used to check if
+ // the sourcenode can be deleted.
+ $this->checkPrivileges($path, '{DAV:}read', self::R_RECURSIVE);
+ break;
+ }
+ }
+
+ /**
+ * Triggered before a new node is created.
+ *
+ * This allows us to check permissions for any operation that creates a
+ * new node, such as PUT, MKCOL, MKCALENDAR, LOCK, COPY and MOVE.
+ *
+ * @param string $uri
+ */
+ public function beforeBind($uri)
+ {
+ list($parentUri) = Uri\split($uri);
+ $this->checkPrivileges($parentUri, '{DAV:}bind');
+ }
+
+ /**
+ * Triggered before a node is deleted.
+ *
+ * This allows us to check permissions for any operation that will delete
+ * an existing node.
+ *
+ * @param string $uri
+ */
+ public function beforeUnbind($uri)
+ {
+ list($parentUri) = Uri\split($uri);
+ $this->checkPrivileges($parentUri, '{DAV:}unbind', self::R_RECURSIVEPARENTS);
+ }
+
+ /**
+ * Triggered before a node is unlocked.
+ *
+ * @param string $uri
+ * @TODO: not yet implemented
+ */
+ public function beforeUnlock($uri, DAV\Locks\LockInfo $lock)
+ {
+ }
+
+ /**
+ * Triggered before properties are looked up in specific nodes.
+ *
+ * @TODO really should be broken into multiple methods, or even a class.
+ */
+ public function propFind(DAV\PropFind $propFind, DAV\INode $node)
+ {
+ $path = $propFind->getPath();
+
+ // Checking the read permission
+ if (!$this->checkPrivileges($path, '{DAV:}read', self::R_PARENT, false)) {
+ // User is not allowed to read properties
+
+ // Returning false causes the property-fetching system to pretend
+ // that the node does not exist, and will cause it to be hidden
+ // from listings such as PROPFIND or the browser plugin.
+ if ($this->hideNodesFromListings) {
+ return false;
+ }
+
+ // Otherwise we simply mark every property as 403.
+ foreach ($propFind->getRequestedProperties() as $requestedProperty) {
+ $propFind->set($requestedProperty, null, 403);
+ }
+
+ return;
+ }
+
+ /* Adding principal properties */
+ if ($node instanceof IPrincipal) {
+ $propFind->handle('{DAV:}alternate-URI-set', function () use ($node) {
+ return new Href($node->getAlternateUriSet());
+ });
+ $propFind->handle('{DAV:}principal-URL', function () use ($node) {
+ return new Href($node->getPrincipalUrl().'/');
+ });
+ $propFind->handle('{DAV:}group-member-set', function () use ($node) {
+ $members = $node->getGroupMemberSet();
+ foreach ($members as $k => $member) {
+ $members[$k] = rtrim($member, '/').'/';
+ }
+
+ return new Href($members);
+ });
+ $propFind->handle('{DAV:}group-membership', function () use ($node) {
+ $members = $node->getGroupMembership();
+ foreach ($members as $k => $member) {
+ $members[$k] = rtrim($member, '/').'/';
+ }
+
+ return new Href($members);
+ });
+ $propFind->handle('{DAV:}displayname', [$node, 'getDisplayName']);
+ }
+
+ $propFind->handle('{DAV:}principal-collection-set', function () {
+ $val = $this->principalCollectionSet;
+ // Ensuring all collections end with a slash
+ foreach ($val as $k => $v) {
+ $val[$k] = $v.'/';
+ }
+
+ return new Href($val);
+ });
+ $propFind->handle('{DAV:}current-user-principal', function () {
+ if ($url = $this->getCurrentUserPrincipal()) {
+ return new Xml\Property\Principal(Xml\Property\Principal::HREF, $url.'/');
+ } else {
+ return new Xml\Property\Principal(Xml\Property\Principal::UNAUTHENTICATED);
+ }
+ });
+ $propFind->handle('{DAV:}supported-privilege-set', function () use ($node) {
+ return new Xml\Property\SupportedPrivilegeSet($this->getSupportedPrivilegeSet($node));
+ });
+ $propFind->handle('{DAV:}current-user-privilege-set', function () use ($node, $propFind, $path) {
+ if (!$this->checkPrivileges($path, '{DAV:}read-current-user-privilege-set', self::R_PARENT, false)) {
+ $propFind->set('{DAV:}current-user-privilege-set', null, 403);
+ } else {
+ $val = $this->getCurrentUserPrivilegeSet($node);
+
+ return new Xml\Property\CurrentUserPrivilegeSet($val);
+ }
+ });
+ $propFind->handle('{DAV:}acl', function () use ($node, $propFind, $path) {
+ /* The ACL property contains all the permissions */
+ if (!$this->checkPrivileges($path, '{DAV:}read-acl', self::R_PARENT, false)) {
+ $propFind->set('{DAV:}acl', null, 403);
+ } else {
+ $acl = $this->getACL($node);
+
+ return new Xml\Property\Acl($this->getACL($node));
+ }
+ });
+ $propFind->handle('{DAV:}acl-restrictions', function () {
+ return new Xml\Property\AclRestrictions();
+ });
+
+ /* Adding ACL properties */
+ if ($node instanceof IACL) {
+ $propFind->handle('{DAV:}owner', function () use ($node) {
+ return new Href($node->getOwner().'/');
+ });
+ }
+ }
+
+ /**
+ * This method intercepts PROPPATCH methods and make sure the
+ * group-member-set is updated correctly.
+ *
+ * @param string $path
+ */
+ public function propPatch($path, DAV\PropPatch $propPatch)
+ {
+ $propPatch->handle('{DAV:}group-member-set', function ($value) use ($path) {
+ if (is_null($value)) {
+ $memberSet = [];
+ } elseif ($value instanceof Href) {
+ $memberSet = array_map(
+ [$this->server, 'calculateUri'],
+ $value->getHrefs()
+ );
+ } else {
+ throw new DAV\Exception('The group-member-set property MUST be an instance of Sabre\DAV\Property\HrefList or null');
+ }
+ $node = $this->server->tree->getNodeForPath($path);
+ if (!($node instanceof IPrincipal)) {
+ // Fail
+ return false;
+ }
+
+ $node->setGroupMemberSet($memberSet);
+ // We must also clear our cache, just in case
+
+ $this->principalMembershipCache = [];
+
+ return true;
+ });
+ }
+
+ /**
+ * This method handles HTTP REPORT requests.
+ *
+ * @param string $reportName
+ * @param mixed $report
+ * @param mixed $path
+ */
+ public function report($reportName, $report, $path)
+ {
+ switch ($reportName) {
+ case '{DAV:}principal-property-search':
+ $this->server->transactionType = 'report-principal-property-search';
+ $this->principalPropertySearchReport($path, $report);
+
+ return false;
+ case '{DAV:}principal-search-property-set':
+ $this->server->transactionType = 'report-principal-search-property-set';
+ $this->principalSearchPropertySetReport($path, $report);
+
+ return false;
+ case '{DAV:}expand-property':
+ $this->server->transactionType = 'report-expand-property';
+ $this->expandPropertyReport($path, $report);
+
+ return false;
+ case '{DAV:}principal-match':
+ $this->server->transactionType = 'report-principal-match';
+ $this->principalMatchReport($path, $report);
+
+ return false;
+ case '{DAV:}acl-principal-prop-set':
+ $this->server->transactionType = 'acl-principal-prop-set';
+ $this->aclPrincipalPropSetReport($path, $report);
+
+ return false;
+ }
+ }
+
+ /**
+ * This method is responsible for handling the 'ACL' event.
+ *
+ * @return bool
+ */
+ public function httpAcl(RequestInterface $request, ResponseInterface $response)
+ {
+ $path = $request->getPath();
+ $body = $request->getBodyAsString();
+
+ if (!$body) {
+ throw new DAV\Exception\BadRequest('XML body expected in ACL request');
+ }
+
+ $acl = $this->server->xml->expect('{DAV:}acl', $body);
+ $newAcl = $acl->getPrivileges();
+
+ // Normalizing urls
+ foreach ($newAcl as $k => $newAce) {
+ $newAcl[$k]['principal'] = $this->server->calculateUri($newAce['principal']);
+ }
+ $node = $this->server->tree->getNodeForPath($path);
+
+ if (!$node instanceof IACL) {
+ throw new DAV\Exception\MethodNotAllowed('This node does not support the ACL method');
+ }
+
+ $oldAcl = $this->getACL($node);
+
+ $supportedPrivileges = $this->getFlatPrivilegeSet($node);
+
+ /* Checking if protected principals from the existing principal set are
+ not overwritten. */
+ foreach ($oldAcl as $oldAce) {
+ if (!isset($oldAce['protected']) || !$oldAce['protected']) {
+ continue;
+ }
+
+ $found = false;
+ foreach ($newAcl as $newAce) {
+ if (
+ $newAce['privilege'] === $oldAce['privilege'] &&
+ $newAce['principal'] === $oldAce['principal'] &&
+ $newAce['protected']
+ ) {
+ $found = true;
+ }
+ }
+
+ if (!$found) {
+ throw new Exception\AceConflict('This resource contained a protected {DAV:}ace, but this privilege did not occur in the ACL request');
+ }
+ }
+
+ foreach ($newAcl as $newAce) {
+ // Do we recognize the privilege
+ if (!isset($supportedPrivileges[$newAce['privilege']])) {
+ throw new Exception\NotSupportedPrivilege('The privilege you specified ('.$newAce['privilege'].') is not recognized by this server');
+ }
+
+ if ($supportedPrivileges[$newAce['privilege']]['abstract']) {
+ throw new Exception\NoAbstract('The privilege you specified ('.$newAce['privilege'].') is an abstract privilege');
+ }
+
+ // Looking up the principal
+ try {
+ $principal = $this->server->tree->getNodeForPath($newAce['principal']);
+ } catch (NotFound $e) {
+ throw new Exception\NotRecognizedPrincipal('The specified principal ('.$newAce['principal'].') does not exist');
+ }
+ if (!($principal instanceof IPrincipal)) {
+ throw new Exception\NotRecognizedPrincipal('The specified uri ('.$newAce['principal'].') is not a principal');
+ }
+ }
+ $node->setACL($newAcl);
+
+ $response->setStatus(200);
+
+ // Breaking the event chain, because we handled this method.
+ return false;
+ }
+
+ /* }}} */
+
+ /* Reports {{{ */
+
+ /**
+ * The principal-match report is defined in RFC3744, section 9.3.
+ *
+ * This report allows a client to figure out based on the current user,
+ * or a principal URL, the principal URL and principal URLs of groups that
+ * principal belongs to.
+ *
+ * @param string $path
+ */
+ protected function principalMatchReport($path, Xml\Request\PrincipalMatchReport $report)
+ {
+ $depth = $this->server->getHTTPDepth(0);
+ if (0 !== $depth) {
+ throw new BadRequest('The principal-match report is only defined on Depth: 0');
+ }
+
+ $currentPrincipals = $this->getCurrentUserPrincipals();
+
+ $result = [];
+
+ if (Xml\Request\PrincipalMatchReport::SELF === $report->type) {
+ // Finding all principals under the request uri that match the
+ // current principal.
+ foreach ($currentPrincipals as $currentPrincipal) {
+ if ($currentPrincipal === $path || 0 === strpos($currentPrincipal, $path.'/')) {
+ $result[] = $currentPrincipal;
+ }
+ }
+ } else {
+ // We need to find all resources that have a property that matches
+ // one of the current principals.
+ $candidates = $this->server->getPropertiesForPath(
+ $path,
+ [$report->principalProperty],
+ 1
+ );
+
+ foreach ($candidates as $candidate) {
+ if (!isset($candidate[200][$report->principalProperty])) {
+ continue;
+ }
+
+ $hrefs = $candidate[200][$report->principalProperty];
+
+ if (!$hrefs instanceof Href) {
+ continue;
+ }
+
+ foreach ($hrefs->getHrefs() as $href) {
+ if (in_array(trim($href, '/'), $currentPrincipals)) {
+ $result[] = $candidate['href'];
+ continue 2;
+ }
+ }
+ }
+ }
+
+ $responses = [];
+
+ foreach ($result as $item) {
+ $properties = [];
+
+ if ($report->properties) {
+ $foo = $this->server->getPropertiesForPath($item, $report->properties);
+ $foo = $foo[0];
+ $item = $foo['href'];
+ unset($foo['href']);
+ $properties = $foo;
+ }
+
+ $responses[] = new DAV\Xml\Element\Response(
+ $item,
+ $properties,
+ '200'
+ );
+ }
+
+ $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
+ $this->server->httpResponse->setStatus(207);
+ $this->server->httpResponse->setBody(
+ $this->server->xml->write(
+ '{DAV:}multistatus',
+ $responses,
+ $this->server->getBaseUri()
+ )
+ );
+ }
+
+ /**
+ * The expand-property report is defined in RFC3253 section 3.8.
+ *
+ * This report is very similar to a standard PROPFIND. The difference is
+ * that it has the additional ability to look at properties containing a
+ * {DAV:}href element, follow that property and grab additional elements
+ * there.
+ *
+ * Other rfc's, such as ACL rely on this report, so it made sense to put
+ * it in this plugin.
+ *
+ * @param string $path
+ * @param Xml\Request\ExpandPropertyReport $report
+ */
+ protected function expandPropertyReport($path, $report)
+ {
+ $depth = $this->server->getHTTPDepth(0);
+
+ $result = $this->expandProperties($path, $report->properties, $depth);
+
+ $xml = $this->server->xml->write(
+ '{DAV:}multistatus',
+ new DAV\Xml\Response\MultiStatus($result),
+ $this->server->getBaseUri()
+ );
+ $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
+ $this->server->httpResponse->setStatus(207);
+ $this->server->httpResponse->setBody($xml);
+ }
+
+ /**
+ * This method expands all the properties and returns
+ * a list with property values.
+ *
+ * @param array $path
+ * @param array $requestedProperties the list of required properties
+ * @param int $depth
+ *
+ * @return array
+ */
+ protected function expandProperties($path, array $requestedProperties, $depth)
+ {
+ $foundProperties = $this->server->getPropertiesForPath($path, array_keys($requestedProperties), $depth);
+
+ $result = [];
+
+ foreach ($foundProperties as $node) {
+ foreach ($requestedProperties as $propertyName => $childRequestedProperties) {
+ // We're only traversing if sub-properties were requested
+ if (!is_array($childRequestedProperties) || 0 === count($childRequestedProperties)) {
+ continue;
+ }
+
+ // We only have to do the expansion if the property was found
+ // and it contains an href element.
+ if (!array_key_exists($propertyName, $node[200])) {
+ continue;
+ }
+
+ if (!$node[200][$propertyName] instanceof DAV\Xml\Property\Href) {
+ continue;
+ }
+
+ $childHrefs = $node[200][$propertyName]->getHrefs();
+ $childProps = [];
+
+ foreach ($childHrefs as $href) {
+ // Gathering the result of the children
+ $childProps[] = [
+ 'name' => '{DAV:}response',
+ 'value' => $this->expandProperties($href, $childRequestedProperties, 0)[0],
+ ];
+ }
+
+ // Replacing the property with its expanded form.
+ $node[200][$propertyName] = $childProps;
+ }
+ $result[] = new DAV\Xml\Element\Response($node['href'], $node);
+ }
+
+ return $result;
+ }
+
+ /**
+ * principalSearchPropertySetReport.
+ *
+ * This method responsible for handing the
+ * {DAV:}principal-search-property-set report. This report returns a list
+ * of properties the client may search on, using the
+ * {DAV:}principal-property-search report.
+ *
+ * @param string $path
+ * @param Xml\Request\PrincipalSearchPropertySetReport $report
+ */
+ protected function principalSearchPropertySetReport($path, $report)
+ {
+ $httpDepth = $this->server->getHTTPDepth(0);
+ if (0 !== $httpDepth) {
+ throw new DAV\Exception\BadRequest('This report is only defined when Depth: 0');
+ }
+
+ $writer = $this->server->xml->getWriter();
+ $writer->openMemory();
+ $writer->startDocument();
+
+ $writer->startElement('{DAV:}principal-search-property-set');
+
+ foreach ($this->principalSearchPropertySet as $propertyName => $description) {
+ $writer->startElement('{DAV:}principal-search-property');
+ $writer->startElement('{DAV:}prop');
+
+ $writer->writeElement($propertyName);
+
+ $writer->endElement(); // prop
+
+ if ($description) {
+ $writer->write([[
+ 'name' => '{DAV:}description',
+ 'value' => $description,
+ 'attributes' => ['xml:lang' => 'en'],
+ ]]);
+ }
+
+ $writer->endElement(); // principal-search-property
+ }
+
+ $writer->endElement(); // principal-search-property-set
+
+ $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
+ $this->server->httpResponse->setStatus(200);
+ $this->server->httpResponse->setBody($writer->outputMemory());
+ }
+
+ /**
+ * principalPropertySearchReport.
+ *
+ * This method is responsible for handing the
+ * {DAV:}principal-property-search report. This report can be used for
+ * clients to search for groups of principals, based on the value of one
+ * or more properties.
+ *
+ * @param string $path
+ */
+ protected function principalPropertySearchReport($path, Xml\Request\PrincipalPropertySearchReport $report)
+ {
+ if ($report->applyToPrincipalCollectionSet) {
+ $path = null;
+ }
+ if (0 !== $this->server->getHttpDepth('0')) {
+ throw new BadRequest('Depth must be 0');
+ }
+ $result = $this->principalSearch(
+ $report->searchProperties,
+ $report->properties,
+ $path,
+ $report->test
+ );
+
+ $prefer = $this->server->getHTTPPrefer();
+
+ $this->server->httpResponse->setStatus(207);
+ $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
+ $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer');
+ $this->server->httpResponse->setBody($this->server->generateMultiStatus($result, 'minimal' === $prefer['return']));
+ }
+
+ /**
+ * aclPrincipalPropSet REPORT.
+ *
+ * This method is responsible for handling the {DAV:}acl-principal-prop-set
+ * REPORT, as defined in:
+ *
+ * https://tools.ietf.org/html/rfc3744#section-9.2
+ *
+ * This REPORT allows a user to quickly fetch information about all
+ * principals specified in the access control list. Most commonly this
+ * is used to for example generate a UI with ACL rules, allowing you
+ * to show names for principals for every entry.
+ *
+ * @param string $path
+ */
+ protected function aclPrincipalPropSetReport($path, Xml\Request\AclPrincipalPropSetReport $report)
+ {
+ if (0 !== $this->server->getHTTPDepth(0)) {
+ throw new BadRequest('The {DAV:}acl-principal-prop-set REPORT only supports Depth 0');
+ }
+
+ // Fetching ACL rules for the given path. We're using the property
+ // API and not the local getACL, because it will ensure that all
+ // business rules and restrictions are applied.
+ $acl = $this->server->getProperties($path, '{DAV:}acl');
+
+ if (!$acl || !isset($acl['{DAV:}acl'])) {
+ throw new Forbidden('Could not fetch ACL rules for this path');
+ }
+
+ $principals = [];
+ foreach ($acl['{DAV:}acl']->getPrivileges() as $ace) {
+ if ('{' === $ace['principal'][0]) {
+ // It's not a principal, it's one of the special rules such as {DAV:}authenticated
+ continue;
+ }
+
+ $principals[] = $ace['principal'];
+ }
+
+ $properties = $this->server->getPropertiesForMultiplePaths(
+ $principals,
+ $report->properties
+ );
+
+ $this->server->httpResponse->setStatus(207);
+ $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
+ $this->server->httpResponse->setBody(
+ $this->server->generateMultiStatus($properties)
+ );
+ }
+
+ /* }}} */
+
+ /**
+ * This method is used to generate HTML output for the
+ * DAV\Browser\Plugin. This allows us to generate an interface users
+ * can use to create new calendars.
+ *
+ * @param string $output
+ *
+ * @return bool
+ */
+ public function htmlActionsPanel(DAV\INode $node, &$output)
+ {
+ if (!$node instanceof PrincipalCollection) {
+ return;
+ }
+
+ $output .= '
+
';
+
+ return false;
+ }
+
+ /**
+ * Returns a bunch of meta-data about the plugin.
+ *
+ * Providing this information is optional, and is mainly displayed by the
+ * Browser plugin.
+ *
+ * The description key in the returned array may contain html and will not
+ * be sanitized.
+ *
+ * @return array
+ */
+ public function getPluginInfo()
+ {
+ return [
+ 'name' => $this->getPluginName(),
+ 'description' => 'Adds support for WebDAV ACL (rfc3744)',
+ 'link' => 'http://sabre.io/dav/acl/',
+ ];
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/Principal.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Principal.php
new file mode 100644
index 0000000..ada38ab
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/Principal.php
@@ -0,0 +1,199 @@
+principalBackend = $principalBackend;
+ $this->principalProperties = $principalProperties;
+ }
+
+ /**
+ * Returns the full principal url.
+ *
+ * @return string
+ */
+ public function getPrincipalUrl()
+ {
+ return $this->principalProperties['uri'];
+ }
+
+ /**
+ * Returns a list of alternative urls for a principal.
+ *
+ * This can for example be an email address, or ldap url.
+ *
+ * @return array
+ */
+ public function getAlternateUriSet()
+ {
+ $uris = [];
+ if (isset($this->principalProperties['{DAV:}alternate-URI-set'])) {
+ $uris = $this->principalProperties['{DAV:}alternate-URI-set'];
+ }
+
+ if (isset($this->principalProperties['{http://sabredav.org/ns}email-address'])) {
+ $uris[] = 'mailto:'.$this->principalProperties['{http://sabredav.org/ns}email-address'];
+ }
+
+ return array_unique($uris);
+ }
+
+ /**
+ * Returns the list of group members.
+ *
+ * If this principal is a group, this function should return
+ * all member principal uri's for the group.
+ *
+ * @return array
+ */
+ public function getGroupMemberSet()
+ {
+ return $this->principalBackend->getGroupMemberSet($this->principalProperties['uri']);
+ }
+
+ /**
+ * Returns the list of groups this principal is member of.
+ *
+ * If this principal is a member of a (list of) groups, this function
+ * should return a list of principal uri's for it's members.
+ *
+ * @return array
+ */
+ public function getGroupMembership()
+ {
+ return $this->principalBackend->getGroupMemberShip($this->principalProperties['uri']);
+ }
+
+ /**
+ * Sets a list of group members.
+ *
+ * If this principal is a group, this method sets all the group members.
+ * The list of members is always overwritten, never appended to.
+ *
+ * This method should throw an exception if the members could not be set.
+ */
+ public function setGroupMemberSet(array $groupMembers)
+ {
+ $this->principalBackend->setGroupMemberSet($this->principalProperties['uri'], $groupMembers);
+ }
+
+ /**
+ * Returns this principals name.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ $uri = $this->principalProperties['uri'];
+ list(, $name) = Uri\split($uri);
+
+ return $name;
+ }
+
+ /**
+ * Returns the name of the user.
+ *
+ * @return string
+ */
+ public function getDisplayName()
+ {
+ if (isset($this->principalProperties['{DAV:}displayname'])) {
+ return $this->principalProperties['{DAV:}displayname'];
+ } else {
+ return $this->getName();
+ }
+ }
+
+ /**
+ * Returns a list of properties.
+ *
+ * @param array $requestedProperties
+ *
+ * @return array
+ */
+ public function getProperties($requestedProperties)
+ {
+ $newProperties = [];
+ foreach ($requestedProperties as $propName) {
+ if (isset($this->principalProperties[$propName])) {
+ $newProperties[$propName] = $this->principalProperties[$propName];
+ }
+ }
+
+ return $newProperties;
+ }
+
+ /**
+ * Updates properties on this node.
+ *
+ * This method received a PropPatch object, which contains all the
+ * information about the update.
+ *
+ * To update specific properties, call the 'handle' method on this object.
+ * Read the PropPatch documentation for more information.
+ */
+ public function propPatch(DAV\PropPatch $propPatch)
+ {
+ return $this->principalBackend->updatePrincipal(
+ $this->principalProperties['uri'],
+ $propPatch
+ );
+ }
+
+ /**
+ * Returns the owner principal.
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner()
+ {
+ return $this->principalProperties['uri'];
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/AbstractBackend.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/AbstractBackend.php
new file mode 100644
index 0000000..f61a0c9
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/AbstractBackend.php
@@ -0,0 +1,54 @@
+searchPrincipals(
+ $principalPrefix,
+ ['{http://sabredav.org/ns}email-address' => substr($uri, 7)]
+ );
+
+ if ($result) {
+ return $result[0];
+ }
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/BackendInterface.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/BackendInterface.php
new file mode 100644
index 0000000..2fd3191
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/BackendInterface.php
@@ -0,0 +1,143 @@
+ [
+ 'dbField' => 'displayname',
+ ],
+
+ /*
+ * This is the users' primary email-address.
+ */
+ '{http://sabredav.org/ns}email-address' => [
+ 'dbField' => 'email',
+ ],
+ ];
+
+ /**
+ * Sets up the backend.
+ */
+ public function __construct(\PDO $pdo)
+ {
+ $this->pdo = $pdo;
+ }
+
+ /**
+ * Returns a list of principals based on a prefix.
+ *
+ * This prefix will often contain something like 'principals'. You are only
+ * expected to return principals that are in this base path.
+ *
+ * You are expected to return at least a 'uri' for every user, you can
+ * return any additional properties if you wish so. Common properties are:
+ * {DAV:}displayname
+ * {http://sabredav.org/ns}email-address - This is a custom SabreDAV
+ * field that's actually injected in a number of other properties. If
+ * you have an email address, use this property.
+ *
+ * @param string $prefixPath
+ *
+ * @return array
+ */
+ public function getPrincipalsByPrefix($prefixPath)
+ {
+ $fields = [
+ 'uri',
+ ];
+
+ foreach ($this->fieldMap as $key => $value) {
+ $fields[] = $value['dbField'];
+ }
+ $result = $this->pdo->query('SELECT '.implode(',', $fields).' FROM '.$this->tableName);
+
+ $principals = [];
+
+ while ($row = $result->fetch(\PDO::FETCH_ASSOC)) {
+ // Checking if the principal is in the prefix
+ list($rowPrefix) = Uri\split($row['uri']);
+ if ($rowPrefix !== $prefixPath) {
+ continue;
+ }
+
+ $principal = [
+ 'uri' => $row['uri'],
+ ];
+ foreach ($this->fieldMap as $key => $value) {
+ if ($row[$value['dbField']]) {
+ $principal[$key] = $row[$value['dbField']];
+ }
+ }
+ $principals[] = $principal;
+ }
+
+ return $principals;
+ }
+
+ /**
+ * Returns a specific principal, specified by it's path.
+ * The returned structure should be the exact same as from
+ * getPrincipalsByPrefix.
+ *
+ * @param string $path
+ *
+ * @return array
+ */
+ public function getPrincipalByPath($path)
+ {
+ $fields = [
+ 'id',
+ 'uri',
+ ];
+
+ foreach ($this->fieldMap as $key => $value) {
+ $fields[] = $value['dbField'];
+ }
+ $stmt = $this->pdo->prepare('SELECT '.implode(',', $fields).' FROM '.$this->tableName.' WHERE uri = ?');
+ $stmt->execute([$path]);
+
+ $row = $stmt->fetch(\PDO::FETCH_ASSOC);
+ if (!$row) {
+ return;
+ }
+
+ $principal = [
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ ];
+ foreach ($this->fieldMap as $key => $value) {
+ if ($row[$value['dbField']]) {
+ $principal[$key] = $row[$value['dbField']];
+ }
+ }
+
+ return $principal;
+ }
+
+ /**
+ * Updates one ore more webdav properties on a principal.
+ *
+ * The list of mutations is stored in a Sabre\DAV\PropPatch object.
+ * To do the actual updates, you must tell this object which properties
+ * you're going to process with the handle() method.
+ *
+ * Calling the handle method is like telling the PropPatch object "I
+ * promise I can handle updating this property".
+ *
+ * Read the PropPatch documentation for more info and examples.
+ *
+ * @param string $path
+ */
+ public function updatePrincipal($path, DAV\PropPatch $propPatch)
+ {
+ $propPatch->handle(array_keys($this->fieldMap), function ($properties) use ($path) {
+ $query = 'UPDATE '.$this->tableName.' SET ';
+ $first = true;
+
+ $values = [];
+
+ foreach ($properties as $key => $value) {
+ $dbField = $this->fieldMap[$key]['dbField'];
+
+ if (!$first) {
+ $query .= ', ';
+ }
+ $first = false;
+ $query .= $dbField.' = :'.$dbField;
+ $values[$dbField] = $value;
+ }
+
+ $query .= ' WHERE uri = :uri';
+ $values['uri'] = $path;
+
+ $stmt = $this->pdo->prepare($query);
+ $stmt->execute($values);
+
+ return true;
+ });
+ }
+
+ /**
+ * This method is used to search for principals matching a set of
+ * properties.
+ *
+ * This search is specifically used by RFC3744's principal-property-search
+ * REPORT.
+ *
+ * The actual search should be a unicode-non-case-sensitive search. The
+ * keys in searchProperties are the WebDAV property names, while the values
+ * are the property values to search on.
+ *
+ * By default, if multiple properties are submitted to this method, the
+ * various properties should be combined with 'AND'. If $test is set to
+ * 'anyof', it should be combined using 'OR'.
+ *
+ * This method should simply return an array with full principal uri's.
+ *
+ * If somebody attempted to search on a property the backend does not
+ * support, you should simply return 0 results.
+ *
+ * You can also just return 0 results if you choose to not support
+ * searching at all, but keep in mind that this may stop certain features
+ * from working.
+ *
+ * @param string $prefixPath
+ * @param string $test
+ *
+ * @return array
+ */
+ public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof')
+ {
+ if (0 == count($searchProperties)) {
+ return [];
+ } //No criteria
+
+ $query = 'SELECT uri FROM '.$this->tableName.' WHERE ';
+ $values = [];
+ foreach ($searchProperties as $property => $value) {
+ switch ($property) {
+ case '{DAV:}displayname':
+ $column = 'displayname';
+ break;
+ case '{http://sabredav.org/ns}email-address':
+ $column = 'email';
+ break;
+ default:
+ // Unsupported property
+ return [];
+ }
+ if (count($values) > 0) {
+ $query .= (0 == strcmp($test, 'anyof') ? ' OR ' : ' AND ');
+ }
+ $query .= 'lower('.$column.') LIKE lower(?)';
+ $values[] = '%'.$value.'%';
+ }
+ $stmt = $this->pdo->prepare($query);
+ $stmt->execute($values);
+
+ $principals = [];
+ while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ // Checking if the principal is in the prefix
+ list($rowPrefix) = Uri\split($row['uri']);
+ if ($rowPrefix !== $prefixPath) {
+ continue;
+ }
+
+ $principals[] = $row['uri'];
+ }
+
+ return $principals;
+ }
+
+ /**
+ * Finds a principal by its URI.
+ *
+ * This method may receive any type of uri, but mailto: addresses will be
+ * the most common.
+ *
+ * Implementation of this API is optional. It is currently used by the
+ * CalDAV system to find principals based on their email addresses. If this
+ * API is not implemented, some features may not work correctly.
+ *
+ * This method must return a relative principal path, or null, if the
+ * principal was not found or you refuse to find it.
+ *
+ * @param string $uri
+ * @param string $principalPrefix
+ *
+ * @return string
+ */
+ public function findByUri($uri, $principalPrefix)
+ {
+ $uriParts = Uri\parse($uri);
+
+ // Only two types of uri are supported :
+ // - the "mailto:" scheme with some non-empty address
+ // - a principals uri, in the form "principals/NAME"
+ // In both cases, `path` must not be empty.
+ if (empty($uriParts['path'])) {
+ return null;
+ }
+
+ $uri = null;
+ if ('mailto' === $uriParts['scheme']) {
+ $query = 'SELECT uri FROM '.$this->tableName.' WHERE lower(email)=lower(?)';
+ $stmt = $this->pdo->prepare($query);
+ $stmt->execute([$uriParts['path']]);
+
+ while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ // Checking if the principal is in the prefix
+ list($rowPrefix) = Uri\split($row['uri']);
+ if ($rowPrefix !== $principalPrefix) {
+ continue;
+ }
+
+ $uri = $row['uri'];
+ break; //Stop on first match
+ }
+ } else {
+ $pathParts = Uri\split($uriParts['path']); // We can do this since $uriParts['path'] is not null
+
+ if (2 === count($pathParts) && $pathParts[0] === $principalPrefix) {
+ // Checking that this uri exists
+ $query = 'SELECT * FROM '.$this->tableName.' WHERE uri = ?';
+ $stmt = $this->pdo->prepare($query);
+ $stmt->execute([$uriParts['path']]);
+ $rows = $stmt->fetchAll();
+
+ if (count($rows) > 0) {
+ $uri = $uriParts['path'];
+ }
+ }
+ }
+
+ return $uri;
+ }
+
+ /**
+ * Returns the list of members for a group-principal.
+ *
+ * @param string $principal
+ *
+ * @return array
+ */
+ public function getGroupMemberSet($principal)
+ {
+ $principal = $this->getPrincipalByPath($principal);
+ if (!$principal) {
+ throw new DAV\Exception('Principal not found');
+ }
+ $stmt = $this->pdo->prepare('SELECT principals.uri as uri FROM '.$this->groupMembersTableName.' AS groupmembers LEFT JOIN '.$this->tableName.' AS principals ON groupmembers.member_id = principals.id WHERE groupmembers.principal_id = ?');
+ $stmt->execute([$principal['id']]);
+
+ $result = [];
+ while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $result[] = $row['uri'];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns the list of groups a principal is a member of.
+ *
+ * @param string $principal
+ *
+ * @return array
+ */
+ public function getGroupMembership($principal)
+ {
+ $principal = $this->getPrincipalByPath($principal);
+ if (!$principal) {
+ throw new DAV\Exception('Principal not found');
+ }
+ $stmt = $this->pdo->prepare('SELECT principals.uri as uri FROM '.$this->groupMembersTableName.' AS groupmembers LEFT JOIN '.$this->tableName.' AS principals ON groupmembers.principal_id = principals.id WHERE groupmembers.member_id = ?');
+ $stmt->execute([$principal['id']]);
+
+ $result = [];
+ while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $result[] = $row['uri'];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Updates the list of group members for a group principal.
+ *
+ * The principals should be passed as a list of uri's.
+ *
+ * @param string $principal
+ */
+ public function setGroupMemberSet($principal, array $members)
+ {
+ // Grabbing the list of principal id's.
+ $stmt = $this->pdo->prepare('SELECT id, uri FROM '.$this->tableName.' WHERE uri IN (? '.str_repeat(', ? ', count($members)).');');
+ $stmt->execute(array_merge([$principal], $members));
+
+ $memberIds = [];
+ $principalId = null;
+
+ while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ if ($row['uri'] == $principal) {
+ $principalId = $row['id'];
+ } else {
+ $memberIds[] = $row['id'];
+ }
+ }
+ if (!$principalId) {
+ throw new DAV\Exception('Principal not found');
+ }
+ // Wiping out old members
+ $stmt = $this->pdo->prepare('DELETE FROM '.$this->groupMembersTableName.' WHERE principal_id = ?;');
+ $stmt->execute([$principalId]);
+
+ foreach ($memberIds as $memberId) {
+ $stmt = $this->pdo->prepare('INSERT INTO '.$this->groupMembersTableName.' (principal_id, member_id) VALUES (?, ?);');
+ $stmt->execute([$principalId, $memberId]);
+ }
+ }
+
+ /**
+ * Creates a new principal.
+ *
+ * This method receives a full path for the new principal. The mkCol object
+ * contains any additional webdav properties specified during the creation
+ * of the principal.
+ *
+ * @param string $path
+ */
+ public function createPrincipal($path, MkCol $mkCol)
+ {
+ $stmt = $this->pdo->prepare('INSERT INTO '.$this->tableName.' (uri) VALUES (?)');
+ $stmt->execute([$path]);
+ $this->updatePrincipal($path, $mkCol);
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/PrincipalCollection.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/PrincipalCollection.php
new file mode 100644
index 0000000..b823b6c
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/PrincipalCollection.php
@@ -0,0 +1,96 @@
+principalBackend, $principal);
+ }
+
+ /**
+ * Creates a new collection.
+ *
+ * This method will receive a MkCol object with all the information about
+ * the new collection that's being created.
+ *
+ * The MkCol object contains information about the resourceType of the new
+ * collection. If you don't support the specified resourceType, you should
+ * throw Exception\InvalidResourceType.
+ *
+ * The object also contains a list of WebDAV properties for the new
+ * collection.
+ *
+ * You should call the handle() method on this object to specify exactly
+ * which properties you are storing. This allows the system to figure out
+ * exactly which properties you didn't store, which in turn allows other
+ * plugins (such as the propertystorage plugin) to handle storing the
+ * property for you.
+ *
+ * @param string $name
+ *
+ * @throws InvalidResourceType
+ */
+ public function createExtendedCollection($name, MkCol $mkCol)
+ {
+ if (!$mkCol->hasResourceType('{DAV:}principal')) {
+ throw new InvalidResourceType('Only resources of type {DAV:}principal may be created here');
+ }
+
+ $this->principalBackend->createPrincipal(
+ $this->principalPrefix.'/'.$name,
+ $mkCol
+ );
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ public function getACL()
+ {
+ return [
+ [
+ 'principal' => '{DAV:}authenticated',
+ 'privilege' => '{DAV:}read',
+ 'protected' => true,
+ ],
+ ];
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/Acl.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/Acl.php
new file mode 100644
index 0000000..c6e236d
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/Acl.php
@@ -0,0 +1,257 @@
+privileges = $privileges;
+ $this->prefixBaseUrl = $prefixBaseUrl;
+ }
+
+ /**
+ * Returns the list of privileges for this property.
+ *
+ * @return array
+ */
+ public function getPrivileges()
+ {
+ return $this->privileges;
+ }
+
+ /**
+ * 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)
+ {
+ foreach ($this->privileges as $ace) {
+ $this->serializeAce($writer, $ace);
+ }
+ }
+
+ /**
+ * Generate html representation for this value.
+ *
+ * The html output is 100% trusted, and no effort is being made to sanitize
+ * it. It's up to the implementor to sanitize user provided values.
+ *
+ * The output must be in UTF-8.
+ *
+ * The baseUri parameter is a url to the root of the application, and can
+ * be used to construct local links.
+ *
+ * @return string
+ */
+ public function toHtml(HtmlOutputHelper $html)
+ {
+ ob_start();
+ echo '
';
+ echo '
Principal
Privilege
';
+ foreach ($this->privileges as $privilege) {
+ echo '
';
+ // if it starts with a {, it's a special principal
+ if ('{' === $privilege['principal'][0]) {
+ echo '
';
+
+ return ob_get_clean();
+ }
+
+ /**
+ * 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 mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $elementMap = [
+ '{DAV:}ace' => 'Sabre\Xml\Element\KeyValue',
+ '{DAV:}privilege' => 'Sabre\Xml\Element\Elements',
+ '{DAV:}principal' => 'Sabre\DAVACL\Xml\Property\Principal',
+ ];
+
+ $privileges = [];
+
+ foreach ((array) $reader->parseInnerTree($elementMap) as $element) {
+ if ('{DAV:}ace' !== $element['name']) {
+ continue;
+ }
+ $ace = $element['value'];
+
+ if (empty($ace['{DAV:}principal'])) {
+ throw new DAV\Exception\BadRequest('Each {DAV:}ace element must have one {DAV:}principal element');
+ }
+ $principal = $ace['{DAV:}principal'];
+
+ switch ($principal->getType()) {
+ case Principal::HREF:
+ $principal = $principal->getHref();
+ break;
+ case Principal::AUTHENTICATED:
+ $principal = '{DAV:}authenticated';
+ break;
+ case Principal::UNAUTHENTICATED:
+ $principal = '{DAV:}unauthenticated';
+ break;
+ case Principal::ALL:
+ $principal = '{DAV:}all';
+ break;
+ }
+
+ $protected = array_key_exists('{DAV:}protected', $ace);
+
+ if (!isset($ace['{DAV:}grant'])) {
+ throw new DAV\Exception\NotImplemented('Every {DAV:}ace element must have a {DAV:}grant element. {DAV:}deny is not yet supported');
+ }
+ foreach ($ace['{DAV:}grant'] as $elem) {
+ if ('{DAV:}privilege' !== $elem['name']) {
+ continue;
+ }
+
+ foreach ($elem['value'] as $priv) {
+ $privileges[] = [
+ 'principal' => $principal,
+ 'protected' => $protected,
+ 'privilege' => $priv,
+ ];
+ }
+ }
+ }
+
+ return new self($privileges);
+ }
+
+ /**
+ * Serializes a single access control entry.
+ */
+ private function serializeAce(Writer $writer, array $ace)
+ {
+ $writer->startElement('{DAV:}ace');
+
+ switch ($ace['principal']) {
+ case '{DAV:}authenticated':
+ $principal = new Principal(Principal::AUTHENTICATED);
+ break;
+ case '{DAV:}unauthenticated':
+ $principal = new Principal(Principal::UNAUTHENTICATED);
+ break;
+ case '{DAV:}all':
+ $principal = new Principal(Principal::ALL);
+ break;
+ default:
+ $principal = new Principal(Principal::HREF, $ace['principal']);
+ break;
+ }
+
+ $writer->writeElement('{DAV:}principal', $principal);
+ $writer->startElement('{DAV:}grant');
+ $writer->startElement('{DAV:}privilege');
+
+ $writer->writeElement($ace['privilege']);
+
+ $writer->endElement(); // privilege
+ $writer->endElement(); // grant
+
+ if (!empty($ace['protected'])) {
+ $writer->writeElement('{DAV:}protected');
+ }
+
+ $writer->endElement(); // ace
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/AclRestrictions.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/AclRestrictions.php
new file mode 100644
index 0000000..b5629c8
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/AclRestrictions.php
@@ -0,0 +1,42 @@
+writeElement('{DAV:}grant-only');
+ $writer->writeElement('{DAV:}no-invert');
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/CurrentUserPrivilegeSet.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/CurrentUserPrivilegeSet.php
new file mode 100644
index 0000000..e38a45c
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/CurrentUserPrivilegeSet.php
@@ -0,0 +1,145 @@
+privileges = $privileges;
+ }
+
+ /**
+ * 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)
+ {
+ foreach ($this->privileges as $privName) {
+ $writer->startElement('{DAV:}privilege');
+ $writer->writeElement($privName);
+ $writer->endElement();
+ }
+ }
+
+ /**
+ * Returns true or false, whether the specified principal appears in the
+ * list.
+ *
+ * @param string $privilegeName
+ *
+ * @return bool
+ */
+ public function has($privilegeName)
+ {
+ return in_array($privilegeName, $this->privileges);
+ }
+
+ /**
+ * Returns the list of privileges.
+ *
+ * @return array
+ */
+ public function getValue()
+ {
+ return $this->privileges;
+ }
+
+ /**
+ * 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.
+ *
+ * @return mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $result = [];
+
+ $tree = $reader->parseInnerTree(['{DAV:}privilege' => 'Sabre\\Xml\\Element\\Elements']);
+ foreach ($tree as $element) {
+ if ('{DAV:}privilege' !== $element['name']) {
+ continue;
+ }
+ $result[] = $element['value'][0];
+ }
+
+ return new self($result);
+ }
+
+ /**
+ * Generate html representation for this value.
+ *
+ * The html output is 100% trusted, and no effort is being made to sanitize
+ * it. It's up to the implementor to sanitize user provided values.
+ *
+ * The output must be in UTF-8.
+ *
+ * The baseUri parameter is a url to the root of the application, and can
+ * be used to construct local links.
+ *
+ * @return string
+ */
+ public function toHtml(HtmlOutputHelper $html)
+ {
+ return implode(
+ ', ',
+ array_map([$html, 'xmlName'], $this->getValue())
+ );
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/Principal.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/Principal.php
new file mode 100644
index 0000000..5b9ee45
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/Principal.php
@@ -0,0 +1,186 @@
+type = $type;
+ if (self::HREF === $type && is_null($href)) {
+ throw new DAV\Exception('The href argument must be specified for the HREF principal type.');
+ }
+ if ($href) {
+ $href = rtrim($href, '/').'/';
+ parent::__construct($href);
+ }
+ }
+
+ /**
+ * Returns the principal type.
+ *
+ * @return int
+ */
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ /**
+ * 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)
+ {
+ switch ($this->type) {
+ case self::UNAUTHENTICATED:
+ $writer->writeElement('{DAV:}unauthenticated');
+ break;
+ case self::AUTHENTICATED:
+ $writer->writeElement('{DAV:}authenticated');
+ break;
+ case self::HREF:
+ parent::xmlSerialize($writer);
+ break;
+ case self::ALL:
+ $writer->writeElement('{DAV:}all');
+ break;
+ }
+ }
+
+ /**
+ * Generate html representation for this value.
+ *
+ * The html output is 100% trusted, and no effort is being made to sanitize
+ * it. It's up to the implementor to sanitize user provided values.
+ *
+ * The output must be in UTF-8.
+ *
+ * The baseUri parameter is a url to the root of the application, and can
+ * be used to construct local links.
+ *
+ * @return string
+ */
+ public function toHtml(HtmlOutputHelper $html)
+ {
+ switch ($this->type) {
+ case self::UNAUTHENTICATED:
+ return 'unauthenticated';
+ case self::AUTHENTICATED:
+ return 'authenticated';
+ case self::HREF:
+ return parent::toHtml($html);
+ case self::ALL:
+ return 'all';
+ }
+
+ return 'unknown';
+ }
+
+ /**
+ * 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 mixed
+ */
+ public static function xmlDeserialize(Reader $reader)
+ {
+ $tree = $reader->parseInnerTree()[0];
+
+ switch ($tree['name']) {
+ case '{DAV:}unauthenticated':
+ return new self(self::UNAUTHENTICATED);
+ case '{DAV:}authenticated':
+ return new self(self::AUTHENTICATED);
+ case '{DAV:}href':
+ return new self(self::HREF, $tree['value']);
+ case '{DAV:}all':
+ return new self(self::ALL);
+ default:
+ throw new BadRequest('Unknown or unsupported principal type: '.$tree['name']);
+ }
+ }
+}
diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php
new file mode 100644
index 0000000..6e7514b
--- /dev/null
+++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php
@@ -0,0 +1,146 @@
+privileges = $privileges;
+ }
+
+ /**
+ * Returns the privilege value.
+ *
+ * @return array
+ */
+ public function getValue()
+ {
+ return $this->privileges;
+ }
+
+ /**
+ * 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)
+ {
+ $this->serializePriv($writer, '{DAV:}all', ['aggregates' => $this->privileges]);
+ }
+
+ /**
+ * Generate html representation for this value.
+ *
+ * The html output is 100% trusted, and no effort is being made to sanitize
+ * it. It's up to the implementor to sanitize user provided values.
+ *
+ * The output must be in UTF-8.
+ *
+ * The baseUri parameter is a url to the root of the application, and can
+ * be used to construct local links.
+ *
+ * @return string
+ */
+ public function toHtml(HtmlOutputHelper $html)
+ {
+ $traverse = function ($privName, $priv) use (&$traverse, $html) {
+ echo '