diff --git a/.github/workflows/renovate.yml b/.github/workflows/renovate.yml new file mode 100644 index 0000000..575d87d --- /dev/null +++ b/.github/workflows/renovate.yml @@ -0,0 +1,41 @@ +name: Renovate + +on: + schedule: + - cron: "0 3 * * *" + workflow_dispatch: + +jobs: + renovate: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6.0.2 + + - name: Set up Node.js + uses: actions/setup-node@v6.2.0 + with: + node-version: 24 + cache: npm + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.5' + tools: composer:v2 + + - name: Install Renovate + run: | + npm install --global --no-audit --fund=false \ + --prefix "${{ runner.temp }}/renovate-npm" \ + --cache "${{ runner.temp }}/renovate-npm-cache" \ + renovate + "${{ runner.temp }}/renovate-npm/bin/renovate" --version + + - name: Run Renovate + env: + RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }} + RENOVATE_PLATFORM: gitea + RENOVATE_ENDPOINT: https://git.ktrix.dev/api/v1 + run: | + "${{ runner.temp }}/renovate-npm/bin/renovate" ${{ gitea.repository }} \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..5f55b6b --- /dev/null +++ b/composer.json @@ -0,0 +1,15 @@ +{ + "name": "ktrix/service-dav", + "description": "WebDAV / CalDAV / CardDAV service module for Ktrix", + "type": "library", + "license": "AGPL-3.0-or-later", + "require": { + "php": ">=8.1", + "sabre/dav": "^4.7" + }, + "config": { + "vendor-dir": "lib/vendor", + "optimize-autoloader": true, + "sort-packages": true + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..a65d918 --- /dev/null +++ b/composer.lock @@ -0,0 +1,516 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "ad6d240cf2a7ee11decdfcd3995cf6f0", + "packages": [ + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "sabre/dav", + "version": "4.7.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/dav.git", + "reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/dav/zipball/074373bcd689a30bcf5aaa6bbb20a3395964ce7a", + "reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-dom": "*", + "ext-iconv": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "ext-spl": "*", + "lib-libxml": ">=2.7.0", + "php": "^7.1.0 || ^8.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "sabre/event": "^5.0", + "sabre/http": "^5.0.5", + "sabre/uri": "^2.0", + "sabre/vobject": "^4.2.1", + "sabre/xml": "^2.0.1" + }, + "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-imap": "*", + "ext-pdo": "*" + }, + "bin": [ + "bin/sabredav", + "bin/naturalselection" + ], + "type": "library", + "autoload": { + "psr-4": { + "Sabre\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "WebDAV Framework for PHP", + "homepage": "http://sabre.io/", + "keywords": [ + "CalDAV", + "CardDAV", + "WebDAV", + "framework", + "iCalendar" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/dav/issues", + "source": "https://github.com/fruux/sabre-dav" + }, + "time": "2024-10-29T11:46:02+00:00" + }, + { + "name": "sabre/event", + "version": "5.1.7", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/event.git", + "reference": "86d57e305c272898ba3c28e9bd3d65d5464587c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/event/zipball/86d57e305c272898ba3c28e9bd3d65d5464587c2", + "reference": "86d57e305c272898ba3c28e9bd3d65d5464587c2", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1||^3.63", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "type": "library", + "autoload": { + "files": [ + "lib/coroutine.php", + "lib/Loop/functions.php", + "lib/Promise/functions.php" + ], + "psr-4": { + "Sabre\\Event\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "sabre/event is a library for lightweight event-based programming", + "homepage": "http://sabre.io/event/", + "keywords": [ + "EventEmitter", + "async", + "coroutine", + "eventloop", + "events", + "hooks", + "plugin", + "promise", + "reactor", + "signal" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/event/issues", + "source": "https://github.com/fruux/sabre-event" + }, + "time": "2024-08-27T11:23:05+00:00" + }, + { + "name": "sabre/http", + "version": "5.1.13", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/http.git", + "reference": "7c2a14097d1a0de2347dcbdc91a02f38e338f4db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/http/zipball/7c2a14097d1a0de2347dcbdc91a02f38e338f4db", + "reference": "7c2a14097d1a0de2347dcbdc91a02f38e338f4db", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-curl": "*", + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabre/event": ">=4.0 <6.0", + "sabre/uri": "^2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1||3.63.2", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "suggest": { + "ext-curl": " to make http requests with the Client class" + }, + "type": "library", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Sabre\\HTTP\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "The sabre/http library provides utilities for dealing with http requests and responses. ", + "homepage": "https://github.com/fruux/sabre-http", + "keywords": [ + "http" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/http/issues", + "source": "https://github.com/fruux/sabre-http" + }, + "time": "2025-09-09T10:21:47+00:00" + }, + { + "name": "sabre/uri", + "version": "2.3.4", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/uri.git", + "reference": "b76524c22de90d80ca73143680a8e77b1266c291" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/uri/zipball/b76524c22de90d80ca73143680a8e77b1266c291", + "reference": "b76524c22de90d80ca73143680a8e77b1266c291", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.63", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^1.12", + "phpstan/phpstan-phpunit": "^1.4", + "phpstan/phpstan-strict-rules": "^1.6", + "phpunit/phpunit": "^9.6" + }, + "type": "library", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Sabre\\Uri\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "Functions for making sense out of URIs.", + "homepage": "http://sabre.io/uri/", + "keywords": [ + "rfc3986", + "uri", + "url" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/uri/issues", + "source": "https://github.com/fruux/sabre-uri" + }, + "time": "2024-08-27T12:18:16+00:00" + }, + { + "name": "sabre/vobject", + "version": "4.5.8", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/vobject.git", + "reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/vobject/zipball/d554eb24d64232922e1eab5896cc2f84b3b9ffb1", + "reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabre/xml": "^2.1 || ^3.0 || ^4.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1", + "phpstan/phpstan": "^0.12 || ^1.12 || ^2.0", + "phpunit/php-invoker": "^2.0 || ^3.1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "suggest": { + "hoa/bench": "If you would like to run the benchmark scripts" + }, + "bin": [ + "bin/vobject", + "bin/generate_vcards" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Sabre\\VObject\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + }, + { + "name": "Dominik Tobschall", + "email": "dominik@fruux.com", + "homepage": "http://tobschall.de/", + "role": "Developer" + }, + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net", + "homepage": "http://mnt.io/", + "role": "Developer" + } + ], + "description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects", + "homepage": "http://sabre.io/vobject/", + "keywords": [ + "availability", + "freebusy", + "iCalendar", + "ical", + "ics", + "jCal", + "jCard", + "recurrence", + "rfc2425", + "rfc2426", + "rfc2739", + "rfc4770", + "rfc5545", + "rfc5546", + "rfc6321", + "rfc6350", + "rfc6351", + "rfc6474", + "rfc6638", + "rfc6715", + "rfc6868", + "vCalendar", + "vCard", + "vcf", + "xCal", + "xCard" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/vobject/issues", + "source": "https://github.com/fruux/sabre-vobject" + }, + "time": "2026-01-12T10:45:19+00:00" + }, + { + "name": "sabre/xml", + "version": "2.2.11", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/xml.git", + "reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/xml/zipball/01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc", + "reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "lib-libxml": ">=2.6.20", + "php": "^7.1 || ^8.0", + "sabre/uri": ">=1.0,<3.0.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1||3.63.2", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "type": "library", + "autoload": { + "files": [ + "lib/Deserializer/functions.php", + "lib/Serializer/functions.php" + ], + "psr-4": { + "Sabre\\Xml\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + }, + { + "name": "Markus Staab", + "email": "markus.staab@redaxo.de", + "role": "Developer" + } + ], + "description": "sabre/xml is an XML library that you may not hate.", + "homepage": "https://sabre.io/xml/", + "keywords": [ + "XMLReader", + "XMLWriter", + "dom", + "xml" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/xml/issues", + "source": "https://github.com/fruux/sabre-xml" + }, + "time": "2024-09-06T07:37:46+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.1" + }, + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/lib/Auth/DavAuthBackend.php b/lib/Auth/DavAuthBackend.php new file mode 100644 index 0000000..7077538 --- /dev/null +++ b/lib/Auth/DavAuthBackend.php @@ -0,0 +1,45 @@ +realm = $realm; + } + + public function check(RequestInterface $request, ResponseInterface $response): array + { + if ($this->identity->identity() !== null) { + $userId = $this->identity->identifier(); + return [true, self::PRINCIPAL_PREFIX . $userId]; + } + + return parent::check($request, $response); + } + + protected function validateUserPass($username, $password): bool + { + $user = $this->securityService->authenticatePassword($username, $password); + if ($user === null) { + return false; + } + + $this->identity->initialize($user, false); + return true; + } +} diff --git a/lib/Backends/CalDavBackend.php b/lib/Backends/CalDavBackend.php new file mode 100644 index 0000000..7ad574b --- /dev/null +++ b/lib/Backends/CalDavBackend.php @@ -0,0 +1,325 @@ + Collection (within a provider service) + * Calendar object <=> Entity (VEVENT / VTODO) + * + * Calendar identifiers use the composite key "{providerId}:{serviceId}:{collectionId}". + * Object URIs are client-chosen names (e.g. "event-uid.ics") mapped to provider + * entity identifiers via DavUriMap. + */ +class CalDavBackend extends AbstractBackend +{ + private const ID_SEP = ':'; + + public function __construct( + private readonly ChronoManager $manager, + private readonly SessionIdentity $identity, + private readonly SessionTenant $tenant, + private readonly DavUriMap $uriMap, + ) {} + + public function getCalendarsForUser($principalUri): array + { + $result = []; + + $allCollections = $this->manager->collectionList($this->tenant->identifier(), $this->identity->identifier()); + + foreach ($allCollections as $providerId => $providerServices) { + foreach ($providerServices as $serviceId => $collections) { + foreach ($collections as $collectionId => $collection) { + if (!is_object($collection)) { + continue; + } + $props = $collection->getProperties(); + $label = $props->getLabel(); + $calId = implode(self::ID_SEP, [(string) $providerId, (string) $serviceId, (string) $collectionId]); + + $result[] = [ + DavProperties::ID => $calId, + DavProperties::URI => $this->slugify($label), + DavProperties::DISPLAYNAME => $label, + DavProperties::CALDAV_DESCRIPTION => $props->getDescription() ?? '', + DavProperties::APPLE_COLOR => $props->getColor() ?? '#3a87ad', + DavProperties::CALDAV_COMPONENTS => new SupportedCalendarComponentSet(['VEVENT', 'VTODO']), + DavProperties::CS_CTAG => md5($calId . (string) ($collection->modified()?->getTimestamp() ?? 0)), + DavProperties::SYNC_TOKEN => '1', + DavProperties::PRINCIPAL_URI => $principalUri, + ]; + } + } + } + + return $result; + } + + public function createCalendar($principalUri, $calendarUri, array $properties): string + { + throw new \Sabre\DAV\Exception\Forbidden('Calendar creation is not supported via DAV'); + } + + public function deleteCalendar($calendarId): void + { + throw new \Sabre\DAV\Exception\Forbidden('Calendar deletion is not supported via DAV'); + } + + public function getCalendarObjects($calendarId): array + { + [$providerId, $serviceId, $collectionId] = $this->parseId($calendarId); + $tenantId = $this->tenant->identifier(); + $userId = $this->identity->identifier(); + + $targets = ResourceIdentifiers::fromArray([$calendarId]); + $all = $this->manager->entityListBulk($tenantId, $userId, $targets); + $entities = $all[(string) $providerId][(string) $serviceId][(string) $collectionId] ?? []; + + $mapId = $this->mapId($tenantId, $userId, $calendarId); + $objects = []; + foreach ($entities as $entity) { + if (!is_object($entity)) { + continue; + } + $entityId = (string) $entity->identifier(); + $objectUri = $this->uriMap->getObjectUri($mapId, $entityId) ?? ($entityId . '.ics'); + $data = $this->entityToIcal($entity); + + $objects[] = [ + DavProperties::ID => $entityId, + DavProperties::URI => $objectUri, + DavProperties::LAST_MODIFIED => $entity->modified()?->getTimestamp() ?? time(), + DavProperties::ETAG => '"' . md5($data) . '"', + DavProperties::SIZE => strlen($data), + DavProperties::COMPONENT => 'VEVENT', + DavProperties::CALENDAR_DATA => null, // lazy - fetched on demand + ]; + } + + return $objects; + } + + public function getCalendarObject($calendarId, $objectUri): ?array + { + [$providerId, $serviceId, $collectionId] = $this->parseId($calendarId); + $tenantId = $this->tenant->identifier(); + $userId = $this->identity->identifier(); + $mapId = $this->mapId($tenantId, $userId, $calendarId); + + // Resolve URI => entityId via the map; fall back to treating stripped URI as entityId + $entityId = $this->uriMap->getEntityId($mapId, $objectUri) + ?? preg_replace('/\.ics$/i', '', $objectUri); + + $identifier = new EntityIdentifier($providerId, $serviceId, $collectionId, $entityId); + $fetched = $this->manager->entityFetchBulk($tenantId, $userId, $identifier); + $entity = reset($fetched) ?: null; + + if ($entity === null) { + return null; + } + + $data = $this->entityToIcal($entity); + + return [ + DavProperties::ID => $entityId, + DavProperties::URI => $objectUri, + DavProperties::LAST_MODIFIED => $entity->modified()?->getTimestamp() ?? time(), + DavProperties::ETAG => '"' . md5($data) . '"', + DavProperties::SIZE => strlen($data), + DavProperties::COMPONENT => 'VEVENT', + DavProperties::CALENDAR_DATA => $data, + ]; + } + + public function getMultipleCalendarObjects($calendarId, array $uris): array + { + return array_values(array_filter(array_map( + fn($uri) => $this->getCalendarObject($calendarId, $uri), + $uris, + ))); + } + + public function createCalendarObject($calendarId, $objectUri, $calendarData): ?string + { + [$providerId, $serviceId, $collectionId] = $this->parseId($calendarId); + $tenantId = $this->tenant->identifier(); + $userId = $this->identity->identifier(); + + $vcal = Reader::read($calendarData); + $props = $this->icalToEntityProps($vcal); + $target = new CollectionIdentifier($providerId, $serviceId, $collectionId); + $entity = $this->manager->entityCreate($tenantId, $userId, $target, $props); + + // Persist the client-chosen URI => provider entityId mapping + $this->uriMap->set( + $this->mapId($tenantId, $userId, $calendarId), + $objectUri, + (string) $entity->identifier(), + ); + + return '"' . md5($this->entityToIcal($entity)) . '"'; + } + + public function updateCalendarObject($calendarId, $objectUri, $calendarData): ?string + { + [$providerId, $serviceId, $collectionId] = $this->parseId($calendarId); + $tenantId = $this->tenant->identifier(); + $userId = $this->identity->identifier(); + $mapId = $this->mapId($tenantId, $userId, $calendarId); + + $entityId = $this->uriMap->getEntityId($mapId, $objectUri) + ?? preg_replace('/\.ics$/i', '', $objectUri); + + $vcal = Reader::read($calendarData); + $props = $this->icalToEntityProps($vcal); + $target = new EntityIdentifier($providerId, $serviceId, $collectionId, $entityId); + $entity = $this->manager->entityModify($tenantId, $userId, $target, $props); + + return '"' . md5($this->entityToIcal($entity)) . '"'; + } + + public function deleteCalendarObject($calendarId, $objectUri): void + { + [$providerId, $serviceId, $collectionId] = $this->parseId($calendarId); + $tenantId = $this->tenant->identifier(); + $userId = $this->identity->identifier(); + $mapId = $this->mapId($tenantId, $userId, $calendarId); + + $entityId = $this->uriMap->getEntityId($mapId, $objectUri) + ?? preg_replace('/\.ics$/i', '', $objectUri); + + $target = new EntityIdentifier($providerId, $serviceId, $collectionId, $entityId); + $this->manager->entityDelete($tenantId, $userId, $target); + $this->uriMap->delete($mapId, $objectUri); + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + private function parseId(string $calendarId): array + { + return explode(self::ID_SEP, $calendarId, 3); + } + + private function mapId(string $tenantId, string $userId, string $calendarId): string + { + return 'cal:' . $tenantId . ':' . $userId . ':' . $calendarId; + } + + private function slugify(string $label): string + { + return strtolower(preg_replace('/[^a-z0-9]+/i', '-', trim($label))); + } + + /** + * Serialize a domain entity to an iCalendar string. + * + * The raw data from getDataRaw() is a serialized EventObject array + * with fields matching EventCommonObject property names (label, startsOn, endsOn, ...). + */ + private function entityToIcal(object $entity): string + { + $raw = (array) ($entity->getProperties()->getDataRaw() ?? []); + $utc = new \DateTimeZone('UTC'); + $vcal = new VCalendar(); + + // Create the VEVENT without sabre's auto-generated UID/DTSTAMP defaults, + // otherwise they appear twice (we set our own below), which is invalid. + /** @var \Sabre\VObject\Component $vevent */ + $vevent = $vcal->add('VEVENT', [], false); + $vevent->add('UID', (string) $entity->identifier()); + // Always emit date-times in UTC ("...Z"). A DateTime carrying a fixed + // numeric offset (e.g. from an RFC3339 string) would otherwise serialize + // as TZID=-04:00 with no matching VTIMEZONE, which clients reject. + // DTSTAMP participates in the serialized data used for the ETag. Using + // the current time here made an unchanged entity produce a different + // ETag on every PROPFIND/REPORT and triggered perpetual client syncs. + $stamp = $entity->modified() ?? $entity->created(); + $vevent->add( + 'DTSTAMP', + $stamp !== null + ? \DateTime::createFromImmutable($stamp)->setTimezone($utc) + : (new \DateTime('@0'))->setTimezone($utc), + ); + + if ($created = $entity->created()) { + $vevent->add('CREATED', \DateTime::createFromImmutable($created)->setTimezone($utc)); + } + if ($modified = $entity->modified()) { + $vevent->add('LAST-MODIFIED', \DateTime::createFromImmutable($modified)->setTimezone($utc)); + } + if (!empty($raw['label'])) { + $vevent->add('SUMMARY', (string) $raw['label']); + } + if (!empty($raw['description'])) { + $vevent->add('DESCRIPTION', (string) $raw['description']); + } + if (!empty($raw['startsOn'])) { + $vevent->add('DTSTART', (new \DateTime((string) $raw['startsOn']))->setTimezone($utc)); + } + if (!empty($raw['endsOn'])) { + $vevent->add('DTEND', (new \DateTime((string) $raw['endsOn']))->setTimezone($utc)); + } + // iCal supports one LOCATION property; use first physical location + if (!empty($raw['locationsPhysical']) && is_array($raw['locationsPhysical'])) { + foreach ($raw['locationsPhysical'] as $loc) { + $addr = $loc['address'] ?? $loc['name'] ?? $loc['label'] ?? null; + if ($addr) { + $vevent->add('LOCATION', (string) $addr); + break; + } + } + } + + return $vcal->serialize(); + } + + /** + * Convert a parsed VCALENDAR to an array for entityCreate/entityUpdate. + * Field names match EventCommonObject property names used in serialization. + */ + private function icalToEntityProps(VCalendar $vcal): array + { + $comp = $vcal->VEVENT ?? $vcal->VTODO ?? $vcal->VJOURNAL ?? null; + if ($comp === null) { + return []; + } + + $props = []; + if ($summary = (string) ($comp->SUMMARY ?? '')) { + $props['label'] = $summary; + } + if ($description = (string) ($comp->DESCRIPTION ?? '')) { + $props['description'] = $description; + } + if ($dtstart = $comp->DTSTART ?? null) { + $props['startsOn'] = $dtstart->getDateTime()->format(\DateTime::RFC3339); + } + if ($dtend = $comp->DTEND ?? $comp->DUE ?? null) { + $props['endsOn'] = $dtend->getDateTime()->format(\DateTime::RFC3339); + } + if ($location = (string) ($comp->LOCATION ?? '')) { + $props['locationsPhysical'] = [['label' => $location]]; + } + + return $props; + } +} diff --git a/lib/Backends/CardDavBackend.php b/lib/Backends/CardDavBackend.php new file mode 100644 index 0000000..c34fec7 --- /dev/null +++ b/lib/Backends/CardDavBackend.php @@ -0,0 +1,370 @@ + Collection (within a provider service) + * Card <=> Entity (individual / organization / group) + * + * Address book identifiers use the composite key "{providerId}:{serviceId}:{collectionId}". + * Card URIs are client-chosen names (e.g. "contact.vcf") mapped to provider + * entity identifiers via DavUriMap. + */ +class CardDavBackend extends AbstractBackend +{ + private const ID_SEP = ':'; + + public function __construct( + private readonly PeopleManager $manager, + private readonly SessionIdentity $identity, + private readonly SessionTenant $tenant, + private readonly DavUriMap $uriMap, + ) {} + + // ------------------------------------------------------------------------- + // Required: address book listing + // ------------------------------------------------------------------------- + + public function getAddressBooksForUser($principalUri): array + { + $result = []; + + // collectionList returns [providerId][serviceId][collectionId] => CollectionBaseInterface + $allCollections = $this->manager->collectionList($this->tenant->identifier(), $this->identity->identifier()); + + foreach ($allCollections as $providerId => $providerServices) { + foreach ($providerServices as $serviceId => $collections) { + foreach ($collections as $collectionId => $collection) { + if (!is_object($collection)) { + continue; + } + $props = $collection->getProperties(); + $label = $props->getLabel(); + $abId = implode(self::ID_SEP, [(string) $providerId, (string) $serviceId, (string) $collectionId]); + + $result[] = [ + DavProperties::ID => $abId, + DavProperties::URI => $this->slugify($label), + DavProperties::DISPLAYNAME => $label, + DavProperties::CARDDAV_DESCRIPTION => $props->getDescription() ?? '', + DavProperties::CS_CTAG => $collection->signature() ?? '', + DavProperties::PRINCIPAL_URI => $principalUri, + ]; + } + } + } + + return $result; + } + + public function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch): void + { + // not supported + } + + public function createAddressBook($principalUri, $url, array $properties): string + { + throw new \Sabre\DAV\Exception\Forbidden('Address book creation is not supported via DAV'); + } + + public function deleteAddressBook($addressBookId): void + { + throw new \Sabre\DAV\Exception\Forbidden('Address book deletion is not supported via DAV'); + } + + // ------------------------------------------------------------------------- + // Required: cards + // ------------------------------------------------------------------------- + + public function getCards($addressBookId): array + { + $tenantId = $this->tenant->identifier(); + $userId = $this->identity->identifier(); + + $targets = ResourceIdentifiers::fromArray([$addressBookId]); + + // entityListBulk returns [providerId][serviceId][collectionId] => [entityId => EntityBaseInterface] + $entitiesByLocation = $this->manager->entityListBulk($tenantId, $userId, $targets); + + $mapId = $this->mapId($tenantId, $userId, $addressBookId); + $cards = []; + foreach ($entitiesByLocation as $providerServices) { + foreach ($providerServices as $serviceCollections) { + foreach ($serviceCollections as $entities) { + foreach ($entities as $entity) { + if (!is_object($entity)) { + continue; + } + $entityId = (string) $entity->identifier(); + $objectUri = $this->uriMap->getObjectUri($mapId, $entityId) ?? ($entityId . '.vcf'); + $data = $this->entityToVcard($entity); + + $cards[] = [ + DavProperties::ID => $entityId, + DavProperties::URI => $objectUri, + DavProperties::LAST_MODIFIED => $entity->modified()?->getTimestamp() ?? time(), + DavProperties::ETAG => '"' . md5($data) . '"', + DavProperties::SIZE => strlen($data), + DavProperties::CARD_DATA => null, // lazy + ]; + } + } + } + } + + return $cards; + } + + public function getCard($addressBookId, $cardUri): ?array + { + [$providerId, $serviceId, $collectionId] = $this->parseId($addressBookId); + $tenantId = $this->tenant->identifier(); + $userId = $this->identity->identifier(); + $mapId = $this->mapId($tenantId, $userId, $addressBookId); + + $entityId = $this->uriMap->getEntityId($mapId, $cardUri) + ?? preg_replace('/\.vcf$/i', '', $cardUri); + + $identifier = new EntityIdentifier($providerId, $serviceId, $collectionId, $entityId); + $fetched = $this->manager->entityFetchBulk($tenantId, $userId, $identifier); + $entity = reset($fetched) ?: null; + + if ($entity === null) { + return null; + } + + $data = $this->entityToVcard($entity); + + return [ + DavProperties::ID => $entityId, + DavProperties::URI => $cardUri, + DavProperties::LAST_MODIFIED => $entity->modified()?->getTimestamp() ?? time(), + DavProperties::ETAG => '"' . md5($data) . '"', + DavProperties::SIZE => strlen($data), + DavProperties::CARD_DATA => $data, + ]; + } + + public function getMultipleCards($addressBookId, array $uris): array + { + return array_values(array_filter(array_map( + fn($uri) => $this->getCard($addressBookId, $uri), + $uris, + ))); + } + + public function createCard($addressBookId, $cardUri, $cardData): ?string + { + [$providerId, $serviceId, $collectionId] = $this->parseId($addressBookId); + $tenantId = $this->tenant->identifier(); + $userId = $this->identity->identifier(); + + $vcard = Reader::read($cardData); + $props = $this->vcardToEntityProps($vcard); + $target = new CollectionIdentifier($providerId, $serviceId, $collectionId); + $entity = $this->manager->entityCreate($tenantId, $userId, $target, $props); + + $this->uriMap->set( + $this->mapId($tenantId, $userId, $addressBookId), + $cardUri, + (string) $entity->identifier(), + ); + + return '"' . md5($this->entityToVcard($entity)) . '"'; + } + + public function updateCard($addressBookId, $cardUri, $cardData): ?string + { + [$providerId, $serviceId, $collectionId] = $this->parseId($addressBookId); + $tenantId = $this->tenant->identifier(); + $userId = $this->identity->identifier(); + $mapId = $this->mapId($tenantId, $userId, $addressBookId); + + $entityId = $this->uriMap->getEntityId($mapId, $cardUri) + ?? preg_replace('/\.vcf$/i', '', $cardUri); + + $vcard = Reader::read($cardData); + $props = $this->vcardToEntityProps($vcard); + $target = new EntityIdentifier($providerId, $serviceId, $collectionId, $entityId); + $entity = $this->manager->entityModify($tenantId, $userId, $target, $props); + + return '"' . md5($this->entityToVcard($entity)) . '"'; + } + + public function deleteCard($addressBookId, $cardUri): bool + { + [$providerId, $serviceId, $collectionId] = $this->parseId($addressBookId); + $tenantId = $this->tenant->identifier(); + $userId = $this->identity->identifier(); + $mapId = $this->mapId($tenantId, $userId, $addressBookId); + + $entityId = $this->uriMap->getEntityId($mapId, $cardUri) + ?? preg_replace('/\.vcf$/i', '', $cardUri); + + $target = new EntityIdentifier($providerId, $serviceId, $collectionId, $entityId); + $outcome = $this->manager->entityDelete($tenantId, $userId, $target); + + // entityDelete returns outcomes keyed by source identifier; a non-error + // disposition ('deleted'|'moved') indicates the card no longer exists here. + $disposition = $outcome[(string) $target]['disposition'] ?? 'error'; + $deleted = $disposition !== 'error'; + if ($deleted) { + $this->uriMap->delete($mapId, $cardUri); + } + return $deleted; + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + private function parseId(string $id): array + { + return explode(self::ID_SEP, $id, 3); + } + + private function mapId(string $tenantId, string $userId, string $addressBookId): string + { + return 'card:' . $tenantId . ':' . $userId . ':' . $addressBookId; + } + + private function slugify(string $label): string + { + return strtolower(preg_replace('/[^a-z0-9]+/i', '-', trim($label))); + } + + /** + * Serialize a domain people entity to a vCard 4.0 string. + * + * The raw data from getDataRaw() is a serialized IndividualObject array with + * fields: label, names (given/family/...), phones, emails, organizations, ... + */ + private function entityToVcard(object $entity): string + { + $raw = (array) ($entity->getProperties()->getDataRaw() ?? []); + $vcard = new VCard(['VERSION' => '4.0', 'UID' => (string) $entity->identifier()]); + + // Full name (FN is required in vCard 4.0) + $fn = (string) ($raw['label'] ?? ''); + $vcard->add('FN', $fn ?: 'Unknown'); + + // Structured name: N;family;given;additional;prefix;suffix + $names = (array) ($raw['names'] ?? []); + $family = (string) ($names['family'] ?? ''); + $given = (string) ($names['given'] ?? ''); + if ($family || $given) { + $vcard->add('N', [ + 'value' => [ + $family, + $given, + (string) ($names['additional'] ?? ''), + (string) ($names['prefix'] ?? ''), + (string) ($names['suffix'] ?? ''), + ], + ]); + } + + // E-mail addresses + foreach ((array) ($raw['emails'] ?? []) as $email) { + if ($address = (string) ($email['address'] ?? '')) { + $vcard->add('EMAIL', $address); + } + } + + // Phone numbers + foreach ((array) ($raw['phones'] ?? []) as $phone) { + if ($number = (string) ($phone['number'] ?? '')) { + $vcard->add('TEL', $number); + } + } + + // Organization (first entry only for simplicity) + foreach ((array) ($raw['organizations'] ?? []) as $org) { + $orgName = (string) ($org['Label'] ?? $org['label'] ?? ''); + if ($orgName) { + $vcard->add('ORG', $orgName); + break; + } + } + + return $vcard->serialize(); + } + + /** + * Convert a parsed vCard into an array for entityCreate/entityUpdate. + * Field names match IndividualObject property names used in serialization. + */ + private function vcardToEntityProps(VCard $vcard): array + { + $props = []; + + if ($fn = (string) ($vcard->FN ?? '')) { + $props['label'] = $fn; + } + + if ($n = $vcard->N) { + $parts = $n->getParts(); + $names = []; + if (!empty($parts[0])) { + $names['family'] = $parts[0]; + } + if (!empty($parts[1])) { + $names['given'] = $parts[1]; + } + if (!empty($parts[2])) { + $names['additional'] = $parts[2]; + } + if (!empty($parts[3])) { + $names['prefix'] = $parts[3]; + } + if (!empty($parts[4])) { + $names['suffix'] = $parts[4]; + } + if ($names) { + $props['names'] = $names; + } + } + + $emails = []; + foreach ($vcard->select('EMAIL') as $email) { + $emails[] = ['address' => (string) $email]; + } + if ($emails) { + $props['emails'] = $emails; + } + + $phones = []; + foreach ($vcard->select('TEL') as $tel) { + $phones[] = ['number' => (string) $tel]; + } + if ($phones) { + $props['phones'] = $phones; + } + + $orgs = []; + foreach ($vcard->select('ORG') as $org) { + $orgs[] = ['Label' => (string) $org]; + } + if ($orgs) { + $props['organizations'] = $orgs; + } + + return $props; + } +} diff --git a/lib/Backends/FilesCollectionNode.php b/lib/Backends/FilesCollectionNode.php new file mode 100644 index 0000000..b125e4f --- /dev/null +++ b/lib/Backends/FilesCollectionNode.php @@ -0,0 +1,118 @@ +collection->getProperties()->getLabel() ?? (string) $this->collection->identifier(); + } + + /** + * Identifier of the collection backing this node + */ + private function collectionIdentifier(): CollectionIdentifier + { + return new CollectionIdentifier( + $this->service->provider(), + (string) $this->service->identifier(), + (string) $this->collection->identifier(), + ); + } + + /** @return INode[] */ + public function getChildren(): array + { + $nodes = []; + + // ----- collections (folders) ----- + $decendents = $this->service->collectionList($this->collection->identifier()); + foreach ($decendents as $decendent) { + $nodes[] = new FilesCollectionNode( + $this->service, + $decendent, + ); + } + + // ----- entities (files) ----- + /** @var \KTXF\Documents\Entity\EntityBaseInterface[] $entities */ + $entities = $this->service->entityListBulk($this->collection->identifier()); + foreach ($entities as $entity) { + if (is_object($entity)) { + $nodes[] = new FilesEntityNode( + $this->service, + $entity, + ); + } + } + + return $nodes; + } + + public function createDirectory($name): void + { + if (!$this->service instanceof ServiceCollectionMutableInterface) { + throw new Forbidden('Service does not support collection creation'); + } + + $properties = $this->service->collectionFresh()->getProperties(); + $properties->setLabel($name); + $this->service->collectionCreate($this->collectionIdentifier(), $properties); + } + + public function createFile($name, $data = null): string + { + if (!$this->service instanceof ServiceEntityMutableInterface) { + throw new Forbidden('Service does not support entity creation'); + } + + $properties = $this->service->entityFresh()->getProperties(); + $properties->setLabel($name); + $entity = $this->service->entityCreate($this->collectionIdentifier(), $properties); + + if ($data !== null) { + $content = is_resource($data) ? stream_get_contents($data) : (string) $data; + $this->service->entityWrite( + new EntityIdentifier( + $this->service->provider(), + (string) $this->service->identifier(), + (string) $entity->collection(), + (string) $entity->identifier(), + ), + $content, + ); + } + + return '"' . md5($entity->identifier() . time()) . '"'; + } + + public function delete(): void + { + if (!$this->service instanceof ServiceCollectionMutableInterface) { + throw new Forbidden('Service does not support collection deletion'); + } + + $this->service->collectionDelete($this->collectionIdentifier(), true); + } +} diff --git a/lib/Backends/FilesEntityNode.php b/lib/Backends/FilesEntityNode.php new file mode 100644 index 0000000..b842c0c --- /dev/null +++ b/lib/Backends/FilesEntityNode.php @@ -0,0 +1,90 @@ +entity->getProperties()->getLabel() ?? (string) $this->entity->identifier(); + } + + /** + * Identifier of the entity backing this node + */ + private function entityIdentifier(): EntityIdentifier + { + return new EntityIdentifier( + $this->service->provider(), + (string) $this->service->identifier(), + (string) $this->entity->collection(), + (string) $this->entity->identifier(), + ); + } + + /** @return resource|string */ + public function get() + { + return $this->service->entityRead($this->entityIdentifier()); + } + + public function put($data): ?string + { + if (!$this->service instanceof ServiceEntityMutableInterface) { + throw new Forbidden('Entity is immutable'); + } + + $this->service->entityWrite( + $this->entityIdentifier(), + is_resource($data) ? stream_get_contents($data) : (string) $data, + ); + + return '"' . md5($this->entity->identifier() . time()) . '"'; + } + + public function delete(): void + { + if (!$this->service instanceof ServiceEntityMutableInterface) { + throw new Forbidden('Entity is immutable'); + } + + $this->service->entityDelete($this->entityIdentifier()); + } + + public function getContentType(): ?string + { + return $this->entity->getProperties()->getMime(); + } + + public function getETag(): ?string + { + return '"' . $this->entity->signature() . '"'; + } + + public function getSize(): ?int + { + return $this->entity->getProperties()->size(); + } + + public function getLastModified(): ?int + { + return $this->entity->modified()?->getTimestamp() ?? null; + } +} diff --git a/lib/Backends/FilesRootNode.php b/lib/Backends/FilesRootNode.php new file mode 100644 index 0000000..49215f7 --- /dev/null +++ b/lib/Backends/FilesRootNode.php @@ -0,0 +1,56 @@ +manager->serviceList($this->tenant->identifier(), $this->identity->identifier()); + + foreach ($services as $providerId => $providerServices) { + foreach ($providerServices as $serviceId => $service) { + $nodes[] = new FilesServiceNode( + $service, + ); + } + } + + return $nodes; + } + + public function createDirectory($name): void + { + throw new Forbidden('Cannot create a provider root directory'); + } +} diff --git a/lib/Backends/FilesServiceNode.php b/lib/Backends/FilesServiceNode.php new file mode 100644 index 0000000..9f506c2 --- /dev/null +++ b/lib/Backends/FilesServiceNode.php @@ -0,0 +1,67 @@ +service->getLabel(); + } + + /** @return INode[] */ + public function getChildren(): array + { + $nodes = []; + + $collections = $this->service->collectionList(null); + foreach ($collections as $collection) { + $nodes[] = new FilesCollectionNode( + $this->service, + $collection, + ); + } + + /** @var \KTXF\Documents\Entity\EntityBaseInterface[] $entities */ + $entities = $this->service->entityListBulk(null); + foreach ($entities as $entity) { + if (is_object($entity)) { + $nodes[] = new FilesEntityNode( + $this->service, + $entity, + ); + } + } + + return $nodes; + } + + public function createDirectory($name): void + { + if (!$this->service instanceof ServiceCollectionMutableInterface) { + throw new Forbidden('Service does not support collection creation'); + } + + $properties = $this->service->collectionFresh()->getProperties(); + $properties->setLabel($name); + $this->service->collectionCreate(null, $properties); + } +} diff --git a/lib/Backends/PrincipalBackend.php b/lib/Backends/PrincipalBackend.php new file mode 100644 index 0000000..15e89de --- /dev/null +++ b/lib/Backends/PrincipalBackend.php @@ -0,0 +1,97 @@ +identity->identity() === null) { + return []; + } + + return [$this->buildPrincipal()]; + } + + public function getPrincipalByPath($path): ?array + { + if ($this->identity->identity() === null) { + return null; + } + + $expected = self::PRINCIPAL_PREFIX . '/' . $this->identity->identifier(); + if ($path !== $expected) { + return null; + } + + return $this->buildPrincipal(); + } + + public function getGroupMemberSet($principal): array + { + return []; + } + + public function getGroupMembership($principal): array + { + return []; + } + + public function setGroupMemberSet($principal, $members): void + { + // not supported + } + + public function updatePrincipal($path, $propPatch): int + { + return 403; + } + + public function searchPrincipals($prefixPath, $searchProperties, $test = 'allof'): array + { + return []; + } + + public function findByUri($uri, $principalPrefix): ?string + { + return null; + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + private function buildPrincipal(): array + { + $userId = $this->identity->identifier(); + $label = $this->identity->label() ?? $userId; + $email = $this->identity->mailAddress() ?? ''; + + return [ + DavProperties::URI => self::PRINCIPAL_PREFIX . '/' . $userId, + DavProperties::DISPLAYNAME => $label, + DavProperties::SABRE_EMAIL => $email, + ]; + } +} diff --git a/lib/Controllers/DavController.php b/lib/Controllers/DavController.php new file mode 100644 index 0000000..3dd1fa1 --- /dev/null +++ b/lib/Controllers/DavController.php @@ -0,0 +1,40 @@ +davServer->process($request); + } +} diff --git a/lib/DavProperties.php b/lib/DavProperties.php new file mode 100644 index 0000000..3eefc2b --- /dev/null +++ b/lib/DavProperties.php @@ -0,0 +1,46 @@ +getServer(); + + $sabreRequest = new SabreRequest( + $request->getMethod(), + $this->normalizeRequestUri($request->getRequestUri()), + $this->normalizeHeaders($request->headers->all()), + ); + + $body = $request->getContent(asResource: false); + if (!empty($body)) { + $sabreRequest->setBody($body); + } + + $sabreResponse = new SabreResponse(); + + $sabreRequest->setBaseUrl($server->getBaseUri()); + + $server->httpRequest = $sabreRequest; + $server->httpResponse = $sabreResponse; + + try { + $server->invokeMethod($sabreRequest, $sabreResponse, false); + } catch (\Sabre\DAV\Exception $e) { + try { $server->emit('exception', [$e]); } catch (\Throwable $ignore) {} + $sabreResponse->setStatus($e->getHTTPCode()); + foreach ($e->getHTTPHeaders($server) as $name => $value) { + $sabreResponse->setHeader($name, $value); + } + $sabreResponse->setBody($this->serializeException($e, $server)); + } catch (\Throwable $e) { + try { $server->emit('exception', [$e]); } catch (\Throwable $ignore) {} + $sabreResponse->setStatus(500); + $sabreResponse->setBody($this->serializeException($e, $server)); + } + + return $this->buildKtxcResponse($sabreResponse); + } + + private function getServer(): Server + { + return $this->buildServer(); + } + + private function buildServer(): Server + { + $principalBackend = new PrincipalBackend($this->identity, $this->tenant); + $calDavBackend = new CalDavBackend($this->chronoManager, $this->identity, $this->tenant, $this->uriMap); + $cardDavBackend = new CardDavBackend($this->peopleManager, $this->identity, $this->tenant, $this->uriMap); + + $server = new Server([ + new PrincipalCollection($principalBackend, PrincipalBackend::PRINCIPAL_PREFIX), + new CalendarRoot($principalBackend, $calDavBackend), + new AddressBookRoot($principalBackend, $cardDavBackend), + new FilesRootNode($this->documentsManager, $this->identity, $this->tenant), + ]); + $server->setBaseUri(self::BASE_URI); + $server->debugExceptions = $this->debug; + + // Authentication plugin — short-circuits when JWT/Bearer already populated + $authBackend = new DavAuthBackend($this->identity, $this->securityService); + $server->addPlugin(new AuthPlugin($authBackend)); + + // DAVACL — required for CalDAV / CardDAV principal resolution + $aclPlugin = new DavACLPlugin(); + $userId = (string) ($this->identity->identifier() ?? 'anonymous'); + $aclPlugin->adminPrincipals = [PrincipalBackend::PRINCIPAL_PREFIX . '/' . $userId]; + $server->addPlugin($aclPlugin); + + $server->addPlugin(new CalDAVPlugin()); + $server->addPlugin(new CardDAVPlugin()); + $server->addPlugin(new VCFExportPlugin()); + + // File-based lock backend (single-server; swap for DB/Redis in multi-node setups) + $davDir = $this->rootDir . '/var/dav'; + if (!is_dir($davDir)) { + mkdir($davDir, 0750, true); + } + $server->addPlugin(new LocksPlugin(new LocksBackendFile($davDir . '/locks.db'))); + + $server->addPlugin(new BrowserPlugin()); + + return $server; + } + + private function serializeException(\Throwable $e, Server $server): string + { + $h = static fn (string $s): string => htmlspecialchars($s, ENT_NOQUOTES, 'UTF-8'); + + $dom = new \DOMDocument('1.0', 'utf-8'); + $dom->formatOutput = true; + $error = $dom->createElementNS('http://sabredav.org/ns', 's:error'); + $error->setAttribute('xmlns:s', 'http://sabredav.org/ns'); + $dom->appendChild($error); + $error->appendChild($dom->createElement('s:exception', $h(get_class($e)))); + $error->appendChild($dom->createElement('s:message', $h($e->getMessage()))); + + if ($this->debug) { + $error->appendChild($dom->createElement('s:file', $h($e->getFile()))); + $error->appendChild($dom->createElement('s:line', $h((string) $e->getLine()))); + $error->appendChild($dom->createElement('s:code', $h((string) $e->getCode()))); + $error->appendChild($dom->createElement('s:stacktrace', $h($e->getTraceAsString()))); + } + + if ($e instanceof \Sabre\DAV\Exception) { + $e->serialize($server, $error); + } + + return $dom->saveXML(); + } + + // ------------------------------------------------------------------------- + // HTTP bridge helpers + // ------------------------------------------------------------------------- + + private function normalizeRequestUri(string $uri): string + { + // Ensure requests for the bare base path (e.g. /dav) include the trailing + // slash so sabre's getPath() keeps them within the base URI /dav/. + $path = strtok($uri, '?'); + if ($path === rtrim(self::BASE_URI, '/')) { + return self::BASE_URI . (str_contains($uri, '?') ? '?' . explode('?', $uri, 2)[1] : ''); + } + return $uri; + } + + private function normalizeHeaders(array $symfonyHeaders): array + { + $normalized = []; + foreach ($symfonyHeaders as $name => $values) { + $key = str_replace(' ', '-', ucwords(str_replace('-', ' ', strtolower($name)))); + $normalized[$key] = implode(', ', (array) $values); + } + return $normalized; + } + + private function buildKtxcResponse(SabreResponse $sabreResponse): Response + { + $status = $sabreResponse->getStatus(); + $body = $sabreResponse->getBody(); + + // Collect headers; skip security headers that KTXC re-applies by default + $headers = []; + foreach ($sabreResponse->getHeaders() as $name => $value) { + $headers[$name] = $value; + } + + + if (is_resource($body)) { + // Stream the body to avoid buffering potentially large files + $stream = $body; + return new StreamedResponse( + static function () use ($stream): void { + while (!feof($stream)) { + echo fread($stream, 8192); + } + }, + $status, + $headers, + ); + } + + return new Response((string) ($body ?? ''), $status, $headers); + } +} diff --git a/lib/DavUriMap.php b/lib/DavUriMap.php new file mode 100644 index 0000000..2bc9c38 --- /dev/null +++ b/lib/DavUriMap.php @@ -0,0 +1,135 @@ +dir = $rootDir . '/var/dav'; + } + + // ------------------------------------------------------------------------- + // Public API + // ------------------------------------------------------------------------- + + /** + * Look up the provider entity identifier for a given DAV object URI. + * + * @param string $mapId Logical map bucket (e.g. "{tenant}:{user}:cal:{calId}") + * @param string $objectUri DAV leaf URI (e.g. "abc-123.ics") + */ + public function getEntityId(string $mapId, string $objectUri): ?string + { + return $this->read($mapId)['obj_to_ent'][$objectUri] ?? null; + } + + /** + * Look up the DAV object URI for a given provider entity identifier. + */ + public function getObjectUri(string $mapId, string $entityId): ?string + { + return $this->read($mapId)['ent_to_obj'][(string) $entityId] ?? null; + } + + /** + * Record a mapping from a DAV object URI to a provider entity identifier. + */ + public function set(string $mapId, string $objectUri, string|int $entityId): void + { + $entityId = (string) $entityId; + $this->update($mapId, static function (array $data) use ($objectUri, $entityId): array { + $data['obj_to_ent'][$objectUri] = $entityId; + $data['ent_to_obj'][$entityId] = $objectUri; + return $data; + }); + } + + /** + * Remove the mapping for a DAV object URI. + */ + public function delete(string $mapId, string $objectUri): void + { + $this->update($mapId, static function (array $data) use ($objectUri): array { + $entityId = $data['obj_to_ent'][$objectUri] ?? null; + if ($entityId !== null) { + unset($data['ent_to_obj'][$entityId]); + } + unset($data['obj_to_ent'][$objectUri]); + return $data; + }); + } + + // ------------------------------------------------------------------------- + // Internal helpers + // ------------------------------------------------------------------------- + + private function mapFile(string $mapId): string + { + return $this->dir . '/map_' . md5($mapId) . '.json'; + } + + private function read(string $mapId): array + { + $file = $this->mapFile($mapId); + if (!file_exists($file)) { + return ['obj_to_ent' => [], 'ent_to_obj' => []]; + } + $contents = file_get_contents($file); + if ($contents === false) { + return ['obj_to_ent' => [], 'ent_to_obj' => []]; + } + return json_decode($contents, true) ?? ['obj_to_ent' => [], 'ent_to_obj' => []]; + } + + private function update(string $mapId, callable $updater): void + { + if (!is_dir($this->dir)) { + mkdir($this->dir, 0750, true); + } + + $file = $this->mapFile($mapId); + $fh = fopen($file, 'c+'); + if ($fh === false) { + return; + } + + try { + flock($fh, LOCK_EX); + $contents = stream_get_contents($fh); + $data = json_decode($contents ?: '{}', true) + ?? ['obj_to_ent' => [], 'ent_to_obj' => []]; + if (!isset($data['obj_to_ent'])) { + $data['obj_to_ent'] = []; + } + if (!isset($data['ent_to_obj'])) { + $data['ent_to_obj'] = []; + } + $data = $updater($data); + ftruncate($fh, 0); + rewind($fh); + fwrite($fh, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + } finally { + flock($fh, LOCK_UN); + fclose($fh); + } + } +} diff --git a/lib/Module.php b/lib/Module.php new file mode 100644 index 0000000..958bee6 --- /dev/null +++ b/lib/Module.php @@ -0,0 +1,55 @@ + [ + 'label' => 'Access DAV Service', + 'description' => 'Allow access to the WebDAV / CalDAV / CardDAV service', + 'group' => 'DAV Service', + ], + ]; + } + + public function boot(): void + { + // Load sabre/dav and its dependencies from the module-local vendor directory. + $vendorAutoload = __DIR__ . '/vendor/autoload.php'; + if (file_exists($vendorAutoload)) { + require_once $vendorAutoload; + } + } +} diff --git a/lib/vendor/autoload.php b/lib/vendor/autoload.php new file mode 100644 index 0000000..0b8a598 --- /dev/null +++ b/lib/vendor/autoload.php @@ -0,0 +1,22 @@ +realpath = realpath($opened_path) ?: $opened_path; + $opened_path = $this->realpath; + $this->handle = fopen($this->realpath, $mode); + $this->position = 0; + + return (bool) $this->handle; + } + + public function stream_read($count) + { + $data = fread($this->handle, $count); + + if ($this->position === 0) { + $data = preg_replace('{^#!.*\r?\n}', '', $data); + } + + $this->position += strlen($data); + + return $data; + } + + public function stream_cast($castAs) + { + return $this->handle; + } + + public function stream_close() + { + fclose($this->handle); + } + + public function stream_lock($operation) + { + return $operation ? flock($this->handle, $operation) : true; + } + + public function stream_seek($offset, $whence) + { + if (0 === fseek($this->handle, $offset, $whence)) { + $this->position = ftell($this->handle); + return true; + } + + return false; + } + + public function stream_tell() + { + return $this->position; + } + + public function stream_eof() + { + return feof($this->handle); + } + + public function stream_stat() + { + return array(); + } + + public function stream_set_option($option, $arg1, $arg2) + { + return true; + } + + public function url_stat($path, $flags) + { + $path = substr($path, 17); + if (file_exists($path)) { + return stat($path); + } + + return false; + } + } + } + + if ( + (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) + || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) + ) { + include("phpvfscomposer://" . __DIR__ . '/..'.'/sabre/vobject/bin/generate_vcards'); + exit(0); + } +} + +include __DIR__ . '/..'.'/sabre/vobject/bin/generate_vcards'; diff --git a/lib/vendor/bin/generate_vcards.bat b/lib/vendor/bin/generate_vcards.bat new file mode 100755 index 0000000..cae5cbc --- /dev/null +++ b/lib/vendor/bin/generate_vcards.bat @@ -0,0 +1,5 @@ +@ECHO OFF +setlocal DISABLEDELAYEDEXPANSION +SET BIN_TARGET=%~dp0/generate_vcards +SET COMPOSER_RUNTIME_BIN_DIR=%~dp0 +php "%BIN_TARGET%" %* diff --git a/lib/vendor/bin/naturalselection b/lib/vendor/bin/naturalselection new file mode 100755 index 0000000..d13c00b --- /dev/null +++ b/lib/vendor/bin/naturalselection @@ -0,0 +1,37 @@ +#!/usr/bin/env sh + +# Support bash to support `source` with fallback on $0 if this does not run with bash +# https://stackoverflow.com/a/35006505/6512 +selfArg="$BASH_SOURCE" +if [ -z "$selfArg" ]; then + selfArg="$0" +fi + +self=$(realpath $selfArg 2> /dev/null) +if [ -z "$self" ]; then + self="$selfArg" +fi + +dir=$(cd "${self%[/\\]*}" > /dev/null; cd '../sabre/dav/bin' && pwd) + +if [ -d /proc/cygdrive ]; then + case $(which php) in + $(readlink -n /proc/cygdrive)/*) + # We are in Cygwin using Windows php, so the path must be translated + dir=$(cygpath -m "$dir"); + ;; + esac +fi + +export COMPOSER_RUNTIME_BIN_DIR="$(cd "${self%[/\\]*}" > /dev/null; pwd)" + +# If bash is sourcing this file, we have to source the target as well +bashSource="$BASH_SOURCE" +if [ -n "$bashSource" ]; then + if [ "$bashSource" != "$0" ]; then + source "${dir}/naturalselection" "$@" + return + fi +fi + +"${dir}/naturalselection" "$@" diff --git a/lib/vendor/bin/naturalselection.bat b/lib/vendor/bin/naturalselection.bat new file mode 100755 index 0000000..1979ed1 --- /dev/null +++ b/lib/vendor/bin/naturalselection.bat @@ -0,0 +1,5 @@ +@ECHO OFF +setlocal DISABLEDELAYEDEXPANSION +SET BIN_TARGET=%~dp0/../sabre/dav/bin/naturalselection +SET COMPOSER_RUNTIME_BIN_DIR=%~dp0 +python "%BIN_TARGET%" %* diff --git a/lib/vendor/bin/sabredav b/lib/vendor/bin/sabredav new file mode 100755 index 0000000..6e915f6 --- /dev/null +++ b/lib/vendor/bin/sabredav @@ -0,0 +1,37 @@ +#!/usr/bin/env sh + +# Support bash to support `source` with fallback on $0 if this does not run with bash +# https://stackoverflow.com/a/35006505/6512 +selfArg="$BASH_SOURCE" +if [ -z "$selfArg" ]; then + selfArg="$0" +fi + +self=$(realpath $selfArg 2> /dev/null) +if [ -z "$self" ]; then + self="$selfArg" +fi + +dir=$(cd "${self%[/\\]*}" > /dev/null; cd '../sabre/dav/bin' && pwd) + +if [ -d /proc/cygdrive ]; then + case $(which php) in + $(readlink -n /proc/cygdrive)/*) + # We are in Cygwin using Windows php, so the path must be translated + dir=$(cygpath -m "$dir"); + ;; + esac +fi + +export COMPOSER_RUNTIME_BIN_DIR="$(cd "${self%[/\\]*}" > /dev/null; pwd)" + +# If bash is sourcing this file, we have to source the target as well +bashSource="$BASH_SOURCE" +if [ -n "$bashSource" ]; then + if [ "$bashSource" != "$0" ]; then + source "${dir}/sabredav" "$@" + return + fi +fi + +"${dir}/sabredav" "$@" diff --git a/lib/vendor/bin/sabredav.bat b/lib/vendor/bin/sabredav.bat new file mode 100755 index 0000000..ab3bbe6 --- /dev/null +++ b/lib/vendor/bin/sabredav.bat @@ -0,0 +1,5 @@ +@ECHO OFF +setlocal DISABLEDELAYEDEXPANSION +SET BIN_TARGET=%~dp0/../sabre/dav/bin/sabredav +SET COMPOSER_RUNTIME_BIN_DIR=%~dp0 +sh "%BIN_TARGET%" %* diff --git a/lib/vendor/bin/vobject b/lib/vendor/bin/vobject new file mode 100755 index 0000000..0a78d49 --- /dev/null +++ b/lib/vendor/bin/vobject @@ -0,0 +1,120 @@ +#!/usr/bin/env php +realpath = realpath($opened_path) ?: $opened_path; + $opened_path = $this->realpath; + $this->handle = fopen($this->realpath, $mode); + $this->position = 0; + + return (bool) $this->handle; + } + + public function stream_read($count) + { + $data = fread($this->handle, $count); + + if ($this->position === 0) { + $data = preg_replace('{^#!.*\r?\n}', '', $data); + } + + $this->position += strlen($data); + + return $data; + } + + public function stream_cast($castAs) + { + return $this->handle; + } + + public function stream_close() + { + fclose($this->handle); + } + + public function stream_lock($operation) + { + return $operation ? flock($this->handle, $operation) : true; + } + + public function stream_seek($offset, $whence) + { + if (0 === fseek($this->handle, $offset, $whence)) { + $this->position = ftell($this->handle); + return true; + } + + return false; + } + + public function stream_tell() + { + return $this->position; + } + + public function stream_eof() + { + return feof($this->handle); + } + + public function stream_stat() + { + return array(); + } + + public function stream_set_option($option, $arg1, $arg2) + { + return true; + } + + public function url_stat($path, $flags) + { + $path = substr($path, 17); + if (file_exists($path)) { + return stat($path); + } + + return false; + } + } + } + + if ( + (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) + || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) + ) { + include("phpvfscomposer://" . __DIR__ . '/..'.'/sabre/vobject/bin/vobject'); + exit(0); + } +} + +include __DIR__ . '/..'.'/sabre/vobject/bin/vobject'; diff --git a/lib/vendor/bin/vobject.bat b/lib/vendor/bin/vobject.bat new file mode 100755 index 0000000..c43b741 --- /dev/null +++ b/lib/vendor/bin/vobject.bat @@ -0,0 +1,5 @@ +@ECHO OFF +setlocal DISABLEDELAYEDEXPANSION +SET BIN_TARGET=%~dp0/vobject +SET COMPOSER_RUNTIME_BIN_DIR=%~dp0 +php "%BIN_TARGET%" %* diff --git a/lib/vendor/composer/ClassLoader.php b/lib/vendor/composer/ClassLoader.php new file mode 100644 index 0000000..7824d8f --- /dev/null +++ b/lib/vendor/composer/ClassLoader.php @@ -0,0 +1,579 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see https://www.php-fig.org/psr/psr-0/ + * @see https://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + /** @var \Closure(string):void */ + private static $includeFile; + + /** @var string|null */ + private $vendorDir; + + // PSR-4 + /** + * @var array> + */ + private $prefixLengthsPsr4 = array(); + /** + * @var array> + */ + private $prefixDirsPsr4 = array(); + /** + * @var list + */ + private $fallbackDirsPsr4 = array(); + + // PSR-0 + /** + * List of PSR-0 prefixes + * + * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) + * + * @var array>> + */ + private $prefixesPsr0 = array(); + /** + * @var list + */ + private $fallbackDirsPsr0 = array(); + + /** @var bool */ + private $useIncludePath = false; + + /** + * @var array + */ + private $classMap = array(); + + /** @var bool */ + private $classMapAuthoritative = false; + + /** + * @var array + */ + private $missingClasses = array(); + + /** @var string|null */ + private $apcuPrefix; + + /** + * @var array + */ + private static $registeredLoaders = array(); + + /** + * @param string|null $vendorDir + */ + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + self::initializeIncludeClosure(); + } + + /** + * @return array> + */ + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); + } + + return array(); + } + + /** + * @return array> + */ + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + /** + * @return list + */ + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + /** + * @return list + */ + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + /** + * @return array Array of classname => path + */ + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + * + * @return void + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + * + * @return void + */ + public function add($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 base directories + * + * @return void + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + * + * @return void + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + * + * @return void + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + * + * @return void + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + * + * @return void + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + + if (null === $this->vendorDir) { + return; + } + + if ($prepend) { + self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; + } else { + unset(self::$registeredLoaders[$this->vendorDir]); + self::$registeredLoaders[$this->vendorDir] = $this; + } + } + + /** + * Unregisters this instance as an autoloader. + * + * @return void + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + + if (null !== $this->vendorDir) { + unset(self::$registeredLoaders[$this->vendorDir]); + } + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return true|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + $includeFile = self::$includeFile; + $includeFile($file); + + return true; + } + + return null; + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + /** + * Returns the currently registered loaders keyed by their corresponding vendor directories. + * + * @return array + */ + public static function getRegisteredLoaders() + { + return self::$registeredLoaders; + } + + /** + * @param string $class + * @param string $ext + * @return string|false + */ + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } + + /** + * @return void + */ + private static function initializeIncludeClosure() + { + if (self::$includeFile !== null) { + return; + } + + /** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + * + * @param string $file + * @return void + */ + self::$includeFile = \Closure::bind(static function($file) { + include $file; + }, null, null); + } +} diff --git a/lib/vendor/composer/InstalledVersions.php b/lib/vendor/composer/InstalledVersions.php new file mode 100644 index 0000000..87766a2 --- /dev/null +++ b/lib/vendor/composer/InstalledVersions.php @@ -0,0 +1,380 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer; + +use Composer\Autoload\ClassLoader; +use Composer\Semver\VersionParser; + +/** + * This class is copied in every Composer installed project and available to all + * + * See also https://getcomposer.org/doc/07-runtime.md#installed-versions + * + * To require its presence, you can require `composer-runtime-api ^2.0` + * + * @final + */ +class InstalledVersions +{ + /** + * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to + * @internal + */ + private static $selfDir = null; + + /** + * @var mixed[]|null + * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null + */ + private static $installed; + + /** + * @var bool|null + */ + private static $canGetVendors; + + /** + * @var array[] + * @psalm-var array}> + */ + private static $installedByVendor = array(); + + /** + * Returns a list of all package names which are present, either by being installed, replaced or provided + * + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackages() + { + $packages = array(); + foreach (self::getInstalled() as $installed) { + $packages[] = array_keys($installed['versions']); + } + + if (1 === \count($packages)) { + return $packages[0]; + } + + return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); + } + + /** + * Returns a list of all package names with a specific type e.g. 'library' + * + * @param string $type + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackagesByType($type) + { + $packagesByType = array(); + + foreach (self::getInstalled() as $installed) { + foreach ($installed['versions'] as $name => $package) { + if (isset($package['type']) && $package['type'] === $type) { + $packagesByType[] = $name; + } + } + } + + return $packagesByType; + } + + /** + * Checks whether the given package is installed + * + * This also returns true if the package name is provided or replaced by another package + * + * @param string $packageName + * @param bool $includeDevRequirements + * @return bool + */ + public static function isInstalled($packageName, $includeDevRequirements = true) + { + foreach (self::getInstalled() as $installed) { + if (isset($installed['versions'][$packageName])) { + return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; + } + } + + return false; + } + + /** + * Checks whether the given package satisfies a version constraint + * + * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: + * + * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') + * + * @param VersionParser $parser Install composer/semver to have access to this class and functionality + * @param string $packageName + * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package + * @return bool + */ + public static function satisfies(VersionParser $parser, $packageName, $constraint) + { + $constraint = $parser->parseConstraints((string) $constraint); + $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + + return $provided->matches($constraint); + } + + /** + * Returns a version constraint representing all the range(s) which are installed for a given package + * + * It is easier to use this via isInstalled() with the $constraint argument if you need to check + * whether a given version of a package is installed, and not just whether it exists + * + * @param string $packageName + * @return string Version constraint usable with composer/semver + */ + public static function getVersionRanges($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + $ranges = array(); + if (isset($installed['versions'][$packageName]['pretty_version'])) { + $ranges[] = $installed['versions'][$packageName]['pretty_version']; + } + if (array_key_exists('aliases', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); + } + if (array_key_exists('replaced', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); + } + if (array_key_exists('provided', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); + } + + return implode(' || ', $ranges); + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['version'])) { + return null; + } + + return $installed['versions'][$packageName]['version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getPrettyVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['pretty_version'])) { + return null; + } + + return $installed['versions'][$packageName]['pretty_version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference + */ + public static function getReference($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['reference'])) { + return null; + } + + return $installed['versions'][$packageName]['reference']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. + */ + public static function getInstallPath($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @return array + * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} + */ + public static function getRootPackage() + { + $installed = self::getInstalled(); + + return $installed[0]['root']; + } + + /** + * Returns the raw installed.php data for custom implementations + * + * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. + * @return array[] + * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} + */ + public static function getRawData() + { + @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = include __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + + return self::$installed; + } + + /** + * Returns the raw data of all installed.php which are currently loaded for custom implementations + * + * @return array[] + * @psalm-return list}> + */ + public static function getAllRawData() + { + return self::getInstalled(); + } + + /** + * Lets you reload the static array from another file + * + * This is only useful for complex integrations in which a project needs to use + * this class but then also needs to execute another project's autoloader in process, + * and wants to ensure both projects have access to their version of installed.php. + * + * A typical case would be PHPUnit, where it would need to make sure it reads all + * the data it needs from this class, then call reload() with + * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure + * the project in which it runs can then also use this class safely, without + * interference between PHPUnit's dependencies and the project's dependencies. + * + * @param array[] $data A vendor/composer/installed.php data set + * @return void + * + * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data + */ + public static function reload($data) + { + self::$installed = $data; + self::$installedByVendor = array(); + } + + /** + * @return string + */ + private static function getSelfDir() + { + if (self::$selfDir === null) { + self::$selfDir = strtr(__DIR__, '\\', '/'); + } + + return self::$selfDir; + } + + /** + * @return array[] + * @psalm-return list}> + */ + private static function getInstalled() + { + if (null === self::$canGetVendors) { + self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); + } + + $installed = array(); + $copiedLocalDir = false; + + if (self::$canGetVendors) { + foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + if (isset(self::$installedByVendor[$vendorDir])) { + $installed[] = self::$installedByVendor[$vendorDir]; + } elseif (is_file($vendorDir.'/composer/installed.php')) { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require $vendorDir.'/composer/installed.php'; + self::$installedByVendor[$vendorDir] = $required; + $installed[] = $required; + if (strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { + self::$installed = $required; + $copiedLocalDir = true; + } + } + } + } + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require __DIR__ . '/installed.php'; + self::$installed = $required; + } else { + self::$installed = array(); + } + } + + if (self::$installed !== array() && !$copiedLocalDir) { + $installed[] = self::$installed; + } + + return $installed; + } +} diff --git a/lib/vendor/composer/LICENSE b/lib/vendor/composer/LICENSE new file mode 100644 index 0000000..62ecfd8 --- /dev/null +++ b/lib/vendor/composer/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/lib/vendor/composer/autoload_classmap.php b/lib/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000..6e01649 --- /dev/null +++ b/lib/vendor/composer/autoload_classmap.php @@ -0,0 +1,370 @@ + $vendorDir . '/composer/InstalledVersions.php', + 'Psr\\Log\\AbstractLogger' => $vendorDir . '/psr/log/src/AbstractLogger.php', + 'Psr\\Log\\InvalidArgumentException' => $vendorDir . '/psr/log/src/InvalidArgumentException.php', + 'Psr\\Log\\LogLevel' => $vendorDir . '/psr/log/src/LogLevel.php', + 'Psr\\Log\\LoggerAwareInterface' => $vendorDir . '/psr/log/src/LoggerAwareInterface.php', + 'Psr\\Log\\LoggerAwareTrait' => $vendorDir . '/psr/log/src/LoggerAwareTrait.php', + 'Psr\\Log\\LoggerInterface' => $vendorDir . '/psr/log/src/LoggerInterface.php', + 'Psr\\Log\\LoggerTrait' => $vendorDir . '/psr/log/src/LoggerTrait.php', + 'Psr\\Log\\NullLogger' => $vendorDir . '/psr/log/src/NullLogger.php', + 'Sabre\\CalDAV\\Backend\\AbstractBackend' => $vendorDir . '/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php', + 'Sabre\\CalDAV\\Backend\\BackendInterface' => $vendorDir . '/sabre/dav/lib/CalDAV/Backend/BackendInterface.php', + 'Sabre\\CalDAV\\Backend\\NotificationSupport' => $vendorDir . '/sabre/dav/lib/CalDAV/Backend/NotificationSupport.php', + 'Sabre\\CalDAV\\Backend\\PDO' => $vendorDir . '/sabre/dav/lib/CalDAV/Backend/PDO.php', + 'Sabre\\CalDAV\\Backend\\SchedulingSupport' => $vendorDir . '/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php', + 'Sabre\\CalDAV\\Backend\\SharingSupport' => $vendorDir . '/sabre/dav/lib/CalDAV/Backend/SharingSupport.php', + 'Sabre\\CalDAV\\Backend\\SimplePDO' => $vendorDir . '/sabre/dav/lib/CalDAV/Backend/SimplePDO.php', + 'Sabre\\CalDAV\\Backend\\SubscriptionSupport' => $vendorDir . '/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php', + 'Sabre\\CalDAV\\Backend\\SyncSupport' => $vendorDir . '/sabre/dav/lib/CalDAV/Backend/SyncSupport.php', + 'Sabre\\CalDAV\\Calendar' => $vendorDir . '/sabre/dav/lib/CalDAV/Calendar.php', + 'Sabre\\CalDAV\\CalendarHome' => $vendorDir . '/sabre/dav/lib/CalDAV/CalendarHome.php', + 'Sabre\\CalDAV\\CalendarObject' => $vendorDir . '/sabre/dav/lib/CalDAV/CalendarObject.php', + 'Sabre\\CalDAV\\CalendarQueryValidator' => $vendorDir . '/sabre/dav/lib/CalDAV/CalendarQueryValidator.php', + 'Sabre\\CalDAV\\CalendarRoot' => $vendorDir . '/sabre/dav/lib/CalDAV/CalendarRoot.php', + 'Sabre\\CalDAV\\Exception\\InvalidComponentType' => $vendorDir . '/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php', + 'Sabre\\CalDAV\\ICSExportPlugin' => $vendorDir . '/sabre/dav/lib/CalDAV/ICSExportPlugin.php', + 'Sabre\\CalDAV\\ICalendar' => $vendorDir . '/sabre/dav/lib/CalDAV/ICalendar.php', + 'Sabre\\CalDAV\\ICalendarObject' => $vendorDir . '/sabre/dav/lib/CalDAV/ICalendarObject.php', + 'Sabre\\CalDAV\\ICalendarObjectContainer' => $vendorDir . '/sabre/dav/lib/CalDAV/ICalendarObjectContainer.php', + 'Sabre\\CalDAV\\ISharedCalendar' => $vendorDir . '/sabre/dav/lib/CalDAV/ISharedCalendar.php', + 'Sabre\\CalDAV\\Notifications\\Collection' => $vendorDir . '/sabre/dav/lib/CalDAV/Notifications/Collection.php', + 'Sabre\\CalDAV\\Notifications\\ICollection' => $vendorDir . '/sabre/dav/lib/CalDAV/Notifications/ICollection.php', + 'Sabre\\CalDAV\\Notifications\\INode' => $vendorDir . '/sabre/dav/lib/CalDAV/Notifications/INode.php', + 'Sabre\\CalDAV\\Notifications\\Node' => $vendorDir . '/sabre/dav/lib/CalDAV/Notifications/Node.php', + 'Sabre\\CalDAV\\Notifications\\Plugin' => $vendorDir . '/sabre/dav/lib/CalDAV/Notifications/Plugin.php', + 'Sabre\\CalDAV\\Plugin' => $vendorDir . '/sabre/dav/lib/CalDAV/Plugin.php', + 'Sabre\\CalDAV\\Principal\\Collection' => $vendorDir . '/sabre/dav/lib/CalDAV/Principal/Collection.php', + 'Sabre\\CalDAV\\Principal\\IProxyRead' => $vendorDir . '/sabre/dav/lib/CalDAV/Principal/IProxyRead.php', + 'Sabre\\CalDAV\\Principal\\IProxyWrite' => $vendorDir . '/sabre/dav/lib/CalDAV/Principal/IProxyWrite.php', + 'Sabre\\CalDAV\\Principal\\ProxyRead' => $vendorDir . '/sabre/dav/lib/CalDAV/Principal/ProxyRead.php', + 'Sabre\\CalDAV\\Principal\\ProxyWrite' => $vendorDir . '/sabre/dav/lib/CalDAV/Principal/ProxyWrite.php', + 'Sabre\\CalDAV\\Principal\\User' => $vendorDir . '/sabre/dav/lib/CalDAV/Principal/User.php', + 'Sabre\\CalDAV\\Schedule\\IInbox' => $vendorDir . '/sabre/dav/lib/CalDAV/Schedule/IInbox.php', + 'Sabre\\CalDAV\\Schedule\\IMipPlugin' => $vendorDir . '/sabre/dav/lib/CalDAV/Schedule/IMipPlugin.php', + 'Sabre\\CalDAV\\Schedule\\IOutbox' => $vendorDir . '/sabre/dav/lib/CalDAV/Schedule/IOutbox.php', + 'Sabre\\CalDAV\\Schedule\\ISchedulingObject' => $vendorDir . '/sabre/dav/lib/CalDAV/Schedule/ISchedulingObject.php', + 'Sabre\\CalDAV\\Schedule\\Inbox' => $vendorDir . '/sabre/dav/lib/CalDAV/Schedule/Inbox.php', + 'Sabre\\CalDAV\\Schedule\\Outbox' => $vendorDir . '/sabre/dav/lib/CalDAV/Schedule/Outbox.php', + 'Sabre\\CalDAV\\Schedule\\Plugin' => $vendorDir . '/sabre/dav/lib/CalDAV/Schedule/Plugin.php', + 'Sabre\\CalDAV\\Schedule\\SchedulingObject' => $vendorDir . '/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php', + 'Sabre\\CalDAV\\SharedCalendar' => $vendorDir . '/sabre/dav/lib/CalDAV/SharedCalendar.php', + 'Sabre\\CalDAV\\SharingPlugin' => $vendorDir . '/sabre/dav/lib/CalDAV/SharingPlugin.php', + 'Sabre\\CalDAV\\Subscriptions\\ISubscription' => $vendorDir . '/sabre/dav/lib/CalDAV/Subscriptions/ISubscription.php', + 'Sabre\\CalDAV\\Subscriptions\\Plugin' => $vendorDir . '/sabre/dav/lib/CalDAV/Subscriptions/Plugin.php', + 'Sabre\\CalDAV\\Subscriptions\\Subscription' => $vendorDir . '/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php', + 'Sabre\\CalDAV\\Xml\\Filter\\CalendarData' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Filter/CalendarData.php', + 'Sabre\\CalDAV\\Xml\\Filter\\CompFilter' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Filter/CompFilter.php', + 'Sabre\\CalDAV\\Xml\\Filter\\ParamFilter' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Filter/ParamFilter.php', + 'Sabre\\CalDAV\\Xml\\Filter\\PropFilter' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Filter/PropFilter.php', + 'Sabre\\CalDAV\\Xml\\Notification\\Invite' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php', + 'Sabre\\CalDAV\\Xml\\Notification\\InviteReply' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php', + 'Sabre\\CalDAV\\Xml\\Notification\\NotificationInterface' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Notification/NotificationInterface.php', + 'Sabre\\CalDAV\\Xml\\Notification\\SystemStatus' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Notification/SystemStatus.php', + 'Sabre\\CalDAV\\Xml\\Property\\AllowedSharingModes' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Property/AllowedSharingModes.php', + 'Sabre\\CalDAV\\Xml\\Property\\EmailAddressSet' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Property/EmailAddressSet.php', + 'Sabre\\CalDAV\\Xml\\Property\\Invite' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Property/Invite.php', + 'Sabre\\CalDAV\\Xml\\Property\\ScheduleCalendarTransp' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Property/ScheduleCalendarTransp.php', + 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarComponentSet.php', + 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarData' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarData.php', + 'Sabre\\CalDAV\\Xml\\Property\\SupportedCollationSet' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Property/SupportedCollationSet.php', + 'Sabre\\CalDAV\\Xml\\Request\\CalendarMultiGetReport' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Request/CalendarMultiGetReport.php', + 'Sabre\\CalDAV\\Xml\\Request\\CalendarQueryReport' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Request/CalendarQueryReport.php', + 'Sabre\\CalDAV\\Xml\\Request\\FreeBusyQueryReport' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Request/FreeBusyQueryReport.php', + 'Sabre\\CalDAV\\Xml\\Request\\InviteReply' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php', + 'Sabre\\CalDAV\\Xml\\Request\\MkCalendar' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Request/MkCalendar.php', + 'Sabre\\CalDAV\\Xml\\Request\\Share' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Request/Share.php', + 'Sabre\\CardDAV\\AddressBook' => $vendorDir . '/sabre/dav/lib/CardDAV/AddressBook.php', + 'Sabre\\CardDAV\\AddressBookHome' => $vendorDir . '/sabre/dav/lib/CardDAV/AddressBookHome.php', + 'Sabre\\CardDAV\\AddressBookRoot' => $vendorDir . '/sabre/dav/lib/CardDAV/AddressBookRoot.php', + 'Sabre\\CardDAV\\Backend\\AbstractBackend' => $vendorDir . '/sabre/dav/lib/CardDAV/Backend/AbstractBackend.php', + 'Sabre\\CardDAV\\Backend\\BackendInterface' => $vendorDir . '/sabre/dav/lib/CardDAV/Backend/BackendInterface.php', + 'Sabre\\CardDAV\\Backend\\PDO' => $vendorDir . '/sabre/dav/lib/CardDAV/Backend/PDO.php', + 'Sabre\\CardDAV\\Backend\\SyncSupport' => $vendorDir . '/sabre/dav/lib/CardDAV/Backend/SyncSupport.php', + 'Sabre\\CardDAV\\Card' => $vendorDir . '/sabre/dav/lib/CardDAV/Card.php', + 'Sabre\\CardDAV\\IAddressBook' => $vendorDir . '/sabre/dav/lib/CardDAV/IAddressBook.php', + 'Sabre\\CardDAV\\ICard' => $vendorDir . '/sabre/dav/lib/CardDAV/ICard.php', + 'Sabre\\CardDAV\\IDirectory' => $vendorDir . '/sabre/dav/lib/CardDAV/IDirectory.php', + 'Sabre\\CardDAV\\Plugin' => $vendorDir . '/sabre/dav/lib/CardDAV/Plugin.php', + 'Sabre\\CardDAV\\VCFExportPlugin' => $vendorDir . '/sabre/dav/lib/CardDAV/VCFExportPlugin.php', + 'Sabre\\CardDAV\\Xml\\Filter\\AddressData' => $vendorDir . '/sabre/dav/lib/CardDAV/Xml/Filter/AddressData.php', + 'Sabre\\CardDAV\\Xml\\Filter\\ParamFilter' => $vendorDir . '/sabre/dav/lib/CardDAV/Xml/Filter/ParamFilter.php', + 'Sabre\\CardDAV\\Xml\\Filter\\PropFilter' => $vendorDir . '/sabre/dav/lib/CardDAV/Xml/Filter/PropFilter.php', + 'Sabre\\CardDAV\\Xml\\Property\\SupportedAddressData' => $vendorDir . '/sabre/dav/lib/CardDAV/Xml/Property/SupportedAddressData.php', + 'Sabre\\CardDAV\\Xml\\Property\\SupportedCollationSet' => $vendorDir . '/sabre/dav/lib/CardDAV/Xml/Property/SupportedCollationSet.php', + 'Sabre\\CardDAV\\Xml\\Request\\AddressBookMultiGetReport' => $vendorDir . '/sabre/dav/lib/CardDAV/Xml/Request/AddressBookMultiGetReport.php', + 'Sabre\\CardDAV\\Xml\\Request\\AddressBookQueryReport' => $vendorDir . '/sabre/dav/lib/CardDAV/Xml/Request/AddressBookQueryReport.php', + 'Sabre\\DAVACL\\ACLTrait' => $vendorDir . '/sabre/dav/lib/DAVACL/ACLTrait.php', + 'Sabre\\DAVACL\\AbstractPrincipalCollection' => $vendorDir . '/sabre/dav/lib/DAVACL/AbstractPrincipalCollection.php', + 'Sabre\\DAVACL\\Exception\\AceConflict' => $vendorDir . '/sabre/dav/lib/DAVACL/Exception/AceConflict.php', + 'Sabre\\DAVACL\\Exception\\NeedPrivileges' => $vendorDir . '/sabre/dav/lib/DAVACL/Exception/NeedPrivileges.php', + 'Sabre\\DAVACL\\Exception\\NoAbstract' => $vendorDir . '/sabre/dav/lib/DAVACL/Exception/NoAbstract.php', + 'Sabre\\DAVACL\\Exception\\NotRecognizedPrincipal' => $vendorDir . '/sabre/dav/lib/DAVACL/Exception/NotRecognizedPrincipal.php', + 'Sabre\\DAVACL\\Exception\\NotSupportedPrivilege' => $vendorDir . '/sabre/dav/lib/DAVACL/Exception/NotSupportedPrivilege.php', + 'Sabre\\DAVACL\\FS\\Collection' => $vendorDir . '/sabre/dav/lib/DAVACL/FS/Collection.php', + 'Sabre\\DAVACL\\FS\\File' => $vendorDir . '/sabre/dav/lib/DAVACL/FS/File.php', + 'Sabre\\DAVACL\\FS\\HomeCollection' => $vendorDir . '/sabre/dav/lib/DAVACL/FS/HomeCollection.php', + 'Sabre\\DAVACL\\IACL' => $vendorDir . '/sabre/dav/lib/DAVACL/IACL.php', + 'Sabre\\DAVACL\\IPrincipal' => $vendorDir . '/sabre/dav/lib/DAVACL/IPrincipal.php', + 'Sabre\\DAVACL\\IPrincipalCollection' => $vendorDir . '/sabre/dav/lib/DAVACL/IPrincipalCollection.php', + 'Sabre\\DAVACL\\Plugin' => $vendorDir . '/sabre/dav/lib/DAVACL/Plugin.php', + 'Sabre\\DAVACL\\Principal' => $vendorDir . '/sabre/dav/lib/DAVACL/Principal.php', + 'Sabre\\DAVACL\\PrincipalBackend\\AbstractBackend' => $vendorDir . '/sabre/dav/lib/DAVACL/PrincipalBackend/AbstractBackend.php', + 'Sabre\\DAVACL\\PrincipalBackend\\BackendInterface' => $vendorDir . '/sabre/dav/lib/DAVACL/PrincipalBackend/BackendInterface.php', + 'Sabre\\DAVACL\\PrincipalBackend\\CreatePrincipalSupport' => $vendorDir . '/sabre/dav/lib/DAVACL/PrincipalBackend/CreatePrincipalSupport.php', + 'Sabre\\DAVACL\\PrincipalBackend\\PDO' => $vendorDir . '/sabre/dav/lib/DAVACL/PrincipalBackend/PDO.php', + 'Sabre\\DAVACL\\PrincipalCollection' => $vendorDir . '/sabre/dav/lib/DAVACL/PrincipalCollection.php', + 'Sabre\\DAVACL\\Xml\\Property\\Acl' => $vendorDir . '/sabre/dav/lib/DAVACL/Xml/Property/Acl.php', + 'Sabre\\DAVACL\\Xml\\Property\\AclRestrictions' => $vendorDir . '/sabre/dav/lib/DAVACL/Xml/Property/AclRestrictions.php', + 'Sabre\\DAVACL\\Xml\\Property\\CurrentUserPrivilegeSet' => $vendorDir . '/sabre/dav/lib/DAVACL/Xml/Property/CurrentUserPrivilegeSet.php', + 'Sabre\\DAVACL\\Xml\\Property\\Principal' => $vendorDir . '/sabre/dav/lib/DAVACL/Xml/Property/Principal.php', + 'Sabre\\DAVACL\\Xml\\Property\\SupportedPrivilegeSet' => $vendorDir . '/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php', + 'Sabre\\DAVACL\\Xml\\Request\\AclPrincipalPropSetReport' => $vendorDir . '/sabre/dav/lib/DAVACL/Xml/Request/AclPrincipalPropSetReport.php', + 'Sabre\\DAVACL\\Xml\\Request\\ExpandPropertyReport' => $vendorDir . '/sabre/dav/lib/DAVACL/Xml/Request/ExpandPropertyReport.php', + 'Sabre\\DAVACL\\Xml\\Request\\PrincipalMatchReport' => $vendorDir . '/sabre/dav/lib/DAVACL/Xml/Request/PrincipalMatchReport.php', + 'Sabre\\DAVACL\\Xml\\Request\\PrincipalPropertySearchReport' => $vendorDir . '/sabre/dav/lib/DAVACL/Xml/Request/PrincipalPropertySearchReport.php', + 'Sabre\\DAVACL\\Xml\\Request\\PrincipalSearchPropertySetReport' => $vendorDir . '/sabre/dav/lib/DAVACL/Xml/Request/PrincipalSearchPropertySetReport.php', + 'Sabre\\DAV\\Auth\\Backend\\AbstractBasic' => $vendorDir . '/sabre/dav/lib/DAV/Auth/Backend/AbstractBasic.php', + 'Sabre\\DAV\\Auth\\Backend\\AbstractBearer' => $vendorDir . '/sabre/dav/lib/DAV/Auth/Backend/AbstractBearer.php', + 'Sabre\\DAV\\Auth\\Backend\\AbstractDigest' => $vendorDir . '/sabre/dav/lib/DAV/Auth/Backend/AbstractDigest.php', + 'Sabre\\DAV\\Auth\\Backend\\Apache' => $vendorDir . '/sabre/dav/lib/DAV/Auth/Backend/Apache.php', + 'Sabre\\DAV\\Auth\\Backend\\BackendInterface' => $vendorDir . '/sabre/dav/lib/DAV/Auth/Backend/BackendInterface.php', + 'Sabre\\DAV\\Auth\\Backend\\BasicCallBack' => $vendorDir . '/sabre/dav/lib/DAV/Auth/Backend/BasicCallBack.php', + 'Sabre\\DAV\\Auth\\Backend\\File' => $vendorDir . '/sabre/dav/lib/DAV/Auth/Backend/File.php', + 'Sabre\\DAV\\Auth\\Backend\\IMAP' => $vendorDir . '/sabre/dav/lib/DAV/Auth/Backend/IMAP.php', + 'Sabre\\DAV\\Auth\\Backend\\PDO' => $vendorDir . '/sabre/dav/lib/DAV/Auth/Backend/PDO.php', + 'Sabre\\DAV\\Auth\\Backend\\PDOBasicAuth' => $vendorDir . '/sabre/dav/lib/DAV/Auth/Backend/PDOBasicAuth.php', + 'Sabre\\DAV\\Auth\\Plugin' => $vendorDir . '/sabre/dav/lib/DAV/Auth/Plugin.php', + 'Sabre\\DAV\\Browser\\GuessContentType' => $vendorDir . '/sabre/dav/lib/DAV/Browser/GuessContentType.php', + 'Sabre\\DAV\\Browser\\HtmlOutput' => $vendorDir . '/sabre/dav/lib/DAV/Browser/HtmlOutput.php', + 'Sabre\\DAV\\Browser\\HtmlOutputHelper' => $vendorDir . '/sabre/dav/lib/DAV/Browser/HtmlOutputHelper.php', + 'Sabre\\DAV\\Browser\\MapGetToPropFind' => $vendorDir . '/sabre/dav/lib/DAV/Browser/MapGetToPropFind.php', + 'Sabre\\DAV\\Browser\\Plugin' => $vendorDir . '/sabre/dav/lib/DAV/Browser/Plugin.php', + 'Sabre\\DAV\\Browser\\PropFindAll' => $vendorDir . '/sabre/dav/lib/DAV/Browser/PropFindAll.php', + 'Sabre\\DAV\\Client' => $vendorDir . '/sabre/dav/lib/DAV/Client.php', + 'Sabre\\DAV\\Collection' => $vendorDir . '/sabre/dav/lib/DAV/Collection.php', + 'Sabre\\DAV\\CorePlugin' => $vendorDir . '/sabre/dav/lib/DAV/CorePlugin.php', + 'Sabre\\DAV\\Exception' => $vendorDir . '/sabre/dav/lib/DAV/Exception.php', + 'Sabre\\DAV\\Exception\\BadRequest' => $vendorDir . '/sabre/dav/lib/DAV/Exception/BadRequest.php', + 'Sabre\\DAV\\Exception\\Conflict' => $vendorDir . '/sabre/dav/lib/DAV/Exception/Conflict.php', + 'Sabre\\DAV\\Exception\\ConflictingLock' => $vendorDir . '/sabre/dav/lib/DAV/Exception/ConflictingLock.php', + 'Sabre\\DAV\\Exception\\Forbidden' => $vendorDir . '/sabre/dav/lib/DAV/Exception/Forbidden.php', + 'Sabre\\DAV\\Exception\\InsufficientStorage' => $vendorDir . '/sabre/dav/lib/DAV/Exception/InsufficientStorage.php', + 'Sabre\\DAV\\Exception\\InvalidResourceType' => $vendorDir . '/sabre/dav/lib/DAV/Exception/InvalidResourceType.php', + 'Sabre\\DAV\\Exception\\InvalidSyncToken' => $vendorDir . '/sabre/dav/lib/DAV/Exception/InvalidSyncToken.php', + 'Sabre\\DAV\\Exception\\LengthRequired' => $vendorDir . '/sabre/dav/lib/DAV/Exception/LengthRequired.php', + 'Sabre\\DAV\\Exception\\LockTokenMatchesRequestUri' => $vendorDir . '/sabre/dav/lib/DAV/Exception/LockTokenMatchesRequestUri.php', + 'Sabre\\DAV\\Exception\\Locked' => $vendorDir . '/sabre/dav/lib/DAV/Exception/Locked.php', + 'Sabre\\DAV\\Exception\\MethodNotAllowed' => $vendorDir . '/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php', + 'Sabre\\DAV\\Exception\\NotAuthenticated' => $vendorDir . '/sabre/dav/lib/DAV/Exception/NotAuthenticated.php', + 'Sabre\\DAV\\Exception\\NotFound' => $vendorDir . '/sabre/dav/lib/DAV/Exception/NotFound.php', + 'Sabre\\DAV\\Exception\\NotImplemented' => $vendorDir . '/sabre/dav/lib/DAV/Exception/NotImplemented.php', + 'Sabre\\DAV\\Exception\\PaymentRequired' => $vendorDir . '/sabre/dav/lib/DAV/Exception/PaymentRequired.php', + 'Sabre\\DAV\\Exception\\PreconditionFailed' => $vendorDir . '/sabre/dav/lib/DAV/Exception/PreconditionFailed.php', + 'Sabre\\DAV\\Exception\\ReportNotSupported' => $vendorDir . '/sabre/dav/lib/DAV/Exception/ReportNotSupported.php', + 'Sabre\\DAV\\Exception\\RequestedRangeNotSatisfiable' => $vendorDir . '/sabre/dav/lib/DAV/Exception/RequestedRangeNotSatisfiable.php', + 'Sabre\\DAV\\Exception\\ServiceUnavailable' => $vendorDir . '/sabre/dav/lib/DAV/Exception/ServiceUnavailable.php', + 'Sabre\\DAV\\Exception\\TooManyMatches' => $vendorDir . '/sabre/dav/lib/DAV/Exception/TooManyMatches.php', + 'Sabre\\DAV\\Exception\\UnsupportedMediaType' => $vendorDir . '/sabre/dav/lib/DAV/Exception/UnsupportedMediaType.php', + 'Sabre\\DAV\\FSExt\\Directory' => $vendorDir . '/sabre/dav/lib/DAV/FSExt/Directory.php', + 'Sabre\\DAV\\FSExt\\File' => $vendorDir . '/sabre/dav/lib/DAV/FSExt/File.php', + 'Sabre\\DAV\\FS\\Directory' => $vendorDir . '/sabre/dav/lib/DAV/FS/Directory.php', + 'Sabre\\DAV\\FS\\File' => $vendorDir . '/sabre/dav/lib/DAV/FS/File.php', + 'Sabre\\DAV\\FS\\Node' => $vendorDir . '/sabre/dav/lib/DAV/FS/Node.php', + 'Sabre\\DAV\\File' => $vendorDir . '/sabre/dav/lib/DAV/File.php', + 'Sabre\\DAV\\ICollection' => $vendorDir . '/sabre/dav/lib/DAV/ICollection.php', + 'Sabre\\DAV\\ICopyTarget' => $vendorDir . '/sabre/dav/lib/DAV/ICopyTarget.php', + 'Sabre\\DAV\\IExtendedCollection' => $vendorDir . '/sabre/dav/lib/DAV/IExtendedCollection.php', + 'Sabre\\DAV\\IFile' => $vendorDir . '/sabre/dav/lib/DAV/IFile.php', + 'Sabre\\DAV\\IMoveTarget' => $vendorDir . '/sabre/dav/lib/DAV/IMoveTarget.php', + 'Sabre\\DAV\\IMultiGet' => $vendorDir . '/sabre/dav/lib/DAV/IMultiGet.php', + 'Sabre\\DAV\\INode' => $vendorDir . '/sabre/dav/lib/DAV/INode.php', + 'Sabre\\DAV\\INodeByPath' => $vendorDir . '/sabre/dav/lib/DAV/INodeByPath.php', + 'Sabre\\DAV\\IProperties' => $vendorDir . '/sabre/dav/lib/DAV/IProperties.php', + 'Sabre\\DAV\\IQuota' => $vendorDir . '/sabre/dav/lib/DAV/IQuota.php', + 'Sabre\\DAV\\Locks\\Backend\\AbstractBackend' => $vendorDir . '/sabre/dav/lib/DAV/Locks/Backend/AbstractBackend.php', + 'Sabre\\DAV\\Locks\\Backend\\BackendInterface' => $vendorDir . '/sabre/dav/lib/DAV/Locks/Backend/BackendInterface.php', + 'Sabre\\DAV\\Locks\\Backend\\File' => $vendorDir . '/sabre/dav/lib/DAV/Locks/Backend/File.php', + 'Sabre\\DAV\\Locks\\Backend\\PDO' => $vendorDir . '/sabre/dav/lib/DAV/Locks/Backend/PDO.php', + 'Sabre\\DAV\\Locks\\LockInfo' => $vendorDir . '/sabre/dav/lib/DAV/Locks/LockInfo.php', + 'Sabre\\DAV\\Locks\\Plugin' => $vendorDir . '/sabre/dav/lib/DAV/Locks/Plugin.php', + 'Sabre\\DAV\\MkCol' => $vendorDir . '/sabre/dav/lib/DAV/MkCol.php', + 'Sabre\\DAV\\Mount\\Plugin' => $vendorDir . '/sabre/dav/lib/DAV/Mount/Plugin.php', + 'Sabre\\DAV\\Node' => $vendorDir . '/sabre/dav/lib/DAV/Node.php', + 'Sabre\\DAV\\PartialUpdate\\IPatchSupport' => $vendorDir . '/sabre/dav/lib/DAV/PartialUpdate/IPatchSupport.php', + 'Sabre\\DAV\\PartialUpdate\\Plugin' => $vendorDir . '/sabre/dav/lib/DAV/PartialUpdate/Plugin.php', + 'Sabre\\DAV\\PropFind' => $vendorDir . '/sabre/dav/lib/DAV/PropFind.php', + 'Sabre\\DAV\\PropPatch' => $vendorDir . '/sabre/dav/lib/DAV/PropPatch.php', + 'Sabre\\DAV\\PropertyStorage\\Backend\\BackendInterface' => $vendorDir . '/sabre/dav/lib/DAV/PropertyStorage/Backend/BackendInterface.php', + 'Sabre\\DAV\\PropertyStorage\\Backend\\PDO' => $vendorDir . '/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php', + 'Sabre\\DAV\\PropertyStorage\\Plugin' => $vendorDir . '/sabre/dav/lib/DAV/PropertyStorage/Plugin.php', + 'Sabre\\DAV\\Server' => $vendorDir . '/sabre/dav/lib/DAV/Server.php', + 'Sabre\\DAV\\ServerPlugin' => $vendorDir . '/sabre/dav/lib/DAV/ServerPlugin.php', + 'Sabre\\DAV\\Sharing\\ISharedNode' => $vendorDir . '/sabre/dav/lib/DAV/Sharing/ISharedNode.php', + 'Sabre\\DAV\\Sharing\\Plugin' => $vendorDir . '/sabre/dav/lib/DAV/Sharing/Plugin.php', + 'Sabre\\DAV\\SimpleCollection' => $vendorDir . '/sabre/dav/lib/DAV/SimpleCollection.php', + 'Sabre\\DAV\\SimpleFile' => $vendorDir . '/sabre/dav/lib/DAV/SimpleFile.php', + 'Sabre\\DAV\\StringUtil' => $vendorDir . '/sabre/dav/lib/DAV/StringUtil.php', + 'Sabre\\DAV\\Sync\\ISyncCollection' => $vendorDir . '/sabre/dav/lib/DAV/Sync/ISyncCollection.php', + 'Sabre\\DAV\\Sync\\Plugin' => $vendorDir . '/sabre/dav/lib/DAV/Sync/Plugin.php', + 'Sabre\\DAV\\TemporaryFileFilterPlugin' => $vendorDir . '/sabre/dav/lib/DAV/TemporaryFileFilterPlugin.php', + 'Sabre\\DAV\\Tree' => $vendorDir . '/sabre/dav/lib/DAV/Tree.php', + 'Sabre\\DAV\\UUIDUtil' => $vendorDir . '/sabre/dav/lib/DAV/UUIDUtil.php', + 'Sabre\\DAV\\Version' => $vendorDir . '/sabre/dav/lib/DAV/Version.php', + 'Sabre\\DAV\\Xml\\Element\\Prop' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Element/Prop.php', + 'Sabre\\DAV\\Xml\\Element\\Response' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Element/Response.php', + 'Sabre\\DAV\\Xml\\Element\\Sharee' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Element/Sharee.php', + 'Sabre\\DAV\\Xml\\Property\\Complex' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Property/Complex.php', + 'Sabre\\DAV\\Xml\\Property\\GetLastModified' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Property/GetLastModified.php', + 'Sabre\\DAV\\Xml\\Property\\Href' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Property/Href.php', + 'Sabre\\DAV\\Xml\\Property\\Invite' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Property/Invite.php', + 'Sabre\\DAV\\Xml\\Property\\LocalHref' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Property/LocalHref.php', + 'Sabre\\DAV\\Xml\\Property\\LockDiscovery' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Property/LockDiscovery.php', + 'Sabre\\DAV\\Xml\\Property\\ResourceType' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Property/ResourceType.php', + 'Sabre\\DAV\\Xml\\Property\\ShareAccess' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Property/ShareAccess.php', + 'Sabre\\DAV\\Xml\\Property\\SupportedLock' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Property/SupportedLock.php', + 'Sabre\\DAV\\Xml\\Property\\SupportedMethodSet' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Property/SupportedMethodSet.php', + 'Sabre\\DAV\\Xml\\Property\\SupportedReportSet' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Property/SupportedReportSet.php', + 'Sabre\\DAV\\Xml\\Request\\Lock' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Request/Lock.php', + 'Sabre\\DAV\\Xml\\Request\\MkCol' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Request/MkCol.php', + 'Sabre\\DAV\\Xml\\Request\\PropFind' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Request/PropFind.php', + 'Sabre\\DAV\\Xml\\Request\\PropPatch' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Request/PropPatch.php', + 'Sabre\\DAV\\Xml\\Request\\ShareResource' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Request/ShareResource.php', + 'Sabre\\DAV\\Xml\\Request\\SyncCollectionReport' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Request/SyncCollectionReport.php', + 'Sabre\\DAV\\Xml\\Response\\MultiStatus' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Response/MultiStatus.php', + 'Sabre\\DAV\\Xml\\Service' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Service.php', + 'Sabre\\Event\\Emitter' => $vendorDir . '/sabre/event/lib/Emitter.php', + 'Sabre\\Event\\EmitterInterface' => $vendorDir . '/sabre/event/lib/EmitterInterface.php', + 'Sabre\\Event\\EmitterTrait' => $vendorDir . '/sabre/event/lib/EmitterTrait.php', + 'Sabre\\Event\\EventEmitter' => $vendorDir . '/sabre/event/lib/EventEmitter.php', + 'Sabre\\Event\\Loop\\Loop' => $vendorDir . '/sabre/event/lib/Loop/Loop.php', + 'Sabre\\Event\\Promise' => $vendorDir . '/sabre/event/lib/Promise.php', + 'Sabre\\Event\\PromiseAlreadyResolvedException' => $vendorDir . '/sabre/event/lib/PromiseAlreadyResolvedException.php', + 'Sabre\\Event\\Version' => $vendorDir . '/sabre/event/lib/Version.php', + 'Sabre\\Event\\WildcardEmitter' => $vendorDir . '/sabre/event/lib/WildcardEmitter.php', + 'Sabre\\Event\\WildcardEmitterTrait' => $vendorDir . '/sabre/event/lib/WildcardEmitterTrait.php', + 'Sabre\\HTTP\\Auth\\AWS' => $vendorDir . '/sabre/http/lib/Auth/AWS.php', + 'Sabre\\HTTP\\Auth\\AbstractAuth' => $vendorDir . '/sabre/http/lib/Auth/AbstractAuth.php', + 'Sabre\\HTTP\\Auth\\Basic' => $vendorDir . '/sabre/http/lib/Auth/Basic.php', + 'Sabre\\HTTP\\Auth\\Bearer' => $vendorDir . '/sabre/http/lib/Auth/Bearer.php', + 'Sabre\\HTTP\\Auth\\Digest' => $vendorDir . '/sabre/http/lib/Auth/Digest.php', + 'Sabre\\HTTP\\Client' => $vendorDir . '/sabre/http/lib/Client.php', + 'Sabre\\HTTP\\ClientException' => $vendorDir . '/sabre/http/lib/ClientException.php', + 'Sabre\\HTTP\\ClientHttpException' => $vendorDir . '/sabre/http/lib/ClientHttpException.php', + 'Sabre\\HTTP\\HttpException' => $vendorDir . '/sabre/http/lib/HttpException.php', + 'Sabre\\HTTP\\Message' => $vendorDir . '/sabre/http/lib/Message.php', + 'Sabre\\HTTP\\MessageDecoratorTrait' => $vendorDir . '/sabre/http/lib/MessageDecoratorTrait.php', + 'Sabre\\HTTP\\MessageInterface' => $vendorDir . '/sabre/http/lib/MessageInterface.php', + 'Sabre\\HTTP\\Request' => $vendorDir . '/sabre/http/lib/Request.php', + 'Sabre\\HTTP\\RequestDecorator' => $vendorDir . '/sabre/http/lib/RequestDecorator.php', + 'Sabre\\HTTP\\RequestInterface' => $vendorDir . '/sabre/http/lib/RequestInterface.php', + 'Sabre\\HTTP\\Response' => $vendorDir . '/sabre/http/lib/Response.php', + 'Sabre\\HTTP\\ResponseDecorator' => $vendorDir . '/sabre/http/lib/ResponseDecorator.php', + 'Sabre\\HTTP\\ResponseInterface' => $vendorDir . '/sabre/http/lib/ResponseInterface.php', + 'Sabre\\HTTP\\Sapi' => $vendorDir . '/sabre/http/lib/Sapi.php', + 'Sabre\\HTTP\\Version' => $vendorDir . '/sabre/http/lib/Version.php', + 'Sabre\\Uri\\InvalidUriException' => $vendorDir . '/sabre/uri/lib/InvalidUriException.php', + 'Sabre\\Uri\\Version' => $vendorDir . '/sabre/uri/lib/Version.php', + 'Sabre\\VObject\\BirthdayCalendarGenerator' => $vendorDir . '/sabre/vobject/lib/BirthdayCalendarGenerator.php', + 'Sabre\\VObject\\Cli' => $vendorDir . '/sabre/vobject/lib/Cli.php', + 'Sabre\\VObject\\Component' => $vendorDir . '/sabre/vobject/lib/Component.php', + 'Sabre\\VObject\\Component\\Available' => $vendorDir . '/sabre/vobject/lib/Component/Available.php', + 'Sabre\\VObject\\Component\\VAlarm' => $vendorDir . '/sabre/vobject/lib/Component/VAlarm.php', + 'Sabre\\VObject\\Component\\VAvailability' => $vendorDir . '/sabre/vobject/lib/Component/VAvailability.php', + 'Sabre\\VObject\\Component\\VCalendar' => $vendorDir . '/sabre/vobject/lib/Component/VCalendar.php', + 'Sabre\\VObject\\Component\\VCard' => $vendorDir . '/sabre/vobject/lib/Component/VCard.php', + 'Sabre\\VObject\\Component\\VEvent' => $vendorDir . '/sabre/vobject/lib/Component/VEvent.php', + 'Sabre\\VObject\\Component\\VFreeBusy' => $vendorDir . '/sabre/vobject/lib/Component/VFreeBusy.php', + 'Sabre\\VObject\\Component\\VJournal' => $vendorDir . '/sabre/vobject/lib/Component/VJournal.php', + 'Sabre\\VObject\\Component\\VTimeZone' => $vendorDir . '/sabre/vobject/lib/Component/VTimeZone.php', + 'Sabre\\VObject\\Component\\VTodo' => $vendorDir . '/sabre/vobject/lib/Component/VTodo.php', + 'Sabre\\VObject\\DateTimeParser' => $vendorDir . '/sabre/vobject/lib/DateTimeParser.php', + 'Sabre\\VObject\\Document' => $vendorDir . '/sabre/vobject/lib/Document.php', + 'Sabre\\VObject\\ElementList' => $vendorDir . '/sabre/vobject/lib/ElementList.php', + 'Sabre\\VObject\\EofException' => $vendorDir . '/sabre/vobject/lib/EofException.php', + 'Sabre\\VObject\\FreeBusyData' => $vendorDir . '/sabre/vobject/lib/FreeBusyData.php', + 'Sabre\\VObject\\FreeBusyGenerator' => $vendorDir . '/sabre/vobject/lib/FreeBusyGenerator.php', + 'Sabre\\VObject\\ITip\\Broker' => $vendorDir . '/sabre/vobject/lib/ITip/Broker.php', + 'Sabre\\VObject\\ITip\\ITipException' => $vendorDir . '/sabre/vobject/lib/ITip/ITipException.php', + 'Sabre\\VObject\\ITip\\Message' => $vendorDir . '/sabre/vobject/lib/ITip/Message.php', + 'Sabre\\VObject\\ITip\\SameOrganizerForAllComponentsException' => $vendorDir . '/sabre/vobject/lib/ITip/SameOrganizerForAllComponentsException.php', + 'Sabre\\VObject\\InvalidDataException' => $vendorDir . '/sabre/vobject/lib/InvalidDataException.php', + 'Sabre\\VObject\\Node' => $vendorDir . '/sabre/vobject/lib/Node.php', + 'Sabre\\VObject\\PHPUnitAssertions' => $vendorDir . '/sabre/vobject/lib/PHPUnitAssertions.php', + 'Sabre\\VObject\\Parameter' => $vendorDir . '/sabre/vobject/lib/Parameter.php', + 'Sabre\\VObject\\ParseException' => $vendorDir . '/sabre/vobject/lib/ParseException.php', + 'Sabre\\VObject\\Parser\\Json' => $vendorDir . '/sabre/vobject/lib/Parser/Json.php', + 'Sabre\\VObject\\Parser\\MimeDir' => $vendorDir . '/sabre/vobject/lib/Parser/MimeDir.php', + 'Sabre\\VObject\\Parser\\Parser' => $vendorDir . '/sabre/vobject/lib/Parser/Parser.php', + 'Sabre\\VObject\\Parser\\XML' => $vendorDir . '/sabre/vobject/lib/Parser/XML.php', + 'Sabre\\VObject\\Parser\\XML\\Element\\KeyValue' => $vendorDir . '/sabre/vobject/lib/Parser/XML/Element/KeyValue.php', + 'Sabre\\VObject\\Property' => $vendorDir . '/sabre/vobject/lib/Property.php', + 'Sabre\\VObject\\Property\\Binary' => $vendorDir . '/sabre/vobject/lib/Property/Binary.php', + 'Sabre\\VObject\\Property\\Boolean' => $vendorDir . '/sabre/vobject/lib/Property/Boolean.php', + 'Sabre\\VObject\\Property\\FlatText' => $vendorDir . '/sabre/vobject/lib/Property/FlatText.php', + 'Sabre\\VObject\\Property\\FloatValue' => $vendorDir . '/sabre/vobject/lib/Property/FloatValue.php', + 'Sabre\\VObject\\Property\\ICalendar\\CalAddress' => $vendorDir . '/sabre/vobject/lib/Property/ICalendar/CalAddress.php', + 'Sabre\\VObject\\Property\\ICalendar\\Date' => $vendorDir . '/sabre/vobject/lib/Property/ICalendar/Date.php', + 'Sabre\\VObject\\Property\\ICalendar\\DateTime' => $vendorDir . '/sabre/vobject/lib/Property/ICalendar/DateTime.php', + 'Sabre\\VObject\\Property\\ICalendar\\Duration' => $vendorDir . '/sabre/vobject/lib/Property/ICalendar/Duration.php', + 'Sabre\\VObject\\Property\\ICalendar\\Period' => $vendorDir . '/sabre/vobject/lib/Property/ICalendar/Period.php', + 'Sabre\\VObject\\Property\\ICalendar\\Recur' => $vendorDir . '/sabre/vobject/lib/Property/ICalendar/Recur.php', + 'Sabre\\VObject\\Property\\IntegerValue' => $vendorDir . '/sabre/vobject/lib/Property/IntegerValue.php', + 'Sabre\\VObject\\Property\\Text' => $vendorDir . '/sabre/vobject/lib/Property/Text.php', + 'Sabre\\VObject\\Property\\Time' => $vendorDir . '/sabre/vobject/lib/Property/Time.php', + 'Sabre\\VObject\\Property\\Unknown' => $vendorDir . '/sabre/vobject/lib/Property/Unknown.php', + 'Sabre\\VObject\\Property\\Uri' => $vendorDir . '/sabre/vobject/lib/Property/Uri.php', + 'Sabre\\VObject\\Property\\UtcOffset' => $vendorDir . '/sabre/vobject/lib/Property/UtcOffset.php', + 'Sabre\\VObject\\Property\\VCard\\Date' => $vendorDir . '/sabre/vobject/lib/Property/VCard/Date.php', + 'Sabre\\VObject\\Property\\VCard\\DateAndOrTime' => $vendorDir . '/sabre/vobject/lib/Property/VCard/DateAndOrTime.php', + 'Sabre\\VObject\\Property\\VCard\\DateTime' => $vendorDir . '/sabre/vobject/lib/Property/VCard/DateTime.php', + 'Sabre\\VObject\\Property\\VCard\\LanguageTag' => $vendorDir . '/sabre/vobject/lib/Property/VCard/LanguageTag.php', + 'Sabre\\VObject\\Property\\VCard\\PhoneNumber' => $vendorDir . '/sabre/vobject/lib/Property/VCard/PhoneNumber.php', + 'Sabre\\VObject\\Property\\VCard\\TimeStamp' => $vendorDir . '/sabre/vobject/lib/Property/VCard/TimeStamp.php', + 'Sabre\\VObject\\Reader' => $vendorDir . '/sabre/vobject/lib/Reader.php', + 'Sabre\\VObject\\Recur\\EventIterator' => $vendorDir . '/sabre/vobject/lib/Recur/EventIterator.php', + 'Sabre\\VObject\\Recur\\MaxInstancesExceededException' => $vendorDir . '/sabre/vobject/lib/Recur/MaxInstancesExceededException.php', + 'Sabre\\VObject\\Recur\\NoInstancesException' => $vendorDir . '/sabre/vobject/lib/Recur/NoInstancesException.php', + 'Sabre\\VObject\\Recur\\RDateIterator' => $vendorDir . '/sabre/vobject/lib/Recur/RDateIterator.php', + 'Sabre\\VObject\\Recur\\RRuleIterator' => $vendorDir . '/sabre/vobject/lib/Recur/RRuleIterator.php', + 'Sabre\\VObject\\Settings' => $vendorDir . '/sabre/vobject/lib/Settings.php', + 'Sabre\\VObject\\Splitter\\ICalendar' => $vendorDir . '/sabre/vobject/lib/Splitter/ICalendar.php', + 'Sabre\\VObject\\Splitter\\SplitterInterface' => $vendorDir . '/sabre/vobject/lib/Splitter/SplitterInterface.php', + 'Sabre\\VObject\\Splitter\\VCard' => $vendorDir . '/sabre/vobject/lib/Splitter/VCard.php', + 'Sabre\\VObject\\StringUtil' => $vendorDir . '/sabre/vobject/lib/StringUtil.php', + 'Sabre\\VObject\\TimeZoneUtil' => $vendorDir . '/sabre/vobject/lib/TimeZoneUtil.php', + 'Sabre\\VObject\\TimezoneGuesser\\FindFromOffset' => $vendorDir . '/sabre/vobject/lib/TimezoneGuesser/FindFromOffset.php', + 'Sabre\\VObject\\TimezoneGuesser\\FindFromTimezoneIdentifier' => $vendorDir . '/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneIdentifier.php', + 'Sabre\\VObject\\TimezoneGuesser\\FindFromTimezoneMap' => $vendorDir . '/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneMap.php', + 'Sabre\\VObject\\TimezoneGuesser\\GuessFromLicEntry' => $vendorDir . '/sabre/vobject/lib/TimezoneGuesser/GuessFromLicEntry.php', + 'Sabre\\VObject\\TimezoneGuesser\\GuessFromMsTzId' => $vendorDir . '/sabre/vobject/lib/TimezoneGuesser/GuessFromMsTzId.php', + 'Sabre\\VObject\\TimezoneGuesser\\TimezoneFinder' => $vendorDir . '/sabre/vobject/lib/TimezoneGuesser/TimezoneFinder.php', + 'Sabre\\VObject\\TimezoneGuesser\\TimezoneGuesser' => $vendorDir . '/sabre/vobject/lib/TimezoneGuesser/TimezoneGuesser.php', + 'Sabre\\VObject\\UUIDUtil' => $vendorDir . '/sabre/vobject/lib/UUIDUtil.php', + 'Sabre\\VObject\\VCardConverter' => $vendorDir . '/sabre/vobject/lib/VCardConverter.php', + 'Sabre\\VObject\\Version' => $vendorDir . '/sabre/vobject/lib/Version.php', + 'Sabre\\VObject\\Writer' => $vendorDir . '/sabre/vobject/lib/Writer.php', + 'Sabre\\Xml\\ContextStackTrait' => $vendorDir . '/sabre/xml/lib/ContextStackTrait.php', + 'Sabre\\Xml\\Element' => $vendorDir . '/sabre/xml/lib/Element.php', + 'Sabre\\Xml\\Element\\Base' => $vendorDir . '/sabre/xml/lib/Element/Base.php', + 'Sabre\\Xml\\Element\\Cdata' => $vendorDir . '/sabre/xml/lib/Element/Cdata.php', + 'Sabre\\Xml\\Element\\Elements' => $vendorDir . '/sabre/xml/lib/Element/Elements.php', + 'Sabre\\Xml\\Element\\KeyValue' => $vendorDir . '/sabre/xml/lib/Element/KeyValue.php', + 'Sabre\\Xml\\Element\\Uri' => $vendorDir . '/sabre/xml/lib/Element/Uri.php', + 'Sabre\\Xml\\Element\\XmlFragment' => $vendorDir . '/sabre/xml/lib/Element/XmlFragment.php', + 'Sabre\\Xml\\LibXMLException' => $vendorDir . '/sabre/xml/lib/LibXMLException.php', + 'Sabre\\Xml\\ParseException' => $vendorDir . '/sabre/xml/lib/ParseException.php', + 'Sabre\\Xml\\Reader' => $vendorDir . '/sabre/xml/lib/Reader.php', + 'Sabre\\Xml\\Service' => $vendorDir . '/sabre/xml/lib/Service.php', + 'Sabre\\Xml\\Version' => $vendorDir . '/sabre/xml/lib/Version.php', + 'Sabre\\Xml\\Writer' => $vendorDir . '/sabre/xml/lib/Writer.php', + 'Sabre\\Xml\\XmlDeserializable' => $vendorDir . '/sabre/xml/lib/XmlDeserializable.php', + 'Sabre\\Xml\\XmlSerializable' => $vendorDir . '/sabre/xml/lib/XmlSerializable.php', +); diff --git a/lib/vendor/composer/autoload_files.php b/lib/vendor/composer/autoload_files.php new file mode 100644 index 0000000..674815f --- /dev/null +++ b/lib/vendor/composer/autoload_files.php @@ -0,0 +1,16 @@ + $vendorDir . '/sabre/uri/lib/functions.php', + '2b9d0f43f9552984cfa82fee95491826' => $vendorDir . '/sabre/event/lib/coroutine.php', + 'd81bab31d3feb45bfe2f283ea3c8fdf7' => $vendorDir . '/sabre/event/lib/Loop/functions.php', + 'a1cce3d26cc15c00fcd0b3354bd72c88' => $vendorDir . '/sabre/event/lib/Promise/functions.php', + '3569eecfeed3bcf0bad3c998a494ecb8' => $vendorDir . '/sabre/xml/lib/Deserializer/functions.php', + '93aa591bc4ca510c520999e34229ee79' => $vendorDir . '/sabre/xml/lib/Serializer/functions.php', + 'ebdb698ed4152ae445614b69b5e4bb6a' => $vendorDir . '/sabre/http/lib/functions.php', +); diff --git a/lib/vendor/composer/autoload_namespaces.php b/lib/vendor/composer/autoload_namespaces.php new file mode 100644 index 0000000..f1ae7a0 --- /dev/null +++ b/lib/vendor/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ + array($vendorDir . '/sabre/xml/lib'), + 'Sabre\\VObject\\' => array($vendorDir . '/sabre/vobject/lib'), + 'Sabre\\Uri\\' => array($vendorDir . '/sabre/uri/lib'), + 'Sabre\\HTTP\\' => array($vendorDir . '/sabre/http/lib'), + 'Sabre\\Event\\' => array($vendorDir . '/sabre/event/lib'), + 'Sabre\\' => array($vendorDir . '/sabre/dav/lib'), + 'Psr\\Log\\' => array($vendorDir . '/psr/log/src'), +); diff --git a/lib/vendor/composer/autoload_real.php b/lib/vendor/composer/autoload_real.php new file mode 100644 index 0000000..6286096 --- /dev/null +++ b/lib/vendor/composer/autoload_real.php @@ -0,0 +1,50 @@ +register(true); + + $filesToLoad = \Composer\Autoload\ComposerStaticInita07591154e0386cfc9ad0f9217878d04::$files; + $requireFile = \Closure::bind(static function ($fileIdentifier, $file) { + if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; + + require $file; + } + }, null, null); + foreach ($filesToLoad as $fileIdentifier => $file) { + $requireFile($fileIdentifier, $file); + } + + return $loader; + } +} diff --git a/lib/vendor/composer/autoload_static.php b/lib/vendor/composer/autoload_static.php new file mode 100644 index 0000000..307e983 --- /dev/null +++ b/lib/vendor/composer/autoload_static.php @@ -0,0 +1,439 @@ + __DIR__ . '/..' . '/sabre/uri/lib/functions.php', + '2b9d0f43f9552984cfa82fee95491826' => __DIR__ . '/..' . '/sabre/event/lib/coroutine.php', + 'd81bab31d3feb45bfe2f283ea3c8fdf7' => __DIR__ . '/..' . '/sabre/event/lib/Loop/functions.php', + 'a1cce3d26cc15c00fcd0b3354bd72c88' => __DIR__ . '/..' . '/sabre/event/lib/Promise/functions.php', + '3569eecfeed3bcf0bad3c998a494ecb8' => __DIR__ . '/..' . '/sabre/xml/lib/Deserializer/functions.php', + '93aa591bc4ca510c520999e34229ee79' => __DIR__ . '/..' . '/sabre/xml/lib/Serializer/functions.php', + 'ebdb698ed4152ae445614b69b5e4bb6a' => __DIR__ . '/..' . '/sabre/http/lib/functions.php', + ); + + public static $prefixLengthsPsr4 = array ( + 'S' => + array ( + 'Sabre\\Xml\\' => 10, + 'Sabre\\VObject\\' => 14, + 'Sabre\\Uri\\' => 10, + 'Sabre\\HTTP\\' => 11, + 'Sabre\\Event\\' => 12, + 'Sabre\\' => 6, + ), + 'P' => + array ( + 'Psr\\Log\\' => 8, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'Sabre\\Xml\\' => + array ( + 0 => __DIR__ . '/..' . '/sabre/xml/lib', + ), + 'Sabre\\VObject\\' => + array ( + 0 => __DIR__ . '/..' . '/sabre/vobject/lib', + ), + 'Sabre\\Uri\\' => + array ( + 0 => __DIR__ . '/..' . '/sabre/uri/lib', + ), + 'Sabre\\HTTP\\' => + array ( + 0 => __DIR__ . '/..' . '/sabre/http/lib', + ), + 'Sabre\\Event\\' => + array ( + 0 => __DIR__ . '/..' . '/sabre/event/lib', + ), + 'Sabre\\' => + array ( + 0 => __DIR__ . '/..' . '/sabre/dav/lib', + ), + 'Psr\\Log\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/log/src', + ), + ); + + public static $classMap = array ( + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + 'Psr\\Log\\AbstractLogger' => __DIR__ . '/..' . '/psr/log/src/AbstractLogger.php', + 'Psr\\Log\\InvalidArgumentException' => __DIR__ . '/..' . '/psr/log/src/InvalidArgumentException.php', + 'Psr\\Log\\LogLevel' => __DIR__ . '/..' . '/psr/log/src/LogLevel.php', + 'Psr\\Log\\LoggerAwareInterface' => __DIR__ . '/..' . '/psr/log/src/LoggerAwareInterface.php', + 'Psr\\Log\\LoggerAwareTrait' => __DIR__ . '/..' . '/psr/log/src/LoggerAwareTrait.php', + 'Psr\\Log\\LoggerInterface' => __DIR__ . '/..' . '/psr/log/src/LoggerInterface.php', + 'Psr\\Log\\LoggerTrait' => __DIR__ . '/..' . '/psr/log/src/LoggerTrait.php', + 'Psr\\Log\\NullLogger' => __DIR__ . '/..' . '/psr/log/src/NullLogger.php', + 'Sabre\\CalDAV\\Backend\\AbstractBackend' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php', + 'Sabre\\CalDAV\\Backend\\BackendInterface' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Backend/BackendInterface.php', + 'Sabre\\CalDAV\\Backend\\NotificationSupport' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Backend/NotificationSupport.php', + 'Sabre\\CalDAV\\Backend\\PDO' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Backend/PDO.php', + 'Sabre\\CalDAV\\Backend\\SchedulingSupport' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php', + 'Sabre\\CalDAV\\Backend\\SharingSupport' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Backend/SharingSupport.php', + 'Sabre\\CalDAV\\Backend\\SimplePDO' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Backend/SimplePDO.php', + 'Sabre\\CalDAV\\Backend\\SubscriptionSupport' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php', + 'Sabre\\CalDAV\\Backend\\SyncSupport' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Backend/SyncSupport.php', + 'Sabre\\CalDAV\\Calendar' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Calendar.php', + 'Sabre\\CalDAV\\CalendarHome' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/CalendarHome.php', + 'Sabre\\CalDAV\\CalendarObject' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/CalendarObject.php', + 'Sabre\\CalDAV\\CalendarQueryValidator' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/CalendarQueryValidator.php', + 'Sabre\\CalDAV\\CalendarRoot' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/CalendarRoot.php', + 'Sabre\\CalDAV\\Exception\\InvalidComponentType' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php', + 'Sabre\\CalDAV\\ICSExportPlugin' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/ICSExportPlugin.php', + 'Sabre\\CalDAV\\ICalendar' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/ICalendar.php', + 'Sabre\\CalDAV\\ICalendarObject' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/ICalendarObject.php', + 'Sabre\\CalDAV\\ICalendarObjectContainer' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/ICalendarObjectContainer.php', + 'Sabre\\CalDAV\\ISharedCalendar' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/ISharedCalendar.php', + 'Sabre\\CalDAV\\Notifications\\Collection' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Notifications/Collection.php', + 'Sabre\\CalDAV\\Notifications\\ICollection' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Notifications/ICollection.php', + 'Sabre\\CalDAV\\Notifications\\INode' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Notifications/INode.php', + 'Sabre\\CalDAV\\Notifications\\Node' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Notifications/Node.php', + 'Sabre\\CalDAV\\Notifications\\Plugin' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Notifications/Plugin.php', + 'Sabre\\CalDAV\\Plugin' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Plugin.php', + 'Sabre\\CalDAV\\Principal\\Collection' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Principal/Collection.php', + 'Sabre\\CalDAV\\Principal\\IProxyRead' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Principal/IProxyRead.php', + 'Sabre\\CalDAV\\Principal\\IProxyWrite' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Principal/IProxyWrite.php', + 'Sabre\\CalDAV\\Principal\\ProxyRead' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Principal/ProxyRead.php', + 'Sabre\\CalDAV\\Principal\\ProxyWrite' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Principal/ProxyWrite.php', + 'Sabre\\CalDAV\\Principal\\User' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Principal/User.php', + 'Sabre\\CalDAV\\Schedule\\IInbox' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Schedule/IInbox.php', + 'Sabre\\CalDAV\\Schedule\\IMipPlugin' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Schedule/IMipPlugin.php', + 'Sabre\\CalDAV\\Schedule\\IOutbox' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Schedule/IOutbox.php', + 'Sabre\\CalDAV\\Schedule\\ISchedulingObject' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Schedule/ISchedulingObject.php', + 'Sabre\\CalDAV\\Schedule\\Inbox' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Schedule/Inbox.php', + 'Sabre\\CalDAV\\Schedule\\Outbox' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Schedule/Outbox.php', + 'Sabre\\CalDAV\\Schedule\\Plugin' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Schedule/Plugin.php', + 'Sabre\\CalDAV\\Schedule\\SchedulingObject' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php', + 'Sabre\\CalDAV\\SharedCalendar' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/SharedCalendar.php', + 'Sabre\\CalDAV\\SharingPlugin' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/SharingPlugin.php', + 'Sabre\\CalDAV\\Subscriptions\\ISubscription' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Subscriptions/ISubscription.php', + 'Sabre\\CalDAV\\Subscriptions\\Plugin' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Subscriptions/Plugin.php', + 'Sabre\\CalDAV\\Subscriptions\\Subscription' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php', + 'Sabre\\CalDAV\\Xml\\Filter\\CalendarData' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Filter/CalendarData.php', + 'Sabre\\CalDAV\\Xml\\Filter\\CompFilter' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Filter/CompFilter.php', + 'Sabre\\CalDAV\\Xml\\Filter\\ParamFilter' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Filter/ParamFilter.php', + 'Sabre\\CalDAV\\Xml\\Filter\\PropFilter' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Filter/PropFilter.php', + 'Sabre\\CalDAV\\Xml\\Notification\\Invite' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php', + 'Sabre\\CalDAV\\Xml\\Notification\\InviteReply' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php', + 'Sabre\\CalDAV\\Xml\\Notification\\NotificationInterface' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Notification/NotificationInterface.php', + 'Sabre\\CalDAV\\Xml\\Notification\\SystemStatus' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Notification/SystemStatus.php', + 'Sabre\\CalDAV\\Xml\\Property\\AllowedSharingModes' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Property/AllowedSharingModes.php', + 'Sabre\\CalDAV\\Xml\\Property\\EmailAddressSet' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Property/EmailAddressSet.php', + 'Sabre\\CalDAV\\Xml\\Property\\Invite' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Property/Invite.php', + 'Sabre\\CalDAV\\Xml\\Property\\ScheduleCalendarTransp' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Property/ScheduleCalendarTransp.php', + 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarComponentSet.php', + 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarData' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarData.php', + 'Sabre\\CalDAV\\Xml\\Property\\SupportedCollationSet' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Property/SupportedCollationSet.php', + 'Sabre\\CalDAV\\Xml\\Request\\CalendarMultiGetReport' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Request/CalendarMultiGetReport.php', + 'Sabre\\CalDAV\\Xml\\Request\\CalendarQueryReport' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Request/CalendarQueryReport.php', + 'Sabre\\CalDAV\\Xml\\Request\\FreeBusyQueryReport' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Request/FreeBusyQueryReport.php', + 'Sabre\\CalDAV\\Xml\\Request\\InviteReply' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php', + 'Sabre\\CalDAV\\Xml\\Request\\MkCalendar' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Request/MkCalendar.php', + 'Sabre\\CalDAV\\Xml\\Request\\Share' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Request/Share.php', + 'Sabre\\CardDAV\\AddressBook' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/AddressBook.php', + 'Sabre\\CardDAV\\AddressBookHome' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/AddressBookHome.php', + 'Sabre\\CardDAV\\AddressBookRoot' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/AddressBookRoot.php', + 'Sabre\\CardDAV\\Backend\\AbstractBackend' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/Backend/AbstractBackend.php', + 'Sabre\\CardDAV\\Backend\\BackendInterface' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/Backend/BackendInterface.php', + 'Sabre\\CardDAV\\Backend\\PDO' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/Backend/PDO.php', + 'Sabre\\CardDAV\\Backend\\SyncSupport' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/Backend/SyncSupport.php', + 'Sabre\\CardDAV\\Card' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/Card.php', + 'Sabre\\CardDAV\\IAddressBook' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/IAddressBook.php', + 'Sabre\\CardDAV\\ICard' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/ICard.php', + 'Sabre\\CardDAV\\IDirectory' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/IDirectory.php', + 'Sabre\\CardDAV\\Plugin' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/Plugin.php', + 'Sabre\\CardDAV\\VCFExportPlugin' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/VCFExportPlugin.php', + 'Sabre\\CardDAV\\Xml\\Filter\\AddressData' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/Xml/Filter/AddressData.php', + 'Sabre\\CardDAV\\Xml\\Filter\\ParamFilter' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/Xml/Filter/ParamFilter.php', + 'Sabre\\CardDAV\\Xml\\Filter\\PropFilter' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/Xml/Filter/PropFilter.php', + 'Sabre\\CardDAV\\Xml\\Property\\SupportedAddressData' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/Xml/Property/SupportedAddressData.php', + 'Sabre\\CardDAV\\Xml\\Property\\SupportedCollationSet' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/Xml/Property/SupportedCollationSet.php', + 'Sabre\\CardDAV\\Xml\\Request\\AddressBookMultiGetReport' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/Xml/Request/AddressBookMultiGetReport.php', + 'Sabre\\CardDAV\\Xml\\Request\\AddressBookQueryReport' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/Xml/Request/AddressBookQueryReport.php', + 'Sabre\\DAVACL\\ACLTrait' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/ACLTrait.php', + 'Sabre\\DAVACL\\AbstractPrincipalCollection' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/AbstractPrincipalCollection.php', + 'Sabre\\DAVACL\\Exception\\AceConflict' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Exception/AceConflict.php', + 'Sabre\\DAVACL\\Exception\\NeedPrivileges' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Exception/NeedPrivileges.php', + 'Sabre\\DAVACL\\Exception\\NoAbstract' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Exception/NoAbstract.php', + 'Sabre\\DAVACL\\Exception\\NotRecognizedPrincipal' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Exception/NotRecognizedPrincipal.php', + 'Sabre\\DAVACL\\Exception\\NotSupportedPrivilege' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Exception/NotSupportedPrivilege.php', + 'Sabre\\DAVACL\\FS\\Collection' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/FS/Collection.php', + 'Sabre\\DAVACL\\FS\\File' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/FS/File.php', + 'Sabre\\DAVACL\\FS\\HomeCollection' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/FS/HomeCollection.php', + 'Sabre\\DAVACL\\IACL' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/IACL.php', + 'Sabre\\DAVACL\\IPrincipal' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/IPrincipal.php', + 'Sabre\\DAVACL\\IPrincipalCollection' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/IPrincipalCollection.php', + 'Sabre\\DAVACL\\Plugin' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Plugin.php', + 'Sabre\\DAVACL\\Principal' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Principal.php', + 'Sabre\\DAVACL\\PrincipalBackend\\AbstractBackend' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/PrincipalBackend/AbstractBackend.php', + 'Sabre\\DAVACL\\PrincipalBackend\\BackendInterface' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/PrincipalBackend/BackendInterface.php', + 'Sabre\\DAVACL\\PrincipalBackend\\CreatePrincipalSupport' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/PrincipalBackend/CreatePrincipalSupport.php', + 'Sabre\\DAVACL\\PrincipalBackend\\PDO' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/PrincipalBackend/PDO.php', + 'Sabre\\DAVACL\\PrincipalCollection' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/PrincipalCollection.php', + 'Sabre\\DAVACL\\Xml\\Property\\Acl' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Xml/Property/Acl.php', + 'Sabre\\DAVACL\\Xml\\Property\\AclRestrictions' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Xml/Property/AclRestrictions.php', + 'Sabre\\DAVACL\\Xml\\Property\\CurrentUserPrivilegeSet' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Xml/Property/CurrentUserPrivilegeSet.php', + 'Sabre\\DAVACL\\Xml\\Property\\Principal' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Xml/Property/Principal.php', + 'Sabre\\DAVACL\\Xml\\Property\\SupportedPrivilegeSet' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php', + 'Sabre\\DAVACL\\Xml\\Request\\AclPrincipalPropSetReport' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Xml/Request/AclPrincipalPropSetReport.php', + 'Sabre\\DAVACL\\Xml\\Request\\ExpandPropertyReport' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Xml/Request/ExpandPropertyReport.php', + 'Sabre\\DAVACL\\Xml\\Request\\PrincipalMatchReport' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Xml/Request/PrincipalMatchReport.php', + 'Sabre\\DAVACL\\Xml\\Request\\PrincipalPropertySearchReport' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Xml/Request/PrincipalPropertySearchReport.php', + 'Sabre\\DAVACL\\Xml\\Request\\PrincipalSearchPropertySetReport' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Xml/Request/PrincipalSearchPropertySetReport.php', + 'Sabre\\DAV\\Auth\\Backend\\AbstractBasic' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Auth/Backend/AbstractBasic.php', + 'Sabre\\DAV\\Auth\\Backend\\AbstractBearer' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Auth/Backend/AbstractBearer.php', + 'Sabre\\DAV\\Auth\\Backend\\AbstractDigest' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Auth/Backend/AbstractDigest.php', + 'Sabre\\DAV\\Auth\\Backend\\Apache' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Auth/Backend/Apache.php', + 'Sabre\\DAV\\Auth\\Backend\\BackendInterface' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Auth/Backend/BackendInterface.php', + 'Sabre\\DAV\\Auth\\Backend\\BasicCallBack' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Auth/Backend/BasicCallBack.php', + 'Sabre\\DAV\\Auth\\Backend\\File' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Auth/Backend/File.php', + 'Sabre\\DAV\\Auth\\Backend\\IMAP' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Auth/Backend/IMAP.php', + 'Sabre\\DAV\\Auth\\Backend\\PDO' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Auth/Backend/PDO.php', + 'Sabre\\DAV\\Auth\\Backend\\PDOBasicAuth' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Auth/Backend/PDOBasicAuth.php', + 'Sabre\\DAV\\Auth\\Plugin' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Auth/Plugin.php', + 'Sabre\\DAV\\Browser\\GuessContentType' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Browser/GuessContentType.php', + 'Sabre\\DAV\\Browser\\HtmlOutput' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Browser/HtmlOutput.php', + 'Sabre\\DAV\\Browser\\HtmlOutputHelper' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Browser/HtmlOutputHelper.php', + 'Sabre\\DAV\\Browser\\MapGetToPropFind' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Browser/MapGetToPropFind.php', + 'Sabre\\DAV\\Browser\\Plugin' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Browser/Plugin.php', + 'Sabre\\DAV\\Browser\\PropFindAll' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Browser/PropFindAll.php', + 'Sabre\\DAV\\Client' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Client.php', + 'Sabre\\DAV\\Collection' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Collection.php', + 'Sabre\\DAV\\CorePlugin' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/CorePlugin.php', + 'Sabre\\DAV\\Exception' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception.php', + 'Sabre\\DAV\\Exception\\BadRequest' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/BadRequest.php', + 'Sabre\\DAV\\Exception\\Conflict' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/Conflict.php', + 'Sabre\\DAV\\Exception\\ConflictingLock' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/ConflictingLock.php', + 'Sabre\\DAV\\Exception\\Forbidden' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/Forbidden.php', + 'Sabre\\DAV\\Exception\\InsufficientStorage' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/InsufficientStorage.php', + 'Sabre\\DAV\\Exception\\InvalidResourceType' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/InvalidResourceType.php', + 'Sabre\\DAV\\Exception\\InvalidSyncToken' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/InvalidSyncToken.php', + 'Sabre\\DAV\\Exception\\LengthRequired' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/LengthRequired.php', + 'Sabre\\DAV\\Exception\\LockTokenMatchesRequestUri' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/LockTokenMatchesRequestUri.php', + 'Sabre\\DAV\\Exception\\Locked' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/Locked.php', + 'Sabre\\DAV\\Exception\\MethodNotAllowed' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php', + 'Sabre\\DAV\\Exception\\NotAuthenticated' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/NotAuthenticated.php', + 'Sabre\\DAV\\Exception\\NotFound' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/NotFound.php', + 'Sabre\\DAV\\Exception\\NotImplemented' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/NotImplemented.php', + 'Sabre\\DAV\\Exception\\PaymentRequired' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/PaymentRequired.php', + 'Sabre\\DAV\\Exception\\PreconditionFailed' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/PreconditionFailed.php', + 'Sabre\\DAV\\Exception\\ReportNotSupported' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/ReportNotSupported.php', + 'Sabre\\DAV\\Exception\\RequestedRangeNotSatisfiable' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/RequestedRangeNotSatisfiable.php', + 'Sabre\\DAV\\Exception\\ServiceUnavailable' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/ServiceUnavailable.php', + 'Sabre\\DAV\\Exception\\TooManyMatches' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/TooManyMatches.php', + 'Sabre\\DAV\\Exception\\UnsupportedMediaType' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/UnsupportedMediaType.php', + 'Sabre\\DAV\\FSExt\\Directory' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/FSExt/Directory.php', + 'Sabre\\DAV\\FSExt\\File' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/FSExt/File.php', + 'Sabre\\DAV\\FS\\Directory' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/FS/Directory.php', + 'Sabre\\DAV\\FS\\File' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/FS/File.php', + 'Sabre\\DAV\\FS\\Node' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/FS/Node.php', + 'Sabre\\DAV\\File' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/File.php', + 'Sabre\\DAV\\ICollection' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/ICollection.php', + 'Sabre\\DAV\\ICopyTarget' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/ICopyTarget.php', + 'Sabre\\DAV\\IExtendedCollection' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/IExtendedCollection.php', + 'Sabre\\DAV\\IFile' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/IFile.php', + 'Sabre\\DAV\\IMoveTarget' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/IMoveTarget.php', + 'Sabre\\DAV\\IMultiGet' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/IMultiGet.php', + 'Sabre\\DAV\\INode' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/INode.php', + 'Sabre\\DAV\\INodeByPath' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/INodeByPath.php', + 'Sabre\\DAV\\IProperties' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/IProperties.php', + 'Sabre\\DAV\\IQuota' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/IQuota.php', + 'Sabre\\DAV\\Locks\\Backend\\AbstractBackend' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Locks/Backend/AbstractBackend.php', + 'Sabre\\DAV\\Locks\\Backend\\BackendInterface' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Locks/Backend/BackendInterface.php', + 'Sabre\\DAV\\Locks\\Backend\\File' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Locks/Backend/File.php', + 'Sabre\\DAV\\Locks\\Backend\\PDO' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Locks/Backend/PDO.php', + 'Sabre\\DAV\\Locks\\LockInfo' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Locks/LockInfo.php', + 'Sabre\\DAV\\Locks\\Plugin' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Locks/Plugin.php', + 'Sabre\\DAV\\MkCol' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/MkCol.php', + 'Sabre\\DAV\\Mount\\Plugin' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Mount/Plugin.php', + 'Sabre\\DAV\\Node' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Node.php', + 'Sabre\\DAV\\PartialUpdate\\IPatchSupport' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/PartialUpdate/IPatchSupport.php', + 'Sabre\\DAV\\PartialUpdate\\Plugin' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/PartialUpdate/Plugin.php', + 'Sabre\\DAV\\PropFind' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/PropFind.php', + 'Sabre\\DAV\\PropPatch' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/PropPatch.php', + 'Sabre\\DAV\\PropertyStorage\\Backend\\BackendInterface' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/PropertyStorage/Backend/BackendInterface.php', + 'Sabre\\DAV\\PropertyStorage\\Backend\\PDO' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php', + 'Sabre\\DAV\\PropertyStorage\\Plugin' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/PropertyStorage/Plugin.php', + 'Sabre\\DAV\\Server' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Server.php', + 'Sabre\\DAV\\ServerPlugin' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/ServerPlugin.php', + 'Sabre\\DAV\\Sharing\\ISharedNode' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Sharing/ISharedNode.php', + 'Sabre\\DAV\\Sharing\\Plugin' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Sharing/Plugin.php', + 'Sabre\\DAV\\SimpleCollection' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/SimpleCollection.php', + 'Sabre\\DAV\\SimpleFile' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/SimpleFile.php', + 'Sabre\\DAV\\StringUtil' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/StringUtil.php', + 'Sabre\\DAV\\Sync\\ISyncCollection' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Sync/ISyncCollection.php', + 'Sabre\\DAV\\Sync\\Plugin' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Sync/Plugin.php', + 'Sabre\\DAV\\TemporaryFileFilterPlugin' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/TemporaryFileFilterPlugin.php', + 'Sabre\\DAV\\Tree' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Tree.php', + 'Sabre\\DAV\\UUIDUtil' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/UUIDUtil.php', + 'Sabre\\DAV\\Version' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Version.php', + 'Sabre\\DAV\\Xml\\Element\\Prop' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Element/Prop.php', + 'Sabre\\DAV\\Xml\\Element\\Response' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Element/Response.php', + 'Sabre\\DAV\\Xml\\Element\\Sharee' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Element/Sharee.php', + 'Sabre\\DAV\\Xml\\Property\\Complex' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Property/Complex.php', + 'Sabre\\DAV\\Xml\\Property\\GetLastModified' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Property/GetLastModified.php', + 'Sabre\\DAV\\Xml\\Property\\Href' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Property/Href.php', + 'Sabre\\DAV\\Xml\\Property\\Invite' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Property/Invite.php', + 'Sabre\\DAV\\Xml\\Property\\LocalHref' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Property/LocalHref.php', + 'Sabre\\DAV\\Xml\\Property\\LockDiscovery' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Property/LockDiscovery.php', + 'Sabre\\DAV\\Xml\\Property\\ResourceType' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Property/ResourceType.php', + 'Sabre\\DAV\\Xml\\Property\\ShareAccess' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Property/ShareAccess.php', + 'Sabre\\DAV\\Xml\\Property\\SupportedLock' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Property/SupportedLock.php', + 'Sabre\\DAV\\Xml\\Property\\SupportedMethodSet' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Property/SupportedMethodSet.php', + 'Sabre\\DAV\\Xml\\Property\\SupportedReportSet' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Property/SupportedReportSet.php', + 'Sabre\\DAV\\Xml\\Request\\Lock' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Request/Lock.php', + 'Sabre\\DAV\\Xml\\Request\\MkCol' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Request/MkCol.php', + 'Sabre\\DAV\\Xml\\Request\\PropFind' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Request/PropFind.php', + 'Sabre\\DAV\\Xml\\Request\\PropPatch' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Request/PropPatch.php', + 'Sabre\\DAV\\Xml\\Request\\ShareResource' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Request/ShareResource.php', + 'Sabre\\DAV\\Xml\\Request\\SyncCollectionReport' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Request/SyncCollectionReport.php', + 'Sabre\\DAV\\Xml\\Response\\MultiStatus' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Response/MultiStatus.php', + 'Sabre\\DAV\\Xml\\Service' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Service.php', + 'Sabre\\Event\\Emitter' => __DIR__ . '/..' . '/sabre/event/lib/Emitter.php', + 'Sabre\\Event\\EmitterInterface' => __DIR__ . '/..' . '/sabre/event/lib/EmitterInterface.php', + 'Sabre\\Event\\EmitterTrait' => __DIR__ . '/..' . '/sabre/event/lib/EmitterTrait.php', + 'Sabre\\Event\\EventEmitter' => __DIR__ . '/..' . '/sabre/event/lib/EventEmitter.php', + 'Sabre\\Event\\Loop\\Loop' => __DIR__ . '/..' . '/sabre/event/lib/Loop/Loop.php', + 'Sabre\\Event\\Promise' => __DIR__ . '/..' . '/sabre/event/lib/Promise.php', + 'Sabre\\Event\\PromiseAlreadyResolvedException' => __DIR__ . '/..' . '/sabre/event/lib/PromiseAlreadyResolvedException.php', + 'Sabre\\Event\\Version' => __DIR__ . '/..' . '/sabre/event/lib/Version.php', + 'Sabre\\Event\\WildcardEmitter' => __DIR__ . '/..' . '/sabre/event/lib/WildcardEmitter.php', + 'Sabre\\Event\\WildcardEmitterTrait' => __DIR__ . '/..' . '/sabre/event/lib/WildcardEmitterTrait.php', + 'Sabre\\HTTP\\Auth\\AWS' => __DIR__ . '/..' . '/sabre/http/lib/Auth/AWS.php', + 'Sabre\\HTTP\\Auth\\AbstractAuth' => __DIR__ . '/..' . '/sabre/http/lib/Auth/AbstractAuth.php', + 'Sabre\\HTTP\\Auth\\Basic' => __DIR__ . '/..' . '/sabre/http/lib/Auth/Basic.php', + 'Sabre\\HTTP\\Auth\\Bearer' => __DIR__ . '/..' . '/sabre/http/lib/Auth/Bearer.php', + 'Sabre\\HTTP\\Auth\\Digest' => __DIR__ . '/..' . '/sabre/http/lib/Auth/Digest.php', + 'Sabre\\HTTP\\Client' => __DIR__ . '/..' . '/sabre/http/lib/Client.php', + 'Sabre\\HTTP\\ClientException' => __DIR__ . '/..' . '/sabre/http/lib/ClientException.php', + 'Sabre\\HTTP\\ClientHttpException' => __DIR__ . '/..' . '/sabre/http/lib/ClientHttpException.php', + 'Sabre\\HTTP\\HttpException' => __DIR__ . '/..' . '/sabre/http/lib/HttpException.php', + 'Sabre\\HTTP\\Message' => __DIR__ . '/..' . '/sabre/http/lib/Message.php', + 'Sabre\\HTTP\\MessageDecoratorTrait' => __DIR__ . '/..' . '/sabre/http/lib/MessageDecoratorTrait.php', + 'Sabre\\HTTP\\MessageInterface' => __DIR__ . '/..' . '/sabre/http/lib/MessageInterface.php', + 'Sabre\\HTTP\\Request' => __DIR__ . '/..' . '/sabre/http/lib/Request.php', + 'Sabre\\HTTP\\RequestDecorator' => __DIR__ . '/..' . '/sabre/http/lib/RequestDecorator.php', + 'Sabre\\HTTP\\RequestInterface' => __DIR__ . '/..' . '/sabre/http/lib/RequestInterface.php', + 'Sabre\\HTTP\\Response' => __DIR__ . '/..' . '/sabre/http/lib/Response.php', + 'Sabre\\HTTP\\ResponseDecorator' => __DIR__ . '/..' . '/sabre/http/lib/ResponseDecorator.php', + 'Sabre\\HTTP\\ResponseInterface' => __DIR__ . '/..' . '/sabre/http/lib/ResponseInterface.php', + 'Sabre\\HTTP\\Sapi' => __DIR__ . '/..' . '/sabre/http/lib/Sapi.php', + 'Sabre\\HTTP\\Version' => __DIR__ . '/..' . '/sabre/http/lib/Version.php', + 'Sabre\\Uri\\InvalidUriException' => __DIR__ . '/..' . '/sabre/uri/lib/InvalidUriException.php', + 'Sabre\\Uri\\Version' => __DIR__ . '/..' . '/sabre/uri/lib/Version.php', + 'Sabre\\VObject\\BirthdayCalendarGenerator' => __DIR__ . '/..' . '/sabre/vobject/lib/BirthdayCalendarGenerator.php', + 'Sabre\\VObject\\Cli' => __DIR__ . '/..' . '/sabre/vobject/lib/Cli.php', + 'Sabre\\VObject\\Component' => __DIR__ . '/..' . '/sabre/vobject/lib/Component.php', + 'Sabre\\VObject\\Component\\Available' => __DIR__ . '/..' . '/sabre/vobject/lib/Component/Available.php', + 'Sabre\\VObject\\Component\\VAlarm' => __DIR__ . '/..' . '/sabre/vobject/lib/Component/VAlarm.php', + 'Sabre\\VObject\\Component\\VAvailability' => __DIR__ . '/..' . '/sabre/vobject/lib/Component/VAvailability.php', + 'Sabre\\VObject\\Component\\VCalendar' => __DIR__ . '/..' . '/sabre/vobject/lib/Component/VCalendar.php', + 'Sabre\\VObject\\Component\\VCard' => __DIR__ . '/..' . '/sabre/vobject/lib/Component/VCard.php', + 'Sabre\\VObject\\Component\\VEvent' => __DIR__ . '/..' . '/sabre/vobject/lib/Component/VEvent.php', + 'Sabre\\VObject\\Component\\VFreeBusy' => __DIR__ . '/..' . '/sabre/vobject/lib/Component/VFreeBusy.php', + 'Sabre\\VObject\\Component\\VJournal' => __DIR__ . '/..' . '/sabre/vobject/lib/Component/VJournal.php', + 'Sabre\\VObject\\Component\\VTimeZone' => __DIR__ . '/..' . '/sabre/vobject/lib/Component/VTimeZone.php', + 'Sabre\\VObject\\Component\\VTodo' => __DIR__ . '/..' . '/sabre/vobject/lib/Component/VTodo.php', + 'Sabre\\VObject\\DateTimeParser' => __DIR__ . '/..' . '/sabre/vobject/lib/DateTimeParser.php', + 'Sabre\\VObject\\Document' => __DIR__ . '/..' . '/sabre/vobject/lib/Document.php', + 'Sabre\\VObject\\ElementList' => __DIR__ . '/..' . '/sabre/vobject/lib/ElementList.php', + 'Sabre\\VObject\\EofException' => __DIR__ . '/..' . '/sabre/vobject/lib/EofException.php', + 'Sabre\\VObject\\FreeBusyData' => __DIR__ . '/..' . '/sabre/vobject/lib/FreeBusyData.php', + 'Sabre\\VObject\\FreeBusyGenerator' => __DIR__ . '/..' . '/sabre/vobject/lib/FreeBusyGenerator.php', + 'Sabre\\VObject\\ITip\\Broker' => __DIR__ . '/..' . '/sabre/vobject/lib/ITip/Broker.php', + 'Sabre\\VObject\\ITip\\ITipException' => __DIR__ . '/..' . '/sabre/vobject/lib/ITip/ITipException.php', + 'Sabre\\VObject\\ITip\\Message' => __DIR__ . '/..' . '/sabre/vobject/lib/ITip/Message.php', + 'Sabre\\VObject\\ITip\\SameOrganizerForAllComponentsException' => __DIR__ . '/..' . '/sabre/vobject/lib/ITip/SameOrganizerForAllComponentsException.php', + 'Sabre\\VObject\\InvalidDataException' => __DIR__ . '/..' . '/sabre/vobject/lib/InvalidDataException.php', + 'Sabre\\VObject\\Node' => __DIR__ . '/..' . '/sabre/vobject/lib/Node.php', + 'Sabre\\VObject\\PHPUnitAssertions' => __DIR__ . '/..' . '/sabre/vobject/lib/PHPUnitAssertions.php', + 'Sabre\\VObject\\Parameter' => __DIR__ . '/..' . '/sabre/vobject/lib/Parameter.php', + 'Sabre\\VObject\\ParseException' => __DIR__ . '/..' . '/sabre/vobject/lib/ParseException.php', + 'Sabre\\VObject\\Parser\\Json' => __DIR__ . '/..' . '/sabre/vobject/lib/Parser/Json.php', + 'Sabre\\VObject\\Parser\\MimeDir' => __DIR__ . '/..' . '/sabre/vobject/lib/Parser/MimeDir.php', + 'Sabre\\VObject\\Parser\\Parser' => __DIR__ . '/..' . '/sabre/vobject/lib/Parser/Parser.php', + 'Sabre\\VObject\\Parser\\XML' => __DIR__ . '/..' . '/sabre/vobject/lib/Parser/XML.php', + 'Sabre\\VObject\\Parser\\XML\\Element\\KeyValue' => __DIR__ . '/..' . '/sabre/vobject/lib/Parser/XML/Element/KeyValue.php', + 'Sabre\\VObject\\Property' => __DIR__ . '/..' . '/sabre/vobject/lib/Property.php', + 'Sabre\\VObject\\Property\\Binary' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/Binary.php', + 'Sabre\\VObject\\Property\\Boolean' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/Boolean.php', + 'Sabre\\VObject\\Property\\FlatText' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/FlatText.php', + 'Sabre\\VObject\\Property\\FloatValue' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/FloatValue.php', + 'Sabre\\VObject\\Property\\ICalendar\\CalAddress' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/ICalendar/CalAddress.php', + 'Sabre\\VObject\\Property\\ICalendar\\Date' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/ICalendar/Date.php', + 'Sabre\\VObject\\Property\\ICalendar\\DateTime' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/ICalendar/DateTime.php', + 'Sabre\\VObject\\Property\\ICalendar\\Duration' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/ICalendar/Duration.php', + 'Sabre\\VObject\\Property\\ICalendar\\Period' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/ICalendar/Period.php', + 'Sabre\\VObject\\Property\\ICalendar\\Recur' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/ICalendar/Recur.php', + 'Sabre\\VObject\\Property\\IntegerValue' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/IntegerValue.php', + 'Sabre\\VObject\\Property\\Text' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/Text.php', + 'Sabre\\VObject\\Property\\Time' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/Time.php', + 'Sabre\\VObject\\Property\\Unknown' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/Unknown.php', + 'Sabre\\VObject\\Property\\Uri' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/Uri.php', + 'Sabre\\VObject\\Property\\UtcOffset' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/UtcOffset.php', + 'Sabre\\VObject\\Property\\VCard\\Date' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/VCard/Date.php', + 'Sabre\\VObject\\Property\\VCard\\DateAndOrTime' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/VCard/DateAndOrTime.php', + 'Sabre\\VObject\\Property\\VCard\\DateTime' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/VCard/DateTime.php', + 'Sabre\\VObject\\Property\\VCard\\LanguageTag' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/VCard/LanguageTag.php', + 'Sabre\\VObject\\Property\\VCard\\PhoneNumber' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/VCard/PhoneNumber.php', + 'Sabre\\VObject\\Property\\VCard\\TimeStamp' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/VCard/TimeStamp.php', + 'Sabre\\VObject\\Reader' => __DIR__ . '/..' . '/sabre/vobject/lib/Reader.php', + 'Sabre\\VObject\\Recur\\EventIterator' => __DIR__ . '/..' . '/sabre/vobject/lib/Recur/EventIterator.php', + 'Sabre\\VObject\\Recur\\MaxInstancesExceededException' => __DIR__ . '/..' . '/sabre/vobject/lib/Recur/MaxInstancesExceededException.php', + 'Sabre\\VObject\\Recur\\NoInstancesException' => __DIR__ . '/..' . '/sabre/vobject/lib/Recur/NoInstancesException.php', + 'Sabre\\VObject\\Recur\\RDateIterator' => __DIR__ . '/..' . '/sabre/vobject/lib/Recur/RDateIterator.php', + 'Sabre\\VObject\\Recur\\RRuleIterator' => __DIR__ . '/..' . '/sabre/vobject/lib/Recur/RRuleIterator.php', + 'Sabre\\VObject\\Settings' => __DIR__ . '/..' . '/sabre/vobject/lib/Settings.php', + 'Sabre\\VObject\\Splitter\\ICalendar' => __DIR__ . '/..' . '/sabre/vobject/lib/Splitter/ICalendar.php', + 'Sabre\\VObject\\Splitter\\SplitterInterface' => __DIR__ . '/..' . '/sabre/vobject/lib/Splitter/SplitterInterface.php', + 'Sabre\\VObject\\Splitter\\VCard' => __DIR__ . '/..' . '/sabre/vobject/lib/Splitter/VCard.php', + 'Sabre\\VObject\\StringUtil' => __DIR__ . '/..' . '/sabre/vobject/lib/StringUtil.php', + 'Sabre\\VObject\\TimeZoneUtil' => __DIR__ . '/..' . '/sabre/vobject/lib/TimeZoneUtil.php', + 'Sabre\\VObject\\TimezoneGuesser\\FindFromOffset' => __DIR__ . '/..' . '/sabre/vobject/lib/TimezoneGuesser/FindFromOffset.php', + 'Sabre\\VObject\\TimezoneGuesser\\FindFromTimezoneIdentifier' => __DIR__ . '/..' . '/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneIdentifier.php', + 'Sabre\\VObject\\TimezoneGuesser\\FindFromTimezoneMap' => __DIR__ . '/..' . '/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneMap.php', + 'Sabre\\VObject\\TimezoneGuesser\\GuessFromLicEntry' => __DIR__ . '/..' . '/sabre/vobject/lib/TimezoneGuesser/GuessFromLicEntry.php', + 'Sabre\\VObject\\TimezoneGuesser\\GuessFromMsTzId' => __DIR__ . '/..' . '/sabre/vobject/lib/TimezoneGuesser/GuessFromMsTzId.php', + 'Sabre\\VObject\\TimezoneGuesser\\TimezoneFinder' => __DIR__ . '/..' . '/sabre/vobject/lib/TimezoneGuesser/TimezoneFinder.php', + 'Sabre\\VObject\\TimezoneGuesser\\TimezoneGuesser' => __DIR__ . '/..' . '/sabre/vobject/lib/TimezoneGuesser/TimezoneGuesser.php', + 'Sabre\\VObject\\UUIDUtil' => __DIR__ . '/..' . '/sabre/vobject/lib/UUIDUtil.php', + 'Sabre\\VObject\\VCardConverter' => __DIR__ . '/..' . '/sabre/vobject/lib/VCardConverter.php', + 'Sabre\\VObject\\Version' => __DIR__ . '/..' . '/sabre/vobject/lib/Version.php', + 'Sabre\\VObject\\Writer' => __DIR__ . '/..' . '/sabre/vobject/lib/Writer.php', + 'Sabre\\Xml\\ContextStackTrait' => __DIR__ . '/..' . '/sabre/xml/lib/ContextStackTrait.php', + 'Sabre\\Xml\\Element' => __DIR__ . '/..' . '/sabre/xml/lib/Element.php', + 'Sabre\\Xml\\Element\\Base' => __DIR__ . '/..' . '/sabre/xml/lib/Element/Base.php', + 'Sabre\\Xml\\Element\\Cdata' => __DIR__ . '/..' . '/sabre/xml/lib/Element/Cdata.php', + 'Sabre\\Xml\\Element\\Elements' => __DIR__ . '/..' . '/sabre/xml/lib/Element/Elements.php', + 'Sabre\\Xml\\Element\\KeyValue' => __DIR__ . '/..' . '/sabre/xml/lib/Element/KeyValue.php', + 'Sabre\\Xml\\Element\\Uri' => __DIR__ . '/..' . '/sabre/xml/lib/Element/Uri.php', + 'Sabre\\Xml\\Element\\XmlFragment' => __DIR__ . '/..' . '/sabre/xml/lib/Element/XmlFragment.php', + 'Sabre\\Xml\\LibXMLException' => __DIR__ . '/..' . '/sabre/xml/lib/LibXMLException.php', + 'Sabre\\Xml\\ParseException' => __DIR__ . '/..' . '/sabre/xml/lib/ParseException.php', + 'Sabre\\Xml\\Reader' => __DIR__ . '/..' . '/sabre/xml/lib/Reader.php', + 'Sabre\\Xml\\Service' => __DIR__ . '/..' . '/sabre/xml/lib/Service.php', + 'Sabre\\Xml\\Version' => __DIR__ . '/..' . '/sabre/xml/lib/Version.php', + 'Sabre\\Xml\\Writer' => __DIR__ . '/..' . '/sabre/xml/lib/Writer.php', + 'Sabre\\Xml\\XmlDeserializable' => __DIR__ . '/..' . '/sabre/xml/lib/XmlDeserializable.php', + 'Sabre\\Xml\\XmlSerializable' => __DIR__ . '/..' . '/sabre/xml/lib/XmlSerializable.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInita07591154e0386cfc9ad0f9217878d04::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInita07591154e0386cfc9ad0f9217878d04::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInita07591154e0386cfc9ad0f9217878d04::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/lib/vendor/composer/installed.json b/lib/vendor/composer/installed.json new file mode 100644 index 0000000..145cc32 --- /dev/null +++ b/lib/vendor/composer/installed.json @@ -0,0 +1,522 @@ +{ + "packages": [ + { + "name": "psr/log", + "version": "3.0.2", + "version_normalized": "3.0.2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "time": "2024-09-11T13:17:53+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "install-path": "../psr/log" + }, + { + "name": "sabre/dav", + "version": "4.7.0", + "version_normalized": "4.7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/dav.git", + "reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/dav/zipball/074373bcd689a30bcf5aaa6bbb20a3395964ce7a", + "reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-dom": "*", + "ext-iconv": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "ext-spl": "*", + "lib-libxml": ">=2.7.0", + "php": "^7.1.0 || ^8.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "sabre/event": "^5.0", + "sabre/http": "^5.0.5", + "sabre/uri": "^2.0", + "sabre/vobject": "^4.2.1", + "sabre/xml": "^2.0.1" + }, + "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-imap": "*", + "ext-pdo": "*" + }, + "time": "2024-10-29T11:46:02+00:00", + "bin": [ + "bin/sabredav", + "bin/naturalselection" + ], + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Sabre\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "WebDAV Framework for PHP", + "homepage": "http://sabre.io/", + "keywords": [ + "CalDAV", + "CardDAV", + "WebDAV", + "framework", + "iCalendar" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/dav/issues", + "source": "https://github.com/fruux/sabre-dav" + }, + "install-path": "../sabre/dav" + }, + { + "name": "sabre/event", + "version": "5.1.7", + "version_normalized": "5.1.7.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/event.git", + "reference": "86d57e305c272898ba3c28e9bd3d65d5464587c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/event/zipball/86d57e305c272898ba3c28e9bd3d65d5464587c2", + "reference": "86d57e305c272898ba3c28e9bd3d65d5464587c2", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1||^3.63", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "time": "2024-08-27T11:23:05+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "files": [ + "lib/coroutine.php", + "lib/Loop/functions.php", + "lib/Promise/functions.php" + ], + "psr-4": { + "Sabre\\Event\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "sabre/event is a library for lightweight event-based programming", + "homepage": "http://sabre.io/event/", + "keywords": [ + "EventEmitter", + "async", + "coroutine", + "eventloop", + "events", + "hooks", + "plugin", + "promise", + "reactor", + "signal" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/event/issues", + "source": "https://github.com/fruux/sabre-event" + }, + "install-path": "../sabre/event" + }, + { + "name": "sabre/http", + "version": "5.1.13", + "version_normalized": "5.1.13.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/http.git", + "reference": "7c2a14097d1a0de2347dcbdc91a02f38e338f4db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/http/zipball/7c2a14097d1a0de2347dcbdc91a02f38e338f4db", + "reference": "7c2a14097d1a0de2347dcbdc91a02f38e338f4db", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-curl": "*", + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabre/event": ">=4.0 <6.0", + "sabre/uri": "^2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1||3.63.2", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "suggest": { + "ext-curl": " to make http requests with the Client class" + }, + "time": "2025-09-09T10:21:47+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Sabre\\HTTP\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "The sabre/http library provides utilities for dealing with http requests and responses. ", + "homepage": "https://github.com/fruux/sabre-http", + "keywords": [ + "http" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/http/issues", + "source": "https://github.com/fruux/sabre-http" + }, + "install-path": "../sabre/http" + }, + { + "name": "sabre/uri", + "version": "2.3.4", + "version_normalized": "2.3.4.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/uri.git", + "reference": "b76524c22de90d80ca73143680a8e77b1266c291" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/uri/zipball/b76524c22de90d80ca73143680a8e77b1266c291", + "reference": "b76524c22de90d80ca73143680a8e77b1266c291", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.63", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^1.12", + "phpstan/phpstan-phpunit": "^1.4", + "phpstan/phpstan-strict-rules": "^1.6", + "phpunit/phpunit": "^9.6" + }, + "time": "2024-08-27T12:18:16+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Sabre\\Uri\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "Functions for making sense out of URIs.", + "homepage": "http://sabre.io/uri/", + "keywords": [ + "rfc3986", + "uri", + "url" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/uri/issues", + "source": "https://github.com/fruux/sabre-uri" + }, + "install-path": "../sabre/uri" + }, + { + "name": "sabre/vobject", + "version": "4.5.8", + "version_normalized": "4.5.8.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/vobject.git", + "reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/vobject/zipball/d554eb24d64232922e1eab5896cc2f84b3b9ffb1", + "reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabre/xml": "^2.1 || ^3.0 || ^4.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1", + "phpstan/phpstan": "^0.12 || ^1.12 || ^2.0", + "phpunit/php-invoker": "^2.0 || ^3.1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "suggest": { + "hoa/bench": "If you would like to run the benchmark scripts" + }, + "time": "2026-01-12T10:45:19+00:00", + "bin": [ + "bin/vobject", + "bin/generate_vcards" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Sabre\\VObject\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + }, + { + "name": "Dominik Tobschall", + "email": "dominik@fruux.com", + "homepage": "http://tobschall.de/", + "role": "Developer" + }, + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net", + "homepage": "http://mnt.io/", + "role": "Developer" + } + ], + "description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects", + "homepage": "http://sabre.io/vobject/", + "keywords": [ + "availability", + "freebusy", + "iCalendar", + "ical", + "ics", + "jCal", + "jCard", + "recurrence", + "rfc2425", + "rfc2426", + "rfc2739", + "rfc4770", + "rfc5545", + "rfc5546", + "rfc6321", + "rfc6350", + "rfc6351", + "rfc6474", + "rfc6638", + "rfc6715", + "rfc6868", + "vCalendar", + "vCard", + "vcf", + "xCal", + "xCard" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/vobject/issues", + "source": "https://github.com/fruux/sabre-vobject" + }, + "install-path": "../sabre/vobject" + }, + { + "name": "sabre/xml", + "version": "2.2.11", + "version_normalized": "2.2.11.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/xml.git", + "reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/xml/zipball/01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc", + "reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "lib-libxml": ">=2.6.20", + "php": "^7.1 || ^8.0", + "sabre/uri": ">=1.0,<3.0.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1||3.63.2", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "time": "2024-09-06T07:37:46+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "files": [ + "lib/Deserializer/functions.php", + "lib/Serializer/functions.php" + ], + "psr-4": { + "Sabre\\Xml\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + }, + { + "name": "Markus Staab", + "email": "markus.staab@redaxo.de", + "role": "Developer" + } + ], + "description": "sabre/xml is an XML library that you may not hate.", + "homepage": "https://sabre.io/xml/", + "keywords": [ + "XMLReader", + "XMLWriter", + "dom", + "xml" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/xml/issues", + "source": "https://github.com/fruux/sabre-xml" + }, + "install-path": "../sabre/xml" + } + ], + "dev": true, + "dev-package-names": [] +} diff --git a/lib/vendor/composer/installed.php b/lib/vendor/composer/installed.php new file mode 100644 index 0000000..b97014f --- /dev/null +++ b/lib/vendor/composer/installed.php @@ -0,0 +1,86 @@ + array( + 'name' => 'ktrix/service-dav', + 'pretty_version' => '1.0.0+no-version-set', + 'version' => '1.0.0.0', + 'reference' => null, + 'type' => 'library', + 'install_path' => __DIR__ . '/../../../', + 'aliases' => array(), + 'dev' => true, + ), + 'versions' => array( + 'ktrix/service-dav' => array( + 'pretty_version' => '1.0.0+no-version-set', + 'version' => '1.0.0.0', + 'reference' => null, + 'type' => 'library', + 'install_path' => __DIR__ . '/../../../', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'psr/log' => array( + 'pretty_version' => '3.0.2', + 'version' => '3.0.2.0', + 'reference' => 'f16e1d5863e37f8d8c2a01719f5b34baa2b714d3', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/log', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'sabre/dav' => array( + 'pretty_version' => '4.7.0', + 'version' => '4.7.0.0', + 'reference' => '074373bcd689a30bcf5aaa6bbb20a3395964ce7a', + 'type' => 'library', + 'install_path' => __DIR__ . '/../sabre/dav', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'sabre/event' => array( + 'pretty_version' => '5.1.7', + 'version' => '5.1.7.0', + 'reference' => '86d57e305c272898ba3c28e9bd3d65d5464587c2', + 'type' => 'library', + 'install_path' => __DIR__ . '/../sabre/event', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'sabre/http' => array( + 'pretty_version' => '5.1.13', + 'version' => '5.1.13.0', + 'reference' => '7c2a14097d1a0de2347dcbdc91a02f38e338f4db', + 'type' => 'library', + 'install_path' => __DIR__ . '/../sabre/http', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'sabre/uri' => array( + 'pretty_version' => '2.3.4', + 'version' => '2.3.4.0', + 'reference' => 'b76524c22de90d80ca73143680a8e77b1266c291', + 'type' => 'library', + 'install_path' => __DIR__ . '/../sabre/uri', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'sabre/vobject' => array( + 'pretty_version' => '4.5.8', + 'version' => '4.5.8.0', + 'reference' => 'd554eb24d64232922e1eab5896cc2f84b3b9ffb1', + 'type' => 'library', + 'install_path' => __DIR__ . '/../sabre/vobject', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'sabre/xml' => array( + 'pretty_version' => '2.2.11', + 'version' => '2.2.11.0', + 'reference' => '01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc', + 'type' => 'library', + 'install_path' => __DIR__ . '/../sabre/xml', + 'aliases' => array(), + 'dev_requirement' => false, + ), + ), +); diff --git a/lib/vendor/composer/platform_check.php b/lib/vendor/composer/platform_check.php new file mode 100644 index 0000000..4c3a5d6 --- /dev/null +++ b/lib/vendor/composer/platform_check.php @@ -0,0 +1,26 @@ += 80100)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.'; +} + +if ($issues) { + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); + } elseif (!headers_sent()) { + echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; + } + } + trigger_error( + 'Composer detected issues in your platform: ' . implode(' ', $issues), + E_USER_ERROR + ); +} diff --git a/lib/vendor/psr/log/LICENSE b/lib/vendor/psr/log/LICENSE new file mode 100644 index 0000000..474c952 --- /dev/null +++ b/lib/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/vendor/psr/log/README.md b/lib/vendor/psr/log/README.md new file mode 100644 index 0000000..a9f20c4 --- /dev/null +++ b/lib/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/vendor/psr/log/composer.json b/lib/vendor/psr/log/composer.json new file mode 100644 index 0000000..879fc6f --- /dev/null +++ b/lib/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/vendor/psr/log/src/AbstractLogger.php b/lib/vendor/psr/log/src/AbstractLogger.php new file mode 100644 index 0000000..d60a091 --- /dev/null +++ b/lib/vendor/psr/log/src/AbstractLogger.php @@ -0,0 +1,15 @@ +logger = $logger; + } +} diff --git a/lib/vendor/psr/log/src/LoggerInterface.php b/lib/vendor/psr/log/src/LoggerInterface.php new file mode 100644 index 0000000..cb4cf64 --- /dev/null +++ b/lib/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/vendor/psr/log/src/NullLogger.php b/lib/vendor/psr/log/src/NullLogger.php new file mode 100644 index 0000000..de0561e --- /dev/null +++ b/lib/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/vendor/sabre/dav/LICENSE b/lib/vendor/sabre/dav/LICENSE new file mode 100644 index 0000000..fd3539e --- /dev/null +++ b/lib/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/vendor/sabre/dav/README.md b/lib/vendor/sabre/dav/README.md new file mode 100644 index 0000000..32ca1c3 --- /dev/null +++ b/lib/vendor/sabre/dav/README.md @@ -0,0 +1,39 @@ +![sabre's logo](http://sabre.io/img/logo.png) 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.* | ![CI](https://github.com/sabre-io/dav/actions/workflows/ci.yml/badge.svg) | 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/vendor/sabre/dav/bin/build.php b/lib/vendor/sabre/dav/bin/build.php new file mode 100755 index 0000000..4dd25d9 --- /dev/null +++ b/lib/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/vendor/sabre/dav/bin/migrateto20.php b/lib/vendor/sabre/dav/bin/migrateto20.php new file mode 100755 index 0000000..fb24fe5 --- /dev/null +++ b/lib/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/vendor/sabre/dav/bin/migrateto21.php b/lib/vendor/sabre/dav/bin/migrateto21.php new file mode 100755 index 0000000..2c15b0a --- /dev/null +++ b/lib/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/vendor/sabre/dav/bin/migrateto30.php b/lib/vendor/sabre/dav/bin/migrateto30.php new file mode 100755 index 0000000..9798cad --- /dev/null +++ b/lib/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/vendor/sabre/dav/bin/migrateto32.php b/lib/vendor/sabre/dav/bin/migrateto32.php new file mode 100755 index 0000000..09ac55d --- /dev/null +++ b/lib/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/vendor/sabre/dav/bin/sabredav b/lib/vendor/sabre/dav/bin/sabredav new file mode 100755 index 0000000..032371b --- /dev/null +++ b/lib/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/vendor/sabre/dav/bin/sabredav.php b/lib/vendor/sabre/dav/bin/sabredav.php new file mode 100755 index 0000000..71047b8 --- /dev/null +++ b/lib/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/vendor/sabre/dav/composer.json b/lib/vendor/sabre/dav/composer.json new file mode 100644 index 0000000..3154173 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php b/lib/vendor/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php new file mode 100644 index 0000000..c761bff --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Backend/BackendInterface.php b/lib/vendor/sabre/dav/lib/CalDAV/Backend/BackendInterface.php new file mode 100644 index 0000000..ccaa251 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php b/lib/vendor/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php new file mode 100644 index 0000000..69467e5 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php b/lib/vendor/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php new file mode 100644 index 0000000..7655c2e --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Calendar.php b/lib/vendor/sabre/dav/lib/CalDAV/Calendar.php new file mode 100644 index 0000000..ba8c704 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/CalendarHome.php b/lib/vendor/sabre/dav/lib/CalDAV/CalendarHome.php new file mode 100644 index 0000000..49c54a3 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/CalendarObject.php b/lib/vendor/sabre/dav/lib/CalDAV/CalendarObject.php new file mode 100644 index 0000000..671f4b5 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/CalendarQueryValidator.php b/lib/vendor/sabre/dav/lib/CalDAV/CalendarQueryValidator.php new file mode 100644 index 0000000..ee525da --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/CalendarRoot.php b/lib/vendor/sabre/dav/lib/CalDAV/CalendarRoot.php new file mode 100644 index 0000000..3038d21 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php b/lib/vendor/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php new file mode 100644 index 0000000..e94378a --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/ICSExportPlugin.php b/lib/vendor/sabre/dav/lib/CalDAV/ICSExportPlugin.php new file mode 100644 index 0000000..9171e36 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/ICalendar.php b/lib/vendor/sabre/dav/lib/CalDAV/ICalendar.php new file mode 100644 index 0000000..8636e0b --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Notifications/ICollection.php b/lib/vendor/sabre/dav/lib/CalDAV/Notifications/ICollection.php new file mode 100644 index 0000000..b12fb39 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Notifications/Plugin.php b/lib/vendor/sabre/dav/lib/CalDAV/Notifications/Plugin.php new file mode 100644 index 0000000..56b2fe9 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Plugin.php b/lib/vendor/sabre/dav/lib/CalDAV/Plugin.php new file mode 100644 index 0000000..ccb722f --- /dev/null +++ b/lib/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 .= '
+

Create new calendar

+ + +
+
+ +
+ '; + + 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/vendor/sabre/dav/lib/CalDAV/Principal/Collection.php b/lib/vendor/sabre/dav/lib/CalDAV/Principal/Collection.php new file mode 100644 index 0000000..6d02303 --- /dev/null +++ b/lib/vendor/sabre/dav/lib/CalDAV/Principal/Collection.php @@ -0,0 +1,32 @@ +principalBackend, $principalInfo); + } +} diff --git a/lib/vendor/sabre/dav/lib/CalDAV/Principal/IProxyRead.php b/lib/vendor/sabre/dav/lib/CalDAV/Principal/IProxyRead.php new file mode 100644 index 0000000..96e6991 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Principal/ProxyWrite.php b/lib/vendor/sabre/dav/lib/CalDAV/Principal/ProxyWrite.php new file mode 100644 index 0000000..2d1ce7c --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Principal/User.php b/lib/vendor/sabre/dav/lib/CalDAV/Principal/User.php new file mode 100644 index 0000000..88bf4b4 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Schedule/IInbox.php b/lib/vendor/sabre/dav/lib/CalDAV/Schedule/IInbox.php new file mode 100644 index 0000000..64a94be --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Schedule/IOutbox.php b/lib/vendor/sabre/dav/lib/CalDAV/Schedule/IOutbox.php new file mode 100644 index 0000000..384b503 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Schedule/Outbox.php b/lib/vendor/sabre/dav/lib/CalDAV/Schedule/Outbox.php new file mode 100644 index 0000000..1442c4c --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Schedule/Plugin.php b/lib/vendor/sabre/dav/lib/CalDAV/Schedule/Plugin.php new file mode 100644 index 0000000..5bca56d --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php b/lib/vendor/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php new file mode 100644 index 0000000..b40f28a --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/SharedCalendar.php b/lib/vendor/sabre/dav/lib/CalDAV/SharedCalendar.php new file mode 100644 index 0000000..818392f --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/SharingPlugin.php b/lib/vendor/sabre/dav/lib/CalDAV/SharingPlugin.php new file mode 100644 index 0000000..bacfe04 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Subscriptions/ISubscription.php b/lib/vendor/sabre/dav/lib/CalDAV/Subscriptions/ISubscription.php new file mode 100644 index 0000000..e83082c --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php b/lib/vendor/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php new file mode 100644 index 0000000..8d56e64 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Xml/Filter/CalendarData.php b/lib/vendor/sabre/dav/lib/CalDAV/Xml/Filter/CalendarData.php new file mode 100644 index 0000000..c9656d8 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Xml/Filter/CompFilter.php b/lib/vendor/sabre/dav/lib/CalDAV/Xml/Filter/CompFilter.php new file mode 100644 index 0000000..929000b --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Xml/Filter/ParamFilter.php b/lib/vendor/sabre/dav/lib/CalDAV/Xml/Filter/ParamFilter.php new file mode 100644 index 0000000..1e6dd59 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Xml/Filter/PropFilter.php b/lib/vendor/sabre/dav/lib/CalDAV/Xml/Filter/PropFilter.php new file mode 100644 index 0000000..f1d66cc --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php b/lib/vendor/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php new file mode 100644 index 0000000..2dbb0f4 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php b/lib/vendor/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php new file mode 100644 index 0000000..dbdba3b --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Xml/Notification/NotificationInterface.php b/lib/vendor/sabre/dav/lib/CalDAV/Xml/Notification/NotificationInterface.php new file mode 100644 index 0000000..e1b393f --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Xml/Property/AllowedSharingModes.php b/lib/vendor/sabre/dav/lib/CalDAV/Xml/Property/AllowedSharingModes.php new file mode 100644 index 0000000..58acb6d --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Xml/Property/EmailAddressSet.php b/lib/vendor/sabre/dav/lib/CalDAV/Xml/Property/EmailAddressSet.php new file mode 100644 index 0000000..84f7ae0 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Xml/Property/Invite.php b/lib/vendor/sabre/dav/lib/CalDAV/Xml/Property/Invite.php new file mode 100644 index 0000000..c389ca8 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Xml/Property/ScheduleCalendarTransp.php b/lib/vendor/sabre/dav/lib/CalDAV/Xml/Property/ScheduleCalendarTransp.php new file mode 100644 index 0000000..1595220 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarComponentSet.php b/lib/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarComponentSet.php new file mode 100644 index 0000000..d86e7b7 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarData.php b/lib/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarData.php new file mode 100644 index 0000000..5b08933 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCollationSet.php b/lib/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCollationSet.php new file mode 100644 index 0000000..c5ffeee --- /dev/null +++ b/lib/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCollationSet.php @@ -0,0 +1,54 @@ +writeElement('{'.Plugin::NS_CALDAV.'}supported-collation', $collation); + } + } +} diff --git a/lib/vendor/sabre/dav/lib/CalDAV/Xml/Request/CalendarMultiGetReport.php b/lib/vendor/sabre/dav/lib/CalDAV/Xml/Request/CalendarMultiGetReport.php new file mode 100644 index 0000000..4771a20 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Xml/Request/CalendarQueryReport.php b/lib/vendor/sabre/dav/lib/CalDAV/Xml/Request/CalendarQueryReport.php new file mode 100644 index 0000000..5a4df46 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Xml/Request/FreeBusyQueryReport.php b/lib/vendor/sabre/dav/lib/CalDAV/Xml/Request/FreeBusyQueryReport.php new file mode 100644 index 0000000..17df05a --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php b/lib/vendor/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php new file mode 100644 index 0000000..166721e --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Xml/Request/MkCalendar.php b/lib/vendor/sabre/dav/lib/CalDAV/Xml/Request/MkCalendar.php new file mode 100644 index 0000000..b5701e2 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CalDAV/Xml/Request/Share.php b/lib/vendor/sabre/dav/lib/CalDAV/Xml/Request/Share.php new file mode 100644 index 0000000..d597b76 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CardDAV/AddressBook.php b/lib/vendor/sabre/dav/lib/CardDAV/AddressBook.php new file mode 100644 index 0000000..f5744f6 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CardDAV/AddressBookHome.php b/lib/vendor/sabre/dav/lib/CardDAV/AddressBookHome.php new file mode 100644 index 0000000..d7365fb --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CardDAV/AddressBookRoot.php b/lib/vendor/sabre/dav/lib/CardDAV/AddressBookRoot.php new file mode 100644 index 0000000..ee1721a --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CardDAV/Backend/AbstractBackend.php b/lib/vendor/sabre/dav/lib/CardDAV/Backend/AbstractBackend.php new file mode 100644 index 0000000..a900c62 --- /dev/null +++ b/lib/vendor/sabre/dav/lib/CardDAV/Backend/AbstractBackend.php @@ -0,0 +1,38 @@ +getCard($addressBookId, $uri); + }, $uris); + } +} diff --git a/lib/vendor/sabre/dav/lib/CardDAV/Backend/BackendInterface.php b/lib/vendor/sabre/dav/lib/CardDAV/Backend/BackendInterface.php new file mode 100644 index 0000000..f9955ac --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CardDAV/Backend/SyncSupport.php b/lib/vendor/sabre/dav/lib/CardDAV/Backend/SyncSupport.php new file mode 100644 index 0000000..6aaad14 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CardDAV/Card.php b/lib/vendor/sabre/dav/lib/CardDAV/Card.php new file mode 100644 index 0000000..c9cd2bb --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CardDAV/IAddressBook.php b/lib/vendor/sabre/dav/lib/CardDAV/IAddressBook.php new file mode 100644 index 0000000..3f489f4 --- /dev/null +++ b/lib/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 .= '
+

Create new address book

+ + +
+
+ +
+ '; + + 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/vendor/sabre/dav/lib/CardDAV/VCFExportPlugin.php b/lib/vendor/sabre/dav/lib/CardDAV/VCFExportPlugin.php new file mode 100644 index 0000000..431391e --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CardDAV/Xml/Filter/AddressData.php b/lib/vendor/sabre/dav/lib/CardDAV/Xml/Filter/AddressData.php new file mode 100644 index 0000000..b60fceb --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CardDAV/Xml/Filter/ParamFilter.php b/lib/vendor/sabre/dav/lib/CardDAV/Xml/Filter/ParamFilter.php new file mode 100644 index 0000000..0a7ec06 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CardDAV/Xml/Filter/PropFilter.php b/lib/vendor/sabre/dav/lib/CardDAV/Xml/Filter/PropFilter.php new file mode 100644 index 0000000..5dedac8 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CardDAV/Xml/Property/SupportedAddressData.php b/lib/vendor/sabre/dav/lib/CardDAV/Xml/Property/SupportedAddressData.php new file mode 100644 index 0000000..536c5a1 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CardDAV/Xml/Property/SupportedCollationSet.php b/lib/vendor/sabre/dav/lib/CardDAV/Xml/Property/SupportedCollationSet.php new file mode 100644 index 0000000..b19eddd --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CardDAV/Xml/Request/AddressBookMultiGetReport.php b/lib/vendor/sabre/dav/lib/CardDAV/Xml/Request/AddressBookMultiGetReport.php new file mode 100644 index 0000000..491f969 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/CardDAV/Xml/Request/AddressBookQueryReport.php b/lib/vendor/sabre/dav/lib/CardDAV/Xml/Request/AddressBookQueryReport.php new file mode 100644 index 0000000..02402f6 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractBasic.php b/lib/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractBasic.php new file mode 100644 index 0000000..3132333 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractBearer.php b/lib/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractBearer.php new file mode 100644 index 0000000..b681747 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractDigest.php b/lib/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractDigest.php new file mode 100644 index 0000000..297655d --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Auth/Backend/Apache.php b/lib/vendor/sabre/dav/lib/DAV/Auth/Backend/Apache.php new file mode 100644 index 0000000..ebf67ca --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Auth/Backend/BackendInterface.php b/lib/vendor/sabre/dav/lib/DAV/Auth/Backend/BackendInterface.php new file mode 100644 index 0000000..133eac9 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Auth/Backend/BasicCallBack.php b/lib/vendor/sabre/dav/lib/DAV/Auth/Backend/BasicCallBack.php new file mode 100644 index 0000000..5a8bb98 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Auth/Backend/File.php b/lib/vendor/sabre/dav/lib/DAV/Auth/Backend/File.php new file mode 100644 index 0000000..ea2d396 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Auth/Backend/IMAP.php b/lib/vendor/sabre/dav/lib/DAV/Auth/Backend/IMAP.php new file mode 100644 index 0000000..3a18311 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Auth/Backend/PDO.php b/lib/vendor/sabre/dav/lib/DAV/Auth/Backend/PDO.php new file mode 100644 index 0000000..9a06912 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Auth/Backend/PDOBasicAuth.php b/lib/vendor/sabre/dav/lib/DAV/Auth/Backend/PDOBasicAuth.php new file mode 100644 index 0000000..d142cbf --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Auth/Plugin.php b/lib/vendor/sabre/dav/lib/DAV/Auth/Plugin.php new file mode 100644 index 0000000..47fbe20 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Browser/GuessContentType.php b/lib/vendor/sabre/dav/lib/DAV/Browser/GuessContentType.php new file mode 100644 index 0000000..5cda0b8 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Browser/HtmlOutput.php b/lib/vendor/sabre/dav/lib/DAV/Browser/HtmlOutput.php new file mode 100644 index 0000000..be5a284 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Browser/MapGetToPropFind.php b/lib/vendor/sabre/dav/lib/DAV/Browser/MapGetToPropFind.php new file mode 100644 index 0000000..0bbe70c --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Browser/Plugin.php b/lib/vendor/sabre/dav/lib/DAV/Browser/Plugin.php new file mode 100644 index 0000000..a8a6f43 --- /dev/null +++ b/lib/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 .= "

Nodes

\n"; + $html .= ''; + + $subNodes = $this->server->getPropertiesForChildren($path, [ + '{DAV:}displayname', + '{DAV:}resourcetype', + '{DAV:}getcontenttype', + '{DAV:}getcontentlength', + '{DAV:}getlastmodified', + ]); + + foreach ($subNodes as $subPath => $subProps) { + $subNode = $this->server->tree->getNodeForPath($subPath); + $fullPath = $this->server->getBaseUri().HTTP\encodePath($subPath); + list(, $displayPath) = Uri\split($subPath); + + $subNodes[$subPath]['subNode'] = $subNode; + $subNodes[$subPath]['fullPath'] = $fullPath; + $subNodes[$subPath]['displayPath'] = $displayPath; + } + uasort($subNodes, [$this, 'compareNodes']); + + foreach ($subNodes as $subProps) { + $type = [ + 'string' => 'Unknown', + 'icon' => 'cog', + ]; + if (isset($subProps['{DAV:}resourcetype'])) { + $type = $this->mapResourceType($subProps['{DAV:}resourcetype']->getValue(), $subProps['subNode']); + } + + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + + $buttonActions = ''; + if ($subProps['subNode'] instanceof DAV\IFile) { + $buttonActions = ''; + } + $this->server->emit('browserButtonActions', [$subProps['fullPath'], $subProps['subNode'], &$buttonActions]); + + $html .= ''; + $html .= ''; + } + + $html .= '
'.$this->escapeHTML($subProps['displayPath']).''.$this->escapeHTML($type['string']).''; + if (isset($subProps['{DAV:}getcontentlength'])) { + $html .= $this->escapeHTML($subProps['{DAV:}getcontentlength'].' bytes'); + } + $html .= ''; + if (isset($subProps['{DAV:}getlastmodified'])) { + $lastMod = $subProps['{DAV:}getlastmodified']->getTime(); + $html .= $this->escapeHTML($lastMod->format('F j, Y, g:i a')); + } + $html .= ''; + if (isset($subProps['{DAV:}displayname'])) { + $html .= $this->escapeHTML($subProps['{DAV:}displayname']); + } + $html .= ''.$buttonActions.'
'; + } + + $html .= '
'; + $html .= '

Properties

'; + $html .= ''; + + // Allprops request + $propFind = new PropFindAll($path); + $properties = $this->server->getPropertiesByNode($propFind, $node); + + $properties = $propFind->getResultForMultiStatus()[200]; + + foreach ($properties as $propName => $propValue) { + if (!in_array($propName, $this->uninterestingProperties)) { + $html .= $this->drawPropertyRow($propName, $propValue); + } + } + + $html .= '
'; + $html .= '
'; + + /* Start of generating actions */ + + $output = ''; + if ($this->enablePost) { + $this->server->emit('onHTMLActionsPanel', [$node, &$output, $path]); + } + + if ($output) { + $html .= '

Actions

'; + $html .= "
\n"; + $html .= $output; + $html .= "
\n"; + $html .= "
\n"; + } + + $html .= $this->generateFooter(); + + $this->server->httpResponse->setHeader('Content-Security-Policy', "default-src 'none'; img-src 'self'; style-src 'self'; font-src 'self';"); + + return $html; + } + + /** + * Generates the 'plugins' page. + * + * @return string + */ + public function generatePluginListing() + { + $html = $this->generateHeader('Plugins'); + + $html .= '

Plugins

'; + $html .= ''; + foreach ($this->server->getPlugins() as $plugin) { + $info = $plugin->getPluginInfo(); + $html .= ''; + $html .= ''; + $html .= ''; + } + $html .= '
'.$info['name'].''.$info['description'].''; + if (isset($info['link']) && $info['link']) { + $html .= ''; + } + $html .= '
'; + $html .= '
'; + + /* Start of generating actions */ + + $html .= $this->generateFooter(); + + return $html; + } + + /** + * Generates the first block of HTML, including the tag and page + * header. + * + * Returns footer. + * + * @param string $title + * @param string $path + * + * @return string + */ + public function generateHeader($title, $path = null) + { + $version = ''; + if (DAV\Server::$exposeVersion) { + $version = DAV\Version::VERSION; + } + + $vars = [ + 'title' => $this->escapeHTML($title), + 'favicon' => $this->escapeHTML($this->getAssetUrl('favicon.ico')), + 'style' => $this->escapeHTML($this->getAssetUrl('sabredav.css')), + 'iconstyle' => $this->escapeHTML($this->getAssetUrl('openiconic/open-iconic.css')), + 'logo' => $this->escapeHTML($this->getAssetUrl('sabredav.png')), + 'baseUrl' => $this->server->getBaseUri(), + ]; + + $html = << + + + $vars[title] - sabre/dav $version + + + + + + +
+ +
+ + '; + + 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

+ +
+ + +
+

Upload file

+ +
+
+ +
+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/vendor/sabre/dav/lib/DAV/Browser/PropFindAll.php b/lib/vendor/sabre/dav/lib/DAV/Browser/PropFindAll.php new file mode 100644 index 0000000..34702bd --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Browser/assets/favicon.ico b/lib/vendor/sabre/dav/lib/DAV/Browser/assets/favicon.ico new file mode 100644 index 0000000..2b2c10a Binary files /dev/null and b/lib/vendor/sabre/dav/lib/DAV/Browser/assets/favicon.ico differ diff --git a/lib/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/ICON-LICENSE b/lib/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/ICON-LICENSE new file mode 100644 index 0000000..2199f4a --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.css b/lib/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.css new file mode 100644 index 0000000..e748674 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.eot b/lib/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/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.eot differ diff --git a/lib/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.otf b/lib/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/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.otf differ diff --git a/lib/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.svg b/lib/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.svg new file mode 100644 index 0000000..0792c00 --- /dev/null +++ b/lib/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.svg @@ -0,0 +1,543 @@ + + + + + +Created by FontForge 20120731 at Wed Apr 30 22:56:47 2014 + By P.J. Onori +Created by P.J. Onori with FontForge 2.0 (http://fontforge.sf.net) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.ttf b/lib/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/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.ttf differ diff --git a/lib/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.woff b/lib/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/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.woff differ diff --git a/lib/vendor/sabre/dav/lib/DAV/Browser/assets/sabredav.css b/lib/vendor/sabre/dav/lib/DAV/Browser/assets/sabredav.css new file mode 100644 index 0000000..8869597 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Browser/assets/sabredav.png b/lib/vendor/sabre/dav/lib/DAV/Browser/assets/sabredav.png new file mode 100644 index 0000000..48a9739 Binary files /dev/null and b/lib/vendor/sabre/dav/lib/DAV/Browser/assets/sabredav.png differ diff --git a/lib/vendor/sabre/dav/lib/DAV/Client.php b/lib/vendor/sabre/dav/lib/DAV/Client.php new file mode 100644 index 0000000..1028a6b --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Collection.php b/lib/vendor/sabre/dav/lib/DAV/Collection.php new file mode 100644 index 0000000..2728bb2 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/CorePlugin.php b/lib/vendor/sabre/dav/lib/DAV/CorePlugin.php new file mode 100644 index 0000000..dbd8976 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Exception.php b/lib/vendor/sabre/dav/lib/DAV/Exception.php new file mode 100644 index 0000000..9fc1d16 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Exception/Forbidden.php b/lib/vendor/sabre/dav/lib/DAV/Exception/Forbidden.php new file mode 100644 index 0000000..2f882c3 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Exception/InvalidSyncToken.php b/lib/vendor/sabre/dav/lib/DAV/Exception/InvalidSyncToken.php new file mode 100644 index 0000000..f28d20f --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Exception/LengthRequired.php b/lib/vendor/sabre/dav/lib/DAV/Exception/LengthRequired.php new file mode 100644 index 0000000..9d26fcb --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Exception/Locked.php b/lib/vendor/sabre/dav/lib/DAV/Exception/Locked.php new file mode 100644 index 0000000..24fad70 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php b/lib/vendor/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php new file mode 100644 index 0000000..dbf42ed --- /dev/null +++ b/lib/vendor/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php @@ -0,0 +1,46 @@ +getAllowedMethods($server->getRequestUri()); + + return [ + 'Allow' => strtoupper(implode(', ', $methods)), + ]; + } +} diff --git a/lib/vendor/sabre/dav/lib/DAV/Exception/NotAuthenticated.php b/lib/vendor/sabre/dav/lib/DAV/Exception/NotAuthenticated.php new file mode 100644 index 0000000..0a5ba9b --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Exception/ReportNotSupported.php b/lib/vendor/sabre/dav/lib/DAV/Exception/ReportNotSupported.php new file mode 100644 index 0000000..a483838 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Exception/RequestedRangeNotSatisfiable.php b/lib/vendor/sabre/dav/lib/DAV/Exception/RequestedRangeNotSatisfiable.php new file mode 100644 index 0000000..6ccb5b8 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Exception/TooManyMatches.php b/lib/vendor/sabre/dav/lib/DAV/Exception/TooManyMatches.php new file mode 100644 index 0000000..ef6f502 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Exception/UnsupportedMediaType.php b/lib/vendor/sabre/dav/lib/DAV/Exception/UnsupportedMediaType.php new file mode 100644 index 0000000..bc9da30 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/FS/File.php b/lib/vendor/sabre/dav/lib/DAV/FS/File.php new file mode 100644 index 0000000..b78a801 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/FS/Node.php b/lib/vendor/sabre/dav/lib/DAV/FS/Node.php new file mode 100644 index 0000000..32aa747 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/FSExt/Directory.php b/lib/vendor/sabre/dav/lib/DAV/FSExt/Directory.php new file mode 100644 index 0000000..d6aea00 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/FSExt/File.php b/lib/vendor/sabre/dav/lib/DAV/FSExt/File.php new file mode 100644 index 0000000..74849b5 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/File.php b/lib/vendor/sabre/dav/lib/DAV/File.php new file mode 100644 index 0000000..daf83aa --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Locks/Backend/PDO.php b/lib/vendor/sabre/dav/lib/DAV/Locks/Backend/PDO.php new file mode 100644 index 0000000..3f425f9 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Locks/LockInfo.php b/lib/vendor/sabre/dav/lib/DAV/Locks/LockInfo.php new file mode 100644 index 0000000..df82275 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/MkCol.php b/lib/vendor/sabre/dav/lib/DAV/MkCol.php new file mode 100644 index 0000000..f3c5ea5 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Mount/Plugin.php b/lib/vendor/sabre/dav/lib/DAV/Mount/Plugin.php new file mode 100644 index 0000000..b7f4851 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Node.php b/lib/vendor/sabre/dav/lib/DAV/Node.php new file mode 100644 index 0000000..948060d --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/PropFind.php b/lib/vendor/sabre/dav/lib/DAV/PropFind.php new file mode 100644 index 0000000..e9ffb07 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/PropPatch.php b/lib/vendor/sabre/dav/lib/DAV/PropPatch.php new file mode 100644 index 0000000..092909d --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/BackendInterface.php b/lib/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/BackendInterface.php new file mode 100644 index 0000000..64a8825 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php b/lib/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php new file mode 100644 index 0000000..8960331 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/PropertyStorage/Plugin.php b/lib/vendor/sabre/dav/lib/DAV/PropertyStorage/Plugin.php new file mode 100644 index 0000000..da47ec9 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Server.php b/lib/vendor/sabre/dav/lib/DAV/Server.php new file mode 100644 index 0000000..3133e54 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/ServerPlugin.php b/lib/vendor/sabre/dav/lib/DAV/ServerPlugin.php new file mode 100644 index 0000000..70acb01 --- /dev/null +++ b/lib/vendor/sabre/dav/lib/DAV/ServerPlugin.php @@ -0,0 +1,105 @@ + $this->getPluginName(), + 'description' => null, + 'link' => null, + ]; + } +} diff --git a/lib/vendor/sabre/dav/lib/DAV/Sharing/ISharedNode.php b/lib/vendor/sabre/dav/lib/DAV/Sharing/ISharedNode.php new file mode 100644 index 0000000..a746ac7 --- /dev/null +++ b/lib/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 .= '
+

Share this resource

+ +
+ +
+ +
+ '; + } + + /** + * 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/vendor/sabre/dav/lib/DAV/SimpleCollection.php b/lib/vendor/sabre/dav/lib/DAV/SimpleCollection.php new file mode 100644 index 0000000..3cd14d9 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/SimpleFile.php b/lib/vendor/sabre/dav/lib/DAV/SimpleFile.php new file mode 100644 index 0000000..ca808b6 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/StringUtil.php b/lib/vendor/sabre/dav/lib/DAV/StringUtil.php new file mode 100644 index 0000000..edfb7fa --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Sync/Plugin.php b/lib/vendor/sabre/dav/lib/DAV/Sync/Plugin.php new file mode 100644 index 0000000..8609f75 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/TemporaryFileFilterPlugin.php b/lib/vendor/sabre/dav/lib/DAV/TemporaryFileFilterPlugin.php new file mode 100644 index 0000000..9f8ec5b --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Tree.php b/lib/vendor/sabre/dav/lib/DAV/Tree.php new file mode 100644 index 0000000..1483e1b --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/UUIDUtil.php b/lib/vendor/sabre/dav/lib/DAV/UUIDUtil.php new file mode 100644 index 0000000..8c36e1b --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Xml/Element/Response.php b/lib/vendor/sabre/dav/lib/DAV/Xml/Element/Response.php new file mode 100644 index 0000000..df92914 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Xml/Element/Sharee.php b/lib/vendor/sabre/dav/lib/DAV/Xml/Element/Sharee.php new file mode 100644 index 0000000..33564d8 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Xml/Property/Complex.php b/lib/vendor/sabre/dav/lib/DAV/Xml/Property/Complex.php new file mode 100644 index 0000000..787d30d --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Xml/Property/GetLastModified.php b/lib/vendor/sabre/dav/lib/DAV/Xml/Property/GetLastModified.php new file mode 100644 index 0000000..efc15c2 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Xml/Property/Href.php b/lib/vendor/sabre/dav/lib/DAV/Xml/Property/Href.php new file mode 100644 index 0000000..d4e43da --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Xml/Property/Invite.php b/lib/vendor/sabre/dav/lib/DAV/Xml/Property/Invite.php new file mode 100644 index 0000000..e3f0a61 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Xml/Property/LocalHref.php b/lib/vendor/sabre/dav/lib/DAV/Xml/Property/LocalHref.php new file mode 100644 index 0000000..cb79497 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Xml/Property/ResourceType.php b/lib/vendor/sabre/dav/lib/DAV/Xml/Property/ResourceType.php new file mode 100644 index 0000000..75ddcba --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Xml/Property/ShareAccess.php b/lib/vendor/sabre/dav/lib/DAV/Xml/Property/ShareAccess.php new file mode 100644 index 0000000..fdd5555 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedLock.php b/lib/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedLock.php new file mode 100644 index 0000000..100829c --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedMethodSet.php b/lib/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedMethodSet.php new file mode 100644 index 0000000..6344010 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedReportSet.php b/lib/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedReportSet.php new file mode 100644 index 0000000..0b4990e --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Xml/Request/Lock.php b/lib/vendor/sabre/dav/lib/DAV/Xml/Request/Lock.php new file mode 100644 index 0000000..57d12ef --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Xml/Request/MkCol.php b/lib/vendor/sabre/dav/lib/DAV/Xml/Request/MkCol.php new file mode 100644 index 0000000..e0d7e90 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Xml/Request/PropFind.php b/lib/vendor/sabre/dav/lib/DAV/Xml/Request/PropFind.php new file mode 100644 index 0000000..505e7c7 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Xml/Request/PropPatch.php b/lib/vendor/sabre/dav/lib/DAV/Xml/Request/PropPatch.php new file mode 100644 index 0000000..4a27095 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Xml/Request/ShareResource.php b/lib/vendor/sabre/dav/lib/DAV/Xml/Request/ShareResource.php new file mode 100644 index 0000000..79d7dc8 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Xml/Request/SyncCollectionReport.php b/lib/vendor/sabre/dav/lib/DAV/Xml/Request/SyncCollectionReport.php new file mode 100644 index 0000000..8dd9576 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Xml/Response/MultiStatus.php b/lib/vendor/sabre/dav/lib/DAV/Xml/Response/MultiStatus.php new file mode 100644 index 0000000..e824cda --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAV/Xml/Service.php b/lib/vendor/sabre/dav/lib/DAV/Xml/Service.php new file mode 100644 index 0000000..4406b02 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAVACL/ACLTrait.php b/lib/vendor/sabre/dav/lib/DAVACL/ACLTrait.php new file mode 100644 index 0000000..98c1ce3 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAVACL/AbstractPrincipalCollection.php b/lib/vendor/sabre/dav/lib/DAVACL/AbstractPrincipalCollection.php new file mode 100644 index 0000000..d26f7d2 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAVACL/Exception/AceConflict.php b/lib/vendor/sabre/dav/lib/DAVACL/Exception/AceConflict.php new file mode 100644 index 0000000..0fc3f77 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAVACL/Exception/NeedPrivileges.php b/lib/vendor/sabre/dav/lib/DAVACL/Exception/NeedPrivileges.php new file mode 100644 index 0000000..af1f01c --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAVACL/Exception/NoAbstract.php b/lib/vendor/sabre/dav/lib/DAVACL/Exception/NoAbstract.php new file mode 100644 index 0000000..b9c6616 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAVACL/Exception/NotRecognizedPrincipal.php b/lib/vendor/sabre/dav/lib/DAVACL/Exception/NotRecognizedPrincipal.php new file mode 100644 index 0000000..d4e7284 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAVACL/Exception/NotSupportedPrivilege.php b/lib/vendor/sabre/dav/lib/DAVACL/Exception/NotSupportedPrivilege.php new file mode 100644 index 0000000..c04c5fa --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAVACL/FS/Collection.php b/lib/vendor/sabre/dav/lib/DAVACL/FS/Collection.php new file mode 100644 index 0000000..85b04e2 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAVACL/FS/File.php b/lib/vendor/sabre/dav/lib/DAVACL/FS/File.php new file mode 100644 index 0000000..5506aa2 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAVACL/FS/HomeCollection.php b/lib/vendor/sabre/dav/lib/DAVACL/FS/HomeCollection.php new file mode 100644 index 0000000..fa476e0 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAVACL/IACL.php b/lib/vendor/sabre/dav/lib/DAVACL/IACL.php new file mode 100644 index 0000000..291fb24 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAVACL/Plugin.php b/lib/vendor/sabre/dav/lib/DAVACL/Plugin.php new file mode 100644 index 0000000..f049784 --- /dev/null +++ b/lib/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 .= '
+

Create new principal

+ + +
+
+
+ +
+ '; + + 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/vendor/sabre/dav/lib/DAVACL/Principal.php b/lib/vendor/sabre/dav/lib/DAVACL/Principal.php new file mode 100644 index 0000000..ada38ab --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/AbstractBackend.php b/lib/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/AbstractBackend.php new file mode 100644 index 0000000..f61a0c9 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/BackendInterface.php b/lib/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/BackendInterface.php new file mode 100644 index 0000000..2fd3191 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAVACL/PrincipalCollection.php b/lib/vendor/sabre/dav/lib/DAVACL/PrincipalCollection.php new file mode 100644 index 0000000..b823b6c --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAVACL/Xml/Property/Acl.php b/lib/vendor/sabre/dav/lib/DAVACL/Xml/Property/Acl.php new file mode 100644 index 0000000..c6e236d --- /dev/null +++ b/lib/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 ''; + foreach ($this->privileges as $privilege) { + echo ''; + // if it starts with a {, it's a special principal + if ('{' === $privilege['principal'][0]) { + echo ''; + } else { + echo ''; + } + echo ''; + echo ''; + echo ''; + } + echo '
PrincipalPrivilege
', $html->xmlName($privilege['principal']), '', $html->link($privilege['principal']), '', $html->xmlName($privilege['privilege']), ''; + if (!empty($privilege['protected'])) { + echo '(protected)'; + } + 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/vendor/sabre/dav/lib/DAVACL/Xml/Property/AclRestrictions.php b/lib/vendor/sabre/dav/lib/DAVACL/Xml/Property/AclRestrictions.php new file mode 100644 index 0000000..b5629c8 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAVACL/Xml/Property/CurrentUserPrivilegeSet.php b/lib/vendor/sabre/dav/lib/DAVACL/Xml/Property/CurrentUserPrivilegeSet.php new file mode 100644 index 0000000..e38a45c --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAVACL/Xml/Property/Principal.php b/lib/vendor/sabre/dav/lib/DAVACL/Xml/Property/Principal.php new file mode 100644 index 0000000..5b9ee45 --- /dev/null +++ b/lib/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/vendor/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php b/lib/vendor/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php new file mode 100644 index 0000000..6e7514b --- /dev/null +++ b/lib/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 '
  • '; + echo $html->xmlName($privName); + if (isset($priv['abstract']) && $priv['abstract']) { + echo ' (abstract)'; + } + if (isset($priv['description'])) { + echo ' '.$html->h($priv['description']); + } + if (isset($priv['aggregates'])) { + echo "\n
      \n"; + foreach ($priv['aggregates'] as $subPrivName => $subPriv) { + $traverse($subPrivName, $subPriv); + } + echo '
    '; + } + echo "
  • \n"; + }; + + ob_start(); + echo '
      '; + $traverse('{DAV:}all', ['aggregates' => $this->getValue()]); + echo "
    \n"; + + return ob_get_clean(); + } + + /** + * Serializes a property. + * + * This is a recursive function. + * + * @param string $privName + * @param array $privilege + */ + private function serializePriv(Writer $writer, $privName, $privilege) + { + $writer->startElement('{DAV:}supported-privilege'); + + $writer->startElement('{DAV:}privilege'); + $writer->writeElement($privName); + $writer->endElement(); // privilege + + if (!empty($privilege['abstract'])) { + $writer->writeElement('{DAV:}abstract'); + } + if (!empty($privilege['description'])) { + $writer->writeElement('{DAV:}description', $privilege['description']); + } + if (isset($privilege['aggregates'])) { + foreach ($privilege['aggregates'] as $subPrivName => $subPrivilege) { + $this->serializePriv($writer, $subPrivName, $subPrivilege); + } + } + + $writer->endElement(); // supported-privilege + } +} diff --git a/lib/vendor/sabre/dav/lib/DAVACL/Xml/Request/AclPrincipalPropSetReport.php b/lib/vendor/sabre/dav/lib/DAVACL/Xml/Request/AclPrincipalPropSetReport.php new file mode 100644 index 0000000..4fc6127 --- /dev/null +++ b/lib/vendor/sabre/dav/lib/DAVACL/Xml/Request/AclPrincipalPropSetReport.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) + { + $reader->pushContext(); + $reader->elementMap['{DAV:}prop'] = 'Sabre\Xml\Deserializer\enum'; + + $elems = Deserializer\keyValue( + $reader, + 'DAV:' + ); + + $reader->popContext(); + + $report = new self(); + + if (!empty($elems['prop'])) { + $report->properties = $elems['prop']; + } + + return $report; + } +} diff --git a/lib/vendor/sabre/dav/lib/DAVACL/Xml/Request/ExpandPropertyReport.php b/lib/vendor/sabre/dav/lib/DAVACL/Xml/Request/ExpandPropertyReport.php new file mode 100644 index 0000000..70a7e22 --- /dev/null +++ b/lib/vendor/sabre/dav/lib/DAVACL/Xml/Request/ExpandPropertyReport.php @@ -0,0 +1,100 @@ +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(); + + $obj = new self(); + $obj->properties = self::traverse($elems); + + return $obj; + } + + /** + * This method is used by deserializeXml, to recursively parse the + * {DAV:}property elements. + * + * @param array $elems + * + * @return array + */ + private static function traverse($elems) + { + $result = []; + + foreach ($elems as $elem) { + if ('{DAV:}property' !== $elem['name']) { + continue; + } + + $namespace = isset($elem['attributes']['namespace']) ? + $elem['attributes']['namespace'] : + 'DAV:'; + + $propName = '{'.$namespace.'}'.$elem['attributes']['name']; + + $value = null; + if (is_array($elem['value'])) { + $value = self::traverse($elem['value']); + } + + $result[$propName] = $value; + } + + return $result; + } +} diff --git a/lib/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalMatchReport.php b/lib/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalMatchReport.php new file mode 100644 index 0000000..b495824 --- /dev/null +++ b/lib/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalMatchReport.php @@ -0,0 +1,106 @@ +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:}prop'] = 'Sabre\Xml\Deserializer\enum'; + + $elems = Deserializer\keyValue( + $reader, + 'DAV:' + ); + + $reader->popContext(); + + $principalMatch = new self(); + + if (array_key_exists('self', $elems)) { + $principalMatch->type = self::SELF; + } + + if (array_key_exists('principal-property', $elems)) { + $principalMatch->type = self::PRINCIPAL_PROPERTY; + $principalMatch->principalProperty = $elems['principal-property'][0]['name']; + } + + if (!empty($elems['prop'])) { + $principalMatch->properties = $elems['prop']; + } + + return $principalMatch; + } +} diff --git a/lib/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalPropertySearchReport.php b/lib/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalPropertySearchReport.php new file mode 100644 index 0000000..bddceca --- /dev/null +++ b/lib/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalPropertySearchReport.php @@ -0,0 +1,122 @@ +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(); + + $foundSearchProp = false; + $self->test = 'allof'; + if ('anyof' === $reader->getAttribute('test')) { + $self->test = 'anyof'; + } + + $elemMap = [ + '{DAV:}property-search' => 'Sabre\\Xml\\Element\\KeyValue', + '{DAV:}prop' => 'Sabre\\Xml\\Element\\KeyValue', + ]; + + foreach ($reader->parseInnerTree($elemMap) as $elem) { + switch ($elem['name']) { + case '{DAV:}prop': + $self->properties = array_keys($elem['value']); + break; + case '{DAV:}property-search': + $foundSearchProp = true; + // This property has two sub-elements: + // {DAV:}prop - The property to be searched on. This may + // also be more than one + // {DAV:}match - The value to match with + if (!isset($elem['value']['{DAV:}prop']) || !isset($elem['value']['{DAV:}match'])) { + throw new BadRequest('The {DAV:}property-search element must contain one {DAV:}match and one {DAV:}prop element'); + } + foreach ($elem['value']['{DAV:}prop'] as $propName => $discard) { + $self->searchProperties[$propName] = $elem['value']['{DAV:}match']; + } + break; + case '{DAV:}apply-to-principal-collection-set': + $self->applyToPrincipalCollectionSet = true; + break; + } + } + if (!$foundSearchProp) { + throw new BadRequest('The {DAV:}principal-property-search report must contain at least 1 {DAV:}property-search element'); + } + + return $self; + } +} diff --git a/lib/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalSearchPropertySetReport.php b/lib/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalSearchPropertySetReport.php new file mode 100644 index 0000000..7f15d8a --- /dev/null +++ b/lib/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalSearchPropertySetReport.php @@ -0,0 +1,58 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + if (!$reader->isEmptyElement) { + throw new BadRequest('The {DAV:}principal-search-property-set element must be empty'); + } + + // The element is actually empty, so there's not much to do. + $reader->next(); + + $self = new self(); + + return $self; + } +} diff --git a/lib/vendor/sabre/event/.php-cs-fixer.dist.php b/lib/vendor/sabre/event/.php-cs-fixer.dist.php new file mode 100644 index 0000000..319886c --- /dev/null +++ b/lib/vendor/sabre/event/.php-cs-fixer.dist.php @@ -0,0 +1,18 @@ +exclude('vendor') + ->in(__DIR__); + +$config = new PhpCsFixer\Config(); +$config->setRules([ + '@PSR1' => true, + '@Symfony' => true, + 'blank_line_between_import_groups' => false, + 'nullable_type_declaration' => [ + 'syntax' => 'question_mark', + ], + 'nullable_type_declaration_for_default_null_value' => true, +]); +$config->setFinder($finder); +return $config; \ No newline at end of file diff --git a/lib/vendor/sabre/event/LICENSE b/lib/vendor/sabre/event/LICENSE new file mode 100644 index 0000000..962a492 --- /dev/null +++ b/lib/vendor/sabre/event/LICENSE @@ -0,0 +1,27 @@ +Copyright (C) 2013-2016 fruux GmbH (https://fruux.com/) + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name Sabre nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/vendor/sabre/event/composer.json b/lib/vendor/sabre/event/composer.json new file mode 100644 index 0000000..0d3ec06 --- /dev/null +++ b/lib/vendor/sabre/event/composer.json @@ -0,0 +1,69 @@ +{ + "name": "sabre/event", + "description": "sabre/event is a library for lightweight event-based programming", + "keywords": [ + "Events", + "EventEmitter", + "Promise", + "Hooks", + "Plugin", + "Signal", + "Async", + "EventLoop", + "Reactor", + "Coroutine" + ], + "homepage": "http://sabre.io/event/", + "license": "BSD-3-Clause", + "require": { + "php": "^7.1 || ^8.0" + }, + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "source": "https://github.com/fruux/sabre-event" + }, + "autoload": { + "psr-4": { + "Sabre\\Event\\": "lib/" + }, + "files" : [ + "lib/coroutine.php", + "lib/Loop/functions.php", + "lib/Promise/functions.php" + ] + }, + "autoload-dev": { + "psr-4" : { + "Sabre\\Event\\" : "tests/Event" + } + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1||^3.63", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit" : "^7.5 || ^8.5 || ^9.6" + }, + "scripts": { + "phpstan": [ + "phpstan analyse lib tests" + ], + "cs-fixer": [ + "PHP_CS_FIXER_IGNORE_ENV=true php-cs-fixer fix" + ], + "phpunit": [ + "phpunit --configuration tests/phpunit.xml" + ], + "test": [ + "composer phpstan", + "composer cs-fixer", + "composer phpunit" + ] + } +} diff --git a/lib/vendor/sabre/event/lib/Emitter.php b/lib/vendor/sabre/event/lib/Emitter.php new file mode 100644 index 0000000..e1f23fc --- /dev/null +++ b/lib/vendor/sabre/event/lib/Emitter.php @@ -0,0 +1,19 @@ +listeners[$eventName])) { + $this->listeners[$eventName] = [ + true, // If there's only one item, it's sorted + [$priority], + [$callBack], + ]; + } else { + $this->listeners[$eventName][0] = false; // marked as unsorted + $this->listeners[$eventName][1][] = $priority; + $this->listeners[$eventName][2][] = $callBack; + } + } + + /** + * Subscribe to an event exactly once. + */ + public function once(string $eventName, callable $callBack, int $priority = 100) + { + $wrapper = null; + $wrapper = function () use ($eventName, $callBack, &$wrapper) { + $this->removeListener($eventName, $wrapper); + + return \call_user_func_array($callBack, \func_get_args()); + }; + + $this->on($eventName, $wrapper, $priority); + } + + /** + * Emits an event. + * + * This method will return true if 0 or more listeners were successfully + * handled. false is returned if one of the events broke the event chain. + * + * If the continueCallBack is specified, this callback will be called every + * time before the next event handler is called. + * + * If the continueCallback returns false, event propagation stops. This + * allows you to use the eventEmitter as a means for listeners to implement + * functionality in your application, and break the event loop as soon as + * some condition is fulfilled. + * + * Note that returning false from an event subscriber breaks propagation + * and returns false, but if the continue-callback stops propagation, this + * is still considered a 'successful' operation and returns true. + * + * Lastly, if there are 5 event handlers for an event. The continueCallback + * will be called at most 4 times. + */ + public function emit(string $eventName, array $arguments = [], ?callable $continueCallBack = null): bool + { + if (\is_null($continueCallBack)) { + foreach ($this->listeners($eventName) as $listener) { + $result = \call_user_func_array($listener, $arguments); + if (false === $result) { + return false; + } + } + } else { + $listeners = $this->listeners($eventName); + $counter = \count($listeners); + + foreach ($listeners as $listener) { + --$counter; + $result = \call_user_func_array($listener, $arguments); + if (false === $result) { + return false; + } + + if ($counter > 0) { + if (!$continueCallBack()) { + break; + } + } + } + } + + return true; + } + + /** + * Returns the list of listeners for an event. + * + * The list is returned as an array, and the list of events are sorted by + * their priority. + * + * @return callable[] + */ + public function listeners(string $eventName): array + { + if (!isset($this->listeners[$eventName])) { + return []; + } + + // The list is not sorted + if (!$this->listeners[$eventName][0]) { + // Sorting + \array_multisort($this->listeners[$eventName][1], SORT_NUMERIC, $this->listeners[$eventName][2]); + + // Marking the listeners as sorted + $this->listeners[$eventName][0] = true; + } + + return $this->listeners[$eventName][2]; + } + + /** + * Removes a specific listener from an event. + * + * If the listener could not be found, this method will return false. If it + * was removed it will return true. + */ + public function removeListener(string $eventName, callable $listener): bool + { + if (!isset($this->listeners[$eventName])) { + return false; + } + foreach ($this->listeners[$eventName][2] as $index => $check) { + if ($check === $listener) { + unset($this->listeners[$eventName][1][$index]); + unset($this->listeners[$eventName][2][$index]); + + return true; + } + } + + return false; + } + + /** + * Removes all listeners. + * + * If the eventName argument is specified, all listeners for that event are + * removed. If it is not specified, every listener for every event is + * removed. + */ + public function removeAllListeners(?string $eventName = null) + { + if (!\is_null($eventName)) { + unset($this->listeners[$eventName]); + } else { + $this->listeners = []; + } + } + + /** + * The list of listeners. + * + * @var array + */ + protected $listeners = []; +} diff --git a/lib/vendor/sabre/event/lib/EventEmitter.php b/lib/vendor/sabre/event/lib/EventEmitter.php new file mode 100644 index 0000000..865c99b --- /dev/null +++ b/lib/vendor/sabre/event/lib/EventEmitter.php @@ -0,0 +1,20 @@ +timers) { + // Special case when the timers array was empty. + $this->timers[] = [$triggerTime, $cb]; + + return; + } + + // We need to insert these values in the timers array, but the timers + // array must be in reverse-order of trigger times. + // + // So here we search the array for the insertion point. + $index = count($this->timers) - 1; + while (true) { + if ($triggerTime < $this->timers[$index][0]) { + array_splice( + $this->timers, + $index + 1, + 0, + [[$triggerTime, $cb]] + ); + break; + } elseif (0 === $index) { + array_unshift($this->timers, [$triggerTime, $cb]); + break; + } + --$index; + } + } + + /** + * Executes a function every x seconds. + * + * The value this function returns can be used to stop the interval with + * clearInterval. + */ + public function setInterval(callable $cb, float $timeout): array + { + $keepGoing = true; + $f = null; + + $f = function () use ($cb, &$f, $timeout, &$keepGoing) { + if ($keepGoing) { + $cb(); + $this->setTimeout($f, $timeout); + } + }; + $this->setTimeout($f, $timeout); + + // Really the only thing that matters is returning the $keepGoing + // boolean value. + // + // We need to pack it in an array to allow returning by reference. + // Because I'm worried people will be confused by using a boolean as a + // sort of identifier, I added an extra string. + return ['I\'m an implementation detail', &$keepGoing]; + } + + /** + * Stops a running interval. + */ + public function clearInterval(array $intervalId) + { + $intervalId[1] = false; + } + + /** + * Runs a function immediately at the next iteration of the loop. + */ + public function nextTick(callable $cb) + { + $this->nextTick[] = $cb; + } + + /** + * Adds a read stream. + * + * The callback will be called as soon as there is something to read from + * the stream. + * + * You MUST call removeReadStream after you are done with the stream, to + * prevent the eventloop from never stopping. + * + * @param resource $stream + */ + public function addReadStream($stream, callable $cb) + { + $this->readStreams[(int) $stream] = $stream; + $this->readCallbacks[(int) $stream] = $cb; + } + + /** + * Adds a write stream. + * + * The callback will be called as soon as the system reports it's ready to + * receive writes on the stream. + * + * You MUST call removeWriteStream after you are done with the stream, to + * prevent the eventloop from never stopping. + * + * @param resource $stream + */ + public function addWriteStream($stream, callable $cb) + { + $this->writeStreams[(int) $stream] = $stream; + $this->writeCallbacks[(int) $stream] = $cb; + } + + /** + * Stop watching a stream for reads. + * + * @param resource $stream + */ + public function removeReadStream($stream) + { + unset( + $this->readStreams[(int) $stream], + $this->readCallbacks[(int) $stream] + ); + } + + /** + * Stop watching a stream for writes. + * + * @param resource $stream + */ + public function removeWriteStream($stream) + { + unset( + $this->writeStreams[(int) $stream], + $this->writeCallbacks[(int) $stream] + ); + } + + /** + * Runs the loop. + * + * This function will run continuously, until there's no more events to + * handle. + */ + public function run() + { + $this->running = true; + + do { + $hasEvents = $this->tick(true); + } while ($this->running && $hasEvents); + $this->running = false; + } + + /** + * Executes all pending events. + * + * If $block is turned true, this function will block until any event is + * triggered. + * + * If there are now timeouts, nextTick callbacks or events in the loop at + * all, this function will exit immediately. + * + * This function will return true if there are _any_ events left in the + * loop after the tick. + */ + public function tick(bool $block = false): bool + { + $this->runNextTicks(); + $nextTimeout = $this->runTimers(); + + // Calculating how long runStreams should at most wait. + if (!$block) { + // Don't wait + $streamWait = 0; + } elseif ($this->nextTick) { + // There's a pending 'nextTick'. Don't wait. + $streamWait = 0; + } elseif (is_numeric($nextTimeout)) { + // Wait until the next Timeout should trigger. + $streamWait = $nextTimeout; + } else { + // Wait indefinitely + $streamWait = null; + } + + $this->runStreams($streamWait); + + return $this->readStreams || $this->writeStreams || $this->nextTick || $this->timers; + } + + /** + * Stops a running eventloop. + */ + public function stop() + { + $this->running = false; + } + + /** + * Executes all 'nextTick' callbacks. + * + * return void + */ + protected function runNextTicks() + { + $nextTick = $this->nextTick; + $this->nextTick = []; + + foreach ($nextTick as $cb) { + $cb(); + } + } + + /** + * Runs all pending timers. + * + * After running the timer callbacks, this function returns the number of + * seconds until the next timer should be executed. + * + * If there's no more pending timers, this function returns null. + * + * @return float|null + */ + protected function runTimers() + { + $now = microtime(true); + while (($timer = array_pop($this->timers)) && $timer[0] < $now) { + $timer[1](); + } + // Add the last timer back to the array. + if ($timer) { + $this->timers[] = $timer; + + return max(0, $timer[0] - microtime(true)); + } + } + + /** + * Runs all pending stream events. + * + * If $timeout is 0, it will return immediately. If $timeout is null, it + * will wait indefinitely. + * + * @param float|null $timeout + */ + protected function runStreams($timeout) + { + if ($this->readStreams || $this->writeStreams) { + $read = $this->readStreams; + $write = $this->writeStreams; + $except = null; + // stream_select changes behavior in 8.1 to forbid passing non-null microseconds when the seconds are null. + // Older versions of php don't allow passing null to microseconds. + if (null !== $timeout ? stream_select($read, $write, $except, 0, (int) ($timeout * 1000000)) : stream_select($read, $write, $except, null)) { + // See PHP Bug https://bugs.php.net/bug.php?id=62452 + // Fixed in PHP7 + foreach ($read as $readStream) { + $readCb = $this->readCallbacks[(int) $readStream]; + $readCb(); + } + foreach ($write as $writeStream) { + $writeCb = $this->writeCallbacks[(int) $writeStream]; + $writeCb(); + } + } + } elseif ($this->running && ($this->nextTick || $this->timers)) { + usleep(null !== $timeout ? intval($timeout * 1000000) : 200000); + } + } + + /** + * Is the main loop active. + * + * @var bool + */ + protected $running = false; + + /** + * A list of timers, added by setTimeout. + * + * @var array + */ + protected $timers = []; + + /** + * A list of 'nextTick' callbacks. + * + * @var callable[] + */ + protected $nextTick = []; + + /** + * List of readable streams for stream_select, indexed by stream id. + * + * @var resource[] + */ + protected $readStreams = []; + + /** + * List of writable streams for stream_select, indexed by stream id. + * + * @var resource[] + */ + protected $writeStreams = []; + + /** + * List of read callbacks, indexed by stream id. + * + * @var callable[] + */ + protected $readCallbacks = []; + + /** + * List of write callbacks, indexed by stream id. + * + * @var callable[] + */ + protected $writeCallbacks = []; +} diff --git a/lib/vendor/sabre/event/lib/Loop/functions.php b/lib/vendor/sabre/event/lib/Loop/functions.php new file mode 100644 index 0000000..9412a77 --- /dev/null +++ b/lib/vendor/sabre/event/lib/Loop/functions.php @@ -0,0 +1,143 @@ +setTimeout($cb, $timeout); +} + +/** + * Executes a function every x seconds. + * + * The value this function returns can be used to stop the interval with + * clearInterval. + */ +function setInterval(callable $cb, float $timeout): array +{ + return instance()->setInterval($cb, $timeout); +} + +/** + * Stops a running interval. + */ +function clearInterval(array $intervalId) +{ + instance()->clearInterval($intervalId); +} + +/** + * Runs a function immediately at the next iteration of the loop. + */ +function nextTick(callable $cb) +{ + instance()->nextTick($cb); +} + +/** + * Adds a read stream. + * + * The callback will be called as soon as there is something to read from + * the stream. + * + * You MUST call removeReadStream after you are done with the stream, to + * prevent the eventloop from never stopping. + * + * @param resource $stream + */ +function addReadStream($stream, callable $cb) +{ + instance()->addReadStream($stream, $cb); +} + +/** + * Adds a write stream. + * + * The callback will be called as soon as the system reports it's ready to + * receive writes on the stream. + * + * You MUST call removeWriteStream after you are done with the stream, to + * prevent the eventloop from never stopping. + * + * @param resource $stream + */ +function addWriteStream($stream, callable $cb) +{ + instance()->addWriteStream($stream, $cb); +} + +/** + * Stop watching a stream for reads. + * + * @param resource $stream + */ +function removeReadStream($stream) +{ + instance()->removeReadStream($stream); +} + +/** + * Stop watching a stream for writes. + * + * @param resource $stream + */ +function removeWriteStream($stream) +{ + instance()->removeWriteStream($stream); +} + +/** + * Runs the loop. + * + * This function will run continuously, until there's no more events to + * handle. + */ +function run() +{ + instance()->run(); +} + +/** + * Executes all pending events. + * + * If $block is turned true, this function will block until any event is + * triggered. + * + * If there are now timeouts, nextTick callbacks or events in the loop at + * all, this function will exit immediately. + * + * This function will return true if there are _any_ events left in the + * loop after the tick. + */ +function tick(bool $block = false): bool +{ + return instance()->tick($block); +} + +/** + * Stops a running eventloop. + */ +function stop() +{ + instance()->stop(); +} + +/** + * Retrieves or sets the global Loop object. + */ +function instance(?Loop $newLoop = null): Loop +{ + static $loop; + if ($newLoop) { + $loop = $newLoop; + } elseif (!$loop) { + $loop = new Loop(); + } + + return $loop; +} diff --git a/lib/vendor/sabre/event/lib/Promise.php b/lib/vendor/sabre/event/lib/Promise.php new file mode 100644 index 0000000..66903fb --- /dev/null +++ b/lib/vendor/sabre/event/lib/Promise.php @@ -0,0 +1,253 @@ +fulfill and $this->reject. + * Using the executor is optional. + */ + public function __construct(?callable $executor = null) + { + if ($executor) { + $executor( + [$this, 'fulfill'], + [$this, 'reject'] + ); + } + } + + /** + * This method allows you to specify the callback that will be called after + * the promise has been fulfilled or rejected. + * + * Both arguments are optional. + * + * This method returns a new promise, which can be used for chaining. + * If either the onFulfilled or onRejected callback is called, you may + * return a result from this callback. + * + * If the result of this callback is yet another promise, the result of + * _that_ promise will be used to set the result of the returned promise. + * + * If either of the callbacks return any other value, the returned promise + * is automatically fulfilled with that value. + * + * If either of the callbacks throw an exception, the returned promise will + * be rejected and the exception will be passed back. + */ + public function then(?callable $onFulfilled = null, ?callable $onRejected = null): Promise + { + // This new subPromise will be returned from this function, and will + // be fulfilled with the result of the onFulfilled or onRejected event + // handlers. + $subPromise = new self(); + + switch ($this->state) { + case self::PENDING: + // The operation is pending, so we keep a reference to the + // event handlers so we can call them later. + $this->subscribers[] = [$subPromise, $onFulfilled, $onRejected]; + break; + case self::FULFILLED: + // The async operation is already fulfilled, so we trigger the + // onFulfilled callback asap. + $this->invokeCallback($subPromise, $onFulfilled); + break; + case self::REJECTED: + // The async operation failed, so we call the onRejected + // callback asap. + $this->invokeCallback($subPromise, $onRejected); + break; + } + + return $subPromise; + } + + /** + * Add a callback for when this promise is rejected. + * + * Its usage is identical to then(). However, the otherwise() function is + * preferred. + */ + public function otherwise(callable $onRejected): Promise + { + return $this->then(null, $onRejected); + } + + /** + * Marks this promise as fulfilled and sets its return value. + */ + public function fulfill($value = null) + { + if (self::PENDING !== $this->state) { + throw new PromiseAlreadyResolvedException('This promise is already resolved, and you\'re not allowed to resolve a promise more than once'); + } + $this->state = self::FULFILLED; + $this->value = $value; + foreach ($this->subscribers as $subscriber) { + $this->invokeCallback($subscriber[0], $subscriber[1]); + } + } + + /** + * Marks this promise as rejected, and set its rejection reason. + */ + public function reject(\Throwable $reason) + { + if (self::PENDING !== $this->state) { + throw new PromiseAlreadyResolvedException('This promise is already resolved, and you\'re not allowed to resolve a promise more than once'); + } + $this->state = self::REJECTED; + $this->value = $reason; + foreach ($this->subscribers as $subscriber) { + $this->invokeCallback($subscriber[0], $subscriber[2]); + } + } + + /** + * Stops execution until this promise is resolved. + * + * This method stops execution completely. If the promise is successful with + * a value, this method will return this value. If the promise was + * rejected, this method will throw an exception. + * + * This effectively turns the asynchronous operation into a synchronous + * one. In PHP it might be useful to call this on the last promise in a + * chain. + * + * @psalm-return TReturn + */ + public function wait() + { + $hasEvents = true; + while (self::PENDING === $this->state) { + if (!$hasEvents) { + throw new \LogicException('There were no more events in the loop. This promise will never be fulfilled.'); + } + + // As long as the promise is not fulfilled, we tell the event loop + // to handle events, and to block. + $hasEvents = Loop\tick(true); + } + + if (self::FULFILLED === $this->state) { + // If the state of this promise is fulfilled, we can return the value. + return $this->value; + } else { + // If we got here, it means that the asynchronous operation + // errored. Therefore we need to throw an exception. + throw $this->value; + } + } + + /** + * A list of subscribers. Subscribers are the callbacks that want us to let + * them know if the callback was fulfilled or rejected. + * + * @var array + */ + protected $subscribers = []; + + /** + * The result of the promise. + * + * If the promise was fulfilled, this will be the result value. If the + * promise was rejected, this property hold the rejection reason. + */ + protected $value; + + /** + * This method is used to call either an onFulfilled or onRejected callback. + * + * This method makes sure that the result of these callbacks are handled + * correctly, and any chained promises are also correctly fulfilled or + * rejected. + */ + private function invokeCallback(Promise $subPromise, ?callable $callBack = null) + { + // We use 'nextTick' to ensure that the event handlers are always + // triggered outside of the calling stack in which they were originally + // passed to 'then'. + // + // This makes the order of execution more predictable. + Loop\nextTick(function () use ($callBack, $subPromise) { + if (is_callable($callBack)) { + try { + $result = $callBack($this->value); + if ($result instanceof self) { + // If the callback (onRejected or onFulfilled) + // returned a promise, we only fulfill or reject the + // chained promise once that promise has also been + // resolved. + $result->then([$subPromise, 'fulfill'], [$subPromise, 'reject']); + } else { + // If the callback returned any other value, we + // immediately fulfill the chained promise. + $subPromise->fulfill($result); + } + } catch (\Throwable $e) { + // If the event handler threw an exception, we need to make sure that + // the chained promise is rejected as well. + $subPromise->reject($e); + } + } else { + if (self::FULFILLED === $this->state) { + $subPromise->fulfill($this->value); + } else { + $subPromise->reject($this->value); + } + } + }); + } +} diff --git a/lib/vendor/sabre/event/lib/Promise/functions.php b/lib/vendor/sabre/event/lib/Promise/functions.php new file mode 100644 index 0000000..67e80cb --- /dev/null +++ b/lib/vendor/sabre/event/lib/Promise/functions.php @@ -0,0 +1,125 @@ + $subPromise) { + $subPromise->then( + function ($result) use ($promiseIndex, &$completeResult, &$successCount, $success, $promises) { + $completeResult[$promiseIndex] = $result; + ++$successCount; + if ($successCount === count($promises)) { + $success($completeResult); + } + + return $result; + } + )->otherwise( + function ($reason) use ($fail) { + $fail($reason); + } + ); + } + }); +} + +/** + * The race function returns a promise that resolves or rejects as soon as + * one of the promises in the argument resolves or rejects. + * + * The returned promise will resolve or reject with the value or reason of + * that first promise. + * + * @param Promise[] $promises + */ +function race(array $promises): Promise +{ + return new Promise(function ($success, $fail) use ($promises) { + $alreadyDone = false; + foreach ($promises as $promise) { + $promise->then( + function ($result) use ($success, &$alreadyDone) { + if ($alreadyDone) { + return; + } + $alreadyDone = true; + $success($result); + }, + function ($reason) use ($fail, &$alreadyDone) { + if ($alreadyDone) { + return; + } + $alreadyDone = true; + $fail($reason); + } + ); + } + }); +} + +/** + * Returns a Promise that resolves with the given value. + * + * If the value is a promise, the returned promise will attach itself to that + * promise and eventually get the same state as the followed promise. + */ +function resolve($value): Promise +{ + if ($value instanceof Promise) { + return $value->then(); + } else { + $promise = new Promise(); + $promise->fulfill($value); + + return $promise; + } +} + +/** + * Returns a Promise that will reject with the given reason. + */ +function reject(\Throwable $reason): Promise +{ + $promise = new Promise(); + $promise->reject($reason); + + return $promise; +} diff --git a/lib/vendor/sabre/event/lib/PromiseAlreadyResolvedException.php b/lib/vendor/sabre/event/lib/PromiseAlreadyResolvedException.php new file mode 100644 index 0000000..abb6c10 --- /dev/null +++ b/lib/vendor/sabre/event/lib/PromiseAlreadyResolvedException.php @@ -0,0 +1,17 @@ +wildcardListeners; + } else { + $listeners = &$this->listeners; + } + + // Always fully reset the listener index. This is fairly sane for most + // applications, because there's a clear "event registering" and "event + // emitting" phase, but can be slow if there's a lot adding and removing + // of listeners during emitting of events. + $this->listenerIndex = []; + + if (!isset($listeners[$eventName])) { + $listeners[$eventName] = []; + } + $listeners[$eventName][] = [$priority, $callBack]; + } + + /** + * Subscribe to an event exactly once. + */ + public function once(string $eventName, callable $callBack, int $priority = 100) + { + $wrapper = null; + $wrapper = function () use ($eventName, $callBack, &$wrapper) { + $this->removeListener($eventName, $wrapper); + + return \call_user_func_array($callBack, \func_get_args()); + }; + + $this->on($eventName, $wrapper, $priority); + } + + /** + * Emits an event. + * + * This method will return true if 0 or more listeners were successfully + * handled. false is returned if one of the events broke the event chain. + * + * If the continueCallBack is specified, this callback will be called every + * time before the next event handler is called. + * + * If the continueCallback returns false, event propagation stops. This + * allows you to use the eventEmitter as a means for listeners to implement + * functionality in your application, and break the event loop as soon as + * some condition is fulfilled. + * + * Note that returning false from an event subscriber breaks propagation + * and returns false, but if the continue-callback stops propagation, this + * is still considered a 'successful' operation and returns true. + * + * Lastly, if there are 5 event handlers for an event. The continueCallback + * will be called at most 4 times. + */ + public function emit(string $eventName, array $arguments = [], ?callable $continueCallBack = null): bool + { + if (\is_null($continueCallBack)) { + foreach ($this->listeners($eventName) as $listener) { + $result = \call_user_func_array($listener, $arguments); + if (false === $result) { + return false; + } + } + } else { + $listeners = $this->listeners($eventName); + $counter = \count($listeners); + + foreach ($listeners as $listener) { + --$counter; + $result = \call_user_func_array($listener, $arguments); + if (false === $result) { + return false; + } + + if ($counter > 0) { + if (!$continueCallBack()) { + break; + } + } + } + } + + return true; + } + + /** + * Returns the list of listeners for an event. + * + * The list is returned as an array, and the list of events are sorted by + * their priority. + * + * @return callable[] + */ + public function listeners(string $eventName): array + { + if (!\array_key_exists($eventName, $this->listenerIndex)) { + // Create a new index. + $listeners = []; + $listenersPriority = []; + if (isset($this->listeners[$eventName])) { + foreach ($this->listeners[$eventName] as $listener) { + $listenersPriority[] = $listener[0]; + $listeners[] = $listener[1]; + } + } + + foreach ($this->wildcardListeners as $wcEvent => $wcListeners) { + // Wildcard match + if (\substr($eventName, 0, \strlen($wcEvent)) === $wcEvent) { + foreach ($wcListeners as $listener) { + $listenersPriority[] = $listener[0]; + $listeners[] = $listener[1]; + } + } + } + + // Sorting by priority + \array_multisort($listenersPriority, SORT_NUMERIC, $listeners); + + // Creating index + $this->listenerIndex[$eventName] = $listeners; + } + + return $this->listenerIndex[$eventName]; + } + + /** + * Removes a specific listener from an event. + * + * If the listener could not be found, this method will return false. If it + * was removed it will return true. + */ + public function removeListener(string $eventName, callable $listener): bool + { + // If it ends with a wildcard, we use the wildcardListeners array + if ('*' === $eventName[\strlen($eventName) - 1]) { + $eventName = \substr($eventName, 0, -1); + $listeners = &$this->wildcardListeners; + } else { + $listeners = &$this->listeners; + } + + if (!isset($listeners[$eventName])) { + return false; + } + + foreach ($listeners[$eventName] as $index => $check) { + if ($check[1] === $listener) { + // Remove listener + unset($listeners[$eventName][$index]); + // Reset index + $this->listenerIndex = []; + + return true; + } + } + + return false; + } + + /** + * Removes all listeners. + * + * If the eventName argument is specified, all listeners for that event are + * removed. If it is not specified, every listener for every event is + * removed. + */ + public function removeAllListeners(?string $eventName = null) + { + if (\is_null($eventName)) { + $this->listeners = []; + $this->wildcardListeners = []; + } else { + if ('*' === $eventName[\strlen($eventName) - 1]) { + // Wildcard event + unset($this->wildcardListeners[\substr($eventName, 0, -1)]); + } else { + unset($this->listeners[$eventName]); + } + } + + // Reset index + $this->listenerIndex = []; + } + + /** + * The list of listeners. + */ + protected $listeners = []; + + /** + * The list of "wildcard listeners". + */ + protected $wildcardListeners = []; + + /** + * An index of listeners for a specific event name. This helps speeding + * up emitting events after all listeners have been set. + * + * If the list of listeners changes though, the index clears. + */ + protected $listenerIndex = []; +} diff --git a/lib/vendor/sabre/event/lib/coroutine.php b/lib/vendor/sabre/event/lib/coroutine.php new file mode 100644 index 0000000..f664efa --- /dev/null +++ b/lib/vendor/sabre/event/lib/coroutine.php @@ -0,0 +1,122 @@ +request('GET', '/foo'); + * $promise->then(function($value) { + * + * return $httpClient->request('DELETE','/foo'); + * + * })->then(function($value) { + * + * return $httpClient->request('PUT', '/foo'); + * + * })->error(function($reason) { + * + * echo "Failed because: $reason\n"; + * + * }); + * + * Example with coroutines: + * + * coroutine(function() { + * + * try { + * yield $httpClient->request('GET', '/foo'); + * yield $httpClient->request('DELETE', /foo'); + * yield $httpClient->request('PUT', '/foo'); + * } catch(\Throwable $reason) { + * echo "Failed because: $reason\n"; + * } + * + * }); + * + * @psalm-template TReturn + * + * @psalm-param callable():\Generator $gen + * + * @psalm-return Promise + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +function coroutine(callable $gen): Promise +{ + $generator = $gen(); + if (!$generator instanceof \Generator) { + throw new \InvalidArgumentException('You must pass a generator function'); + } + + // This is the value we're returning. + $promise = new Promise(); + + /** + * So tempted to use the mythical y-combinator here, but it's not needed in + * PHP. + */ + $advanceGenerator = function () use (&$advanceGenerator, $generator, $promise) { + while ($generator->valid()) { + $yieldedValue = $generator->current(); + if ($yieldedValue instanceof Promise) { + $yieldedValue->then( + function ($value) use ($generator, &$advanceGenerator) { + $generator->send($value); + $advanceGenerator(); + }, + function (\Throwable $reason) use ($generator, $advanceGenerator) { + $generator->throw($reason); + $advanceGenerator(); + } + )->otherwise(function (\Throwable $reason) use ($promise) { + // This error handler would be called, if something in the + // generator throws an exception, and it's not caught + // locally. + $promise->reject($reason); + }); + // We need to break out of the loop, because $advanceGenerator + // will be called asynchronously when the promise has a result. + break; + } else { + // If the value was not a promise, we'll just let it pass through. + $generator->send($yieldedValue); + } + } + + // If the generator is at the end, and we didn't run into an exception, + // We're grabbing the "return" value and fulfilling our top-level + // promise with its value. + if (!$generator->valid() && Promise::PENDING === $promise->state) { + $returnValue = $generator->getReturn(); + + // The return value is a promise. + if ($returnValue instanceof Promise) { + $returnValue->then(function ($value) use ($promise) { + $promise->fulfill($value); + }, function (\Throwable $reason) use ($promise) { + $promise->reject($reason); + }); + } else { + $promise->fulfill($returnValue); + } + } + }; + + try { + $advanceGenerator(); + } catch (\Throwable $e) { + $promise->reject($e); + } + + return $promise; +} diff --git a/lib/vendor/sabre/http/.github/workflows/ci.yml b/lib/vendor/sabre/http/.github/workflows/ci.yml new file mode 100644 index 0000000..1ec92b8 --- /dev/null +++ b/lib/vendor/sabre/http/.github/workflows/ci.yml @@ -0,0 +1,68 @@ +name: continuous-integration +on: + push: + branches: + - master + - release/* + pull_request: +jobs: + unit-testing: + name: PHPUnit (PHP ${{ matrix.php-versions }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php-versions: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3'] + coverage: ['xdebug'] + code-style: ['yes'] + code-analysis: ['no'] + include: + - php-versions: '7.1' + code-style: 'yes' + code-analysis: 'yes' + - php-versions: '8.4' + code-style: 'yes' + code-analysis: 'yes' + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup PHP, with composer and extensions + uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php + with: + php-version: ${{ matrix.php-versions }} + extensions: ctype, curl, mbstring, xdebug + coverage: ${{ matrix.coverage }} + tools: composer + + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install composer dependencies + run: composer install --no-progress --prefer-dist --optimize-autoloader + + - name: Code Analysis (PHP CS-Fixer) + if: matrix.code-style == 'yes' + run: PHP_CS_FIXER_IGNORE_ENV=true php vendor/bin/php-cs-fixer fix --dry-run --diff + + - name: Code Analysis (PHPStan) + if: matrix.code-analysis == 'yes' + run: composer phpstan + + - name: Run application server + run: php -S localhost:8000 -t tests/www 2>/dev/null & + + - name: Test with phpunit + run: vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-clover clover.xml + + - name: Code Coverage + uses: codecov/codecov-action@v5 + if: matrix.coverage != 'none' diff --git a/lib/vendor/sabre/http/.gitignore b/lib/vendor/sabre/http/.gitignore new file mode 100644 index 0000000..a5356dd --- /dev/null +++ b/lib/vendor/sabre/http/.gitignore @@ -0,0 +1,9 @@ +# Composer +vendor/ +composer.lock + +# Tests +tests/cov/ +.phpunit.result.cache +.php_cs.cache +.php-cs-fixer.cache diff --git a/lib/vendor/sabre/http/.php-cs-fixer.dist.php b/lib/vendor/sabre/http/.php-cs-fixer.dist.php new file mode 100644 index 0000000..f9d4b7a --- /dev/null +++ b/lib/vendor/sabre/http/.php-cs-fixer.dist.php @@ -0,0 +1,17 @@ +exclude('vendor') + ->in(__DIR__); + +$config = new PhpCsFixer\Config(); +$config->setRules([ + '@PSR1' => true, + '@Symfony' => true, + 'nullable_type_declaration' => [ + 'syntax' => 'question_mark', + ], + 'nullable_type_declaration_for_default_null_value' => true, +]); +$config->setFinder($finder); +return $config; \ No newline at end of file diff --git a/lib/vendor/sabre/http/.php_cs.dist b/lib/vendor/sabre/http/.php_cs.dist new file mode 100644 index 0000000..c5c78a9 --- /dev/null +++ b/lib/vendor/sabre/http/.php_cs.dist @@ -0,0 +1,12 @@ +getFinder() + ->exclude('vendor') + ->in(__DIR__); +$config->setRules([ + '@PSR1' => true, + '@Symfony' => true +]); + +return $config; \ No newline at end of file diff --git a/lib/vendor/sabre/http/CHANGELOG.md b/lib/vendor/sabre/http/CHANGELOG.md new file mode 100644 index 0000000..6a2458c --- /dev/null +++ b/lib/vendor/sabre/http/CHANGELOG.md @@ -0,0 +1,403 @@ +ChangeLog +========= + +5.1.13 (2025-09-09) +------------------ + +* #262 properly compute absolute URL (@skjnldsv) + +5.1.12 (2024-08-27) +------------------ + +* #243 add cs-fixer v3 (@phil-davis) + +5.1.11 (2024-07-26) +------------------ + +* #241 PHP 8.4 compliance (@phil-davis) + +5.1.10 (2023-08-18) +------------------ + +* #225 Enhance tests/bootstrap.php to find autoloader in more environments (@phil-davis) + +5.1.9 (2023-08-17) +------------------ + +* #223 skip testParseMimeTypeOnInvalidMimeType (@phil-davis) + +5.1.8 (2023-08-17) +------------------ + +* #215 Improve CURLOPT_HTTPHEADER Setting Assignment (@amrita-shrestha) + +5.1.7 (2023-06-26) +------------------ + +* #98 and #176 Add more tests (@peter279k) +* #207 fix: handle client disconnect properly with ignore_user_abort true (@kesselb) + +5.1.6 (2022-07-15) +------------------ + +* #187 Allow testSendToGetLargeContent peak memory usage to be specified externally (@phil-davis) +* #188 Fix various small typos and grammar (@phil-davis) +* #189 Fix typo in text of status code 203 'Non-Authoritative Information' (@phil-davis) + +5.1.5 (2022-07-09) +------------------ + +* #184 Remove 4GB file size workaround for 32bit OS / Stream Videos on IOS (@schoetju) + +5.1.4 (2022-06-24) +------------------ + +* #182 Fix encoding detection on PHP 8.1 (@come-nc) + +5.1.3 (2021-11-04) +------------------ + +* #179 version bump that was missed in 5.1.2 (@phil-davis) + +5.1.2 (2021-11-04) +------------------------- + +* #169 Ensure $_SERVER keys are read as strings (@fredrik-eriksson) +* #170 Fix deprecated usages on PHP 8.1 (@cedric-anne) +* #175 Add resource size to CURL options in client (from #172 ) (@Dartui) + +5.1.1 (2020-10-03) +------------------------- + +* #160: Added support for PHP 8.0 (@phil-davis) + +5.1.0 (2020-01-31) +------------------------- + +* Added support for PHP 7.4, dropped support for PHP 7.0 (@phil-davis) +* Updated testsuite for phpunit8, added phpstan coverage (@phil-davis) +* Added autoload-dev for test classes (@C0pyR1ght) + +5.0.5 (2019-11-28) +------------------------- + +* #138: Fixed possible infinite loop (@dpakach, @vfreex, @phil-davis) +* #136: Improvement regex content-range (@ho4ho) + +5.0.4 (2019-10-08) +------------------------- + +* #133: Fix short Content-Range download - Regression from 5.0.3 (@phil-davis) + +5.0.3 (2019-10-08) +------------------------- + +* #119: Significantly improve file download speed by enabling mmap based stream_copy_to_stream (@vfreex) + +5.0.2 (2019-09-12) +------------------------- + +* #125: Fix Strict Error if Response Body Empty (@WorksDev, @phil-davis) + +5.0.1 (2019-09-11) +------------------------- + +* #121: fix "Trying to access array offset on value of type bool" in 7.4 (@remicollet) +* #115: Reduce memory footprint when parsing HTTP result (@Gasol) +* #114: Misc code improvements (@mrcnpdlk) +* #111, #118: Added phpstan analysis (@DeepDiver1975, @staabm) +* #107: Tested with php 7.3 (@DeepDiver1975) + + +5.0.0 (2018-06-04) +------------------------- + +* #99: Previous CURL opts are not persisted anymore (@christiaan) +* Final release + +5.0.0-alpha1 (2018-02-16) +------------------------- + +* Now requires PHP 7.0+. +* Supports sabre/event 4.x and 5.x +* Depends on sabre/uri 2. +* hhvm is no longer supported starting this release. +* #65: It's now possible to supply request/response bodies using a callback + functions. This allows very high-speed/low-memory responses to be created. + (@petrkotek). +* Strict typing is used everywhere this is applicable. +* Removed `URLUtil` class. It was deprecated a long time ago, and most of + its functions moved to the `sabre/uri` package. +* Removed `Util` class. Most of its functions moved to the `functions.php` + file. +* #68: The `$method` and `$uri` arguments when constructing a Request object + are now required. +* When `Sapi::getRequest()` is called, we default to setting the HTTP Method + to `CLI`. +* The HTTP response is now initialized with HTTP code `500` instead of `null`, + so if it's not changed, it will be emitted as 500. +* #69: Sending `charset="UTF-8"` on Basic authentication challenges per + [rfc7617][rfc7617]. +* #84: Added support for `SERVER_PROTOCOL HTTP/2.0` (@jens1o) + + +4.2.3 (2017-06-12) +------------------ + +* #74, #77: Work around 4GB file size limit at 32-Bit systems + + +4.2.2 (2017-01-02) +------------------ + +* #72: Handling clients that send invalid `Content-Length` headers. + + +4.2.1 (2016-01-06) +------------------ + +* #56: `getBodyAsString` now returns at most as many bytes as the contents of + the `Content-Length` header. This allows users to pass much larger strings + without having to copy and truncate them. +* The client now sets a default `User-Agent` header identifying this library. + + +4.2.0 (2016-01-04) +------------------ + +* This package now supports sabre/event 3.0. + + +4.1.0 (2015-09-04) +------------------ + +* The async client wouldn't `wait()` for new http requests being started + after the (previous) last request in the queue was resolved. +* Added `Sabre\HTTP\Auth\Bearer`, to easily extract a OAuth2 bearer token. + + +4.0.0 (2015-05-20) +------------------ + +* Deprecated: All static functions from `Sabre\HTTP\URLUtil` and + `Sabre\HTTP\Util` moved to a separate `functions.php`, which is also + autoloaded. The old functions are still there, but will be removed in a + future version. (#49) + + +4.0.0-alpha3 (2015-05-19) +------------------------- + +* Added a parser for the HTTP `Prefer` header, as defined in [RFC7240][rfc7240]. +* Deprecated `Sabre\HTTP\Util::parseHTTPDate`, use `Sabre\HTTP\parseDate()`. +* Deprecated `Sabre\HTTP\Util::toHTTPDate` use `Sabre\HTTP\toDate()`. + + +4.0.0-alpha2 (2015-05-18) +------------------------- + +* #45: Don't send more data than what is promised in the HTTP content-length. + (@dratini0). +* #43: `getCredentials` returns null if incomplete. (@Hywan) +* #48: Now using php-cs-fixer to make our CS consistent (yay!) +* This includes fixes released in version 3.0.5. + + +4.0.0-alpha1 (2015-02-25) +------------------------- + +* #41: Fixing bugs related to comparing URLs in `Request::getPath()`. +* #41: This library now uses the `sabre/uri` package for uri handling. +* Added `421 Misdirected Request` from the HTTP/2.0 spec. + + +3.0.5 (2015-05-11) +------------------ + +* #47 #35: When re-using the client and doing any request after a `HEAD` + request, the client discards the body. + + +3.0.4 (2014-12-10) +------------------ + +* #38: The Authentication helpers no longer overwrite any existing + `WWW-Authenticate` headers, but instead append new headers. This ensures + that multiple authentication systems can exist in the same environment. + + +3.0.3 (2014-12-03) +------------------ + +* Hiding `Authorization` header value from `Request::__toString`. + + +3.0.2 (2014-10-09) +------------------ + +* When parsing `Accept:` headers, we're ignoring invalid parts. Before we + would throw a PHP E_NOTICE. + + +3.0.1 (2014-09-29) +------------------ + +* Minor change in unittests. + + +3.0.0 (2014-09-23) +------------------ + +* `getHeaders()` now returns header values as an array, just like psr/http. +* Added `hasHeader()`. + + +2.1.0-alpha1 (2014-09-15) +------------------------- + +* Changed: Copied most of the header-semantics for the PSR draft for + representing HTTP messages. [Reference here][psr-http]. +* This means that `setHeaders()` does not wipe out every existing header + anymore. +* We also support multiple headers with the same name. +* Use `Request::getHeaderAsArray()` and `Response::getHeaderAsArray()` to + get a hold off multiple headers with the same name. +* If you use `getHeader()`, and there's more than 1 header with that name, we + concatenate all these with a comma. +* `addHeader()` will now preserve an existing header with that name, and add a + second header with the same name. +* The message class should be a lot faster now for looking up headers. No more + array traversal, because we maintain a tiny index. +* Added: `URLUtil::resolve()` to make resolving relative urls super easy. +* Switched to PSR-4. +* #12: Circumventing CURL's FOLLOW_LOCATION and doing it in PHP instead. This + fixes compatibility issues with people that have open_basedir turned on. +* Added: Content negotiation now correctly support mime-type parameters such as + charset. +* Changed: `Util::negotiate()` is now deprecated. Use + `Util::negotiateContentType()` instead. +* #14: The client now only follows http and https urls. + + +2.0.4 (2014-07-14) +------------------ + +* Changed: No longer escaping @ in urls when it's not needed. +* Fixed: #7: Client now correctly deals with responses without a body. + + +2.0.3 (2014-04-17) +------------------ + +* Now works on hhvm! +* Fixed: Now throwing an error when a Request object is being created with + arguments that were valid for sabre/http 1.0. Hopefully this will aid with + debugging for upgraders. + + +2.0.2 (2014-02-09) +------------------ + +* Fixed: Potential security problem in the client. + + +2.0.1 (2014-01-09) +------------------ + +* Fixed: getBodyAsString on an empty body now works. +* Fixed: Version string + + +2.0.0 (2014-01-08) +------------------ + +* Removed: Request::createFromPHPRequest. This is now handled by + Sapi::getRequest. + + +2.0.0alpha6 (2014-01-03) +------------------------ + +* Added: Asynchronous HTTP client. See examples/asyncclient.php. +* Fixed: Issue #4: Don't escape colon (:) when it's not needed. +* Fixed: Fixed a bug in the content negotation script. +* Fixed: Fallback for when CURLOPT_POSTREDIR is not defined (mainly for hhvm). +* Added: The Request and Response object now have a `__toString()` method that + serializes the objects into a standard HTTP message. This is mainly for + debugging purposes. +* Changed: Added Response::getStatusText(). This method returns the + human-readable HTTP status message. This part has been removed from + Response::getStatus(), which now always returns just the status code as an + int. +* Changed: Response::send() is now Sapi::sendResponse($response). +* Changed: Request::createFromPHPRequest is now Sapi::getRequest(). +* Changed: Message::getBodyAsStream and Message::getBodyAsString were added. The + existing Message::getBody changed its behavior, so be careful. + + +2.0.0alpha5 (2013-11-07) +------------------------ + +* Added: HTTP Status 451 Unavailable For Legal Reasons. Fight government + censorship! +* Added: Ability to catch and retry http requests in the client when a curl + error occurs. +* Changed: Request::getPath does not return the query part of the url, so + everything after the ? is stripped. +* Added: a reverse proxy example. + + +2.0.0alpha4 (2013-08-07) +------------------------ + +* Fixed: Doing a GET request with the client uses the last used HTTP method + instead. +* Added: HttpException +* Added: The Client class can now automatically emit exceptions when HTTP errors + occurred. + + +2.0.0alpha3 (2013-07-24) +------------------------ + +* Changed: Now depends on sabre/event package. +* Changed: setHeaders() now overwrites any existing http headers. +* Added: getQueryParameters to RequestInterface. +* Added: Util::negotiate. +* Added: RequestDecorator, ResponseDecorator. +* Added: A very simple HTTP client. +* Added: addHeaders() to append a list of new headers. +* Fixed: Not erroring on unknown HTTP status codes. +* Fixed: Throwing exceptions on invalid HTTP status codes (not 3 digits). +* Fixed: Much better README.md +* Changed: getBody() now uses a bitfield to specify what type to return. + + +2.0.0alpha2 (2013-07-02) +------------------------ + +* Added: Digest & AWS Authentication. +* Added: Message::getHttpVersion and Message::setHttpVersion. +* Added: Request::setRawServerArray, getRawServerValue. +* Added: Request::createFromPHPRequest +* Added: Response::send +* Added: Request::getQueryParameters +* Added: Utility for dealing with HTTP dates. +* Added: Request::setPostData and Request::getPostData. +* Added: Request::setAbsoluteUrl and Request::getAbsoluteUrl. +* Added: URLUtil, methods for calculation relative and base urls. +* Removed: Response::sendBody + + +2.0.0alpha1 (2012-10-07) +------------------------ + +* Fixed: Lots of small naming improvements +* Added: Introduction of Message, MessageInterface, Response, ResponseInterface. + +Before 2.0.0, this package was built-into SabreDAV, where it first appeared in +January 2009. + +[psr-http]: https://github.com/php-fig/fig-standards/blob/master/proposed/http-message.md +[rfc7240]: http://tools.ietf.org/html/rfc7240 +[rfc7617]: https://tools.ietf.org/html/rfc7617 diff --git a/lib/vendor/sabre/http/LICENSE b/lib/vendor/sabre/http/LICENSE new file mode 100644 index 0000000..864041b --- /dev/null +++ b/lib/vendor/sabre/http/LICENSE @@ -0,0 +1,27 @@ +Copyright (C) 2009-2017 fruux GmbH (https://fruux.com/) + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name Sabre nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/vendor/sabre/http/README.md b/lib/vendor/sabre/http/README.md new file mode 100644 index 0000000..2f01c14 --- /dev/null +++ b/lib/vendor/sabre/http/README.md @@ -0,0 +1,747 @@ +sabre/http +========== + +This library provides a toolkit to make working with the [HTTP protocol](https://tools.ietf.org/html/rfc2616) easier. + +Most PHP scripts run within a HTTP request but accessing information about the +HTTP request is cumbersome at least. + +There's bad practices, inconsistencies and confusion. This library is +effectively a wrapper around the following PHP constructs: + +For Input: + +* `$_GET`, +* `$_POST`, +* `$_SERVER`, +* `php://input` or `$HTTP_RAW_POST_DATA`. + +For output: + +* `php://output` or `echo`, +* `header()`. + +What this library provides, is a `Request` object, and a `Response` object. + +The objects are extendable and easily mockable. + +Build status +------------ + +| branch | status | +|--------|---------------------------------------------------------------------------------------------------------------| +| master | [![Build Status](https://travis-ci.org/sabre-io/http.svg?branch=master)](https://travis-ci.org/sabre-io/http) | +| 4.2 | [![Build Status](https://travis-ci.org/sabre-io/http.svg?branch=4.2)](https://travis-ci.org/sabre-io/http) | +| 3.0 | [![Build Status](https://travis-ci.org/sabre-io/http.svg?branch=3.0)](https://travis-ci.org/sabre-io/http) | + +Installation +------------ + +Make sure you have [composer][1] installed. In your project directory, create, +or edit a `composer.json` file, and make sure it contains something like this: + +```json +{ + "require" : { + "sabre/http" : "~5.0.0" + } +} +``` + +After that, just hit `composer install` and you should be rolling. + +Quick history +------------- + +This library came to existence in 2009, as a part of the [`sabre/dav`][2] +project, which uses it heavily. + +It got split off into a separate library to make it easier to manage +releases and hopefully giving it use outside of the scope of just `sabre/dav`. + +Although completely independently developed, this library has a LOT of +overlap with [Symfony's `HttpFoundation`][3]. + +Said library does a lot more stuff and is significantly more popular, +so if you are looking for something to fulfill this particular requirement, +I'd recommend also considering [`HttpFoundation`][3]. + + +Getting started +--------------- + +First and foremost, this library wraps the superglobals. The easiest way to +instantiate a request object is as follows: + +```php +use Sabre\HTTP; + +include 'vendor/autoload.php'; + +$request = HTTP\Sapi::getRequest(); +``` + +This line should only happen once in your entire application. Everywhere else +you should pass this request object around using dependency injection. + +You should always typehint on its interface: + +```php +function handleRequest(HTTP\RequestInterface $request) { + + // Do something with this request :) + +} +``` + +A response object you can just create as such: + +```php +use Sabre\HTTP; + +include 'vendor/autoload.php'; + +$response = new HTTP\Response(); +$response->setStatus(201); // created ! +$response->setHeader('X-Foo', 'bar'); +$response->setBody( + 'success!' +); + +``` + +After you fully constructed your response, you must call: + +```php +HTTP\Sapi::sendResponse($response); +``` + +This line should generally also appear once in your application (at the very +end). + +Decorators +---------- + +It may be useful to extend the `Request` and `Response` objects in your +application, if you for example would like them to carry a bit more +information about the current request. + +For instance, you may want to add an `isLoggedIn` method to the Request +object. + +Simply extending Request and Response may pose some problems: + +1. You may want to extend the objects with new behaviors differently, in + different subsystems of your application, +2. The `Sapi::getRequest` factory always returns an instance of + `Request` so you would have to override the factory method as well, +3. By controlling the instantiation and depend on specific `Request` and + `Response` instances in your library or application, you make it harder to + work with other applications which also use `sabre/http`. + +In short: it would be bad design. Instead, it's recommended to use the +[decorator pattern][6] to add new behavior where you need it. `sabre/http` +provides helper classes to quickly do this. + +Example: + +```php +use Sabre\HTTP; + +class MyRequest extends HTTP\RequestDecorator { + + function isLoggedIn() { + + return true; + + } + +} +``` + +Our application assumes that the true `Request` object was instantiated +somewhere else, by some other subsystem. This could simply be a call like +`$request = Sapi::getRequest()` at the top of your application, +but could also be somewhere in a unit test. + +All we know in the current subsystem, is that we received a `$request` and +that it implements `Sabre\HTTP\RequestInterface`. To decorate this object, +all we need to do is: + +```php +$request = new MyRequest($request); +``` + +And that's it, we now have an `isLoggedIn` method, without having to mess +with the core instances. + + +Client +------ + +This package also contains a simple wrapper around [cURL][4], which will allow +you to write simple clients, using the `Request` and `Response` objects you're +already familiar with. + +It's by no means a replacement for something like [Guzzle][7], but it provides +a simple and lightweight API for making the occasional API call. + +### Usage + +```php +use Sabre\HTTP; + +$request = new HTTP\Request('GET', 'http://example.org/'); +$request->setHeader('X-Foo', 'Bar'); + +$client = new HTTP\Client(); +$response = $client->send($request); + +echo $response->getBodyAsString(); +``` + +The client emits 3 event using [`sabre/event`][5]. `beforeRequest`, +`afterRequest` and `error`. + +```php +$client = new HTTP\Client(); +$client->on('beforeRequest', function($request) { + + // You could use beforeRequest to for example inject a few extra headers. + // into the Request object. + +}); + +$client->on('afterRequest', function($request, $response) { + + // The afterRequest event could be a good time to do some logging, or + // do some rewriting in the response. + +}); + +$client->on('error', function($request, $response, &$retry, $retryCount) { + + // The error event is triggered for every response with a HTTP code higher + // than 399. + +}); + +$client->on('error:401', function($request, $response, &$retry, $retryCount) { + + // You can also listen for specific error codes. This example shows how + // to inject HTTP authentication headers if a 401 was returned. + + if ($retryCount > 1) { + // We're only going to retry exactly once. + } + + $request->setHeader('Authorization', 'Basic xxxxxxxxxx'); + $retry = true; + +}); +``` + +### Asynchronous requests + +The `Client` also supports doing asynchronous requests. This is especially handy +if you need to perform a number of requests, that are allowed to be executed +in parallel. + +The underlying system for this is simply [cURL's multi request handler][8], +but this provides a much nicer API to handle this. + +Sample usage: + +```php + +use Sabre\HTTP; + +$request = new Request('GET', 'http://localhost/'); +$client = new Client(); + +// Executing 1000 requests +for ($i = 0; $i < 1000; $i++) { + $client->sendAsync( + $request, + function(ResponseInterface $response) { + // Success handler + }, + function($error) { + // Error handler + } + ); +} + +// Wait for all requests to get a result. +$client->wait(); + +``` + +Check out `examples/asyncclient.php` for more information. + +Writing a reverse proxy +----------------------- + +With all these tools combined, it becomes very easy to write a simple reverse +http proxy. + +```php +use + Sabre\HTTP\Sapi, + Sabre\HTTP\Client; + +// The url we're proxying to. +$remoteUrl = 'http://example.org/'; + +// The url we're proxying from. Please note that this must be a relative url, +// and basically acts as the base url. +// +// If your $remoteUrl doesn't end with a slash, this one probably shouldn't +// either. +$myBaseUrl = '/reverseproxy.php'; +// $myBaseUrl = '/~evert/sabre/http/examples/reverseproxy.php/'; + +$request = Sapi::getRequest(); +$request->setBaseUrl($myBaseUrl); + +$subRequest = clone $request; + +// Removing the Host header. +$subRequest->removeHeader('Host'); + +// Rewriting the url. +$subRequest->setUrl($remoteUrl . $request->getPath()); + +$client = new Client(); + +// Sends the HTTP request to the server +$response = $client->send($subRequest); + +// Sends the response back to the client that connected to the proxy. +Sapi::sendResponse($response); +``` + +The Request and Response API's +------------------------------ + +### Request + +```php + +/** + * Creates the request object + * + * @param string $method + * @param string $url + * @param array $headers + * @param resource $body + */ +public function __construct($method = null, $url = null, array $headers = null, $body = null); + +/** + * Returns the current HTTP method + * + * @return string + */ +function getMethod(); + +/** + * Sets the HTTP method + * + * @param string $method + * @return void + */ +function setMethod($method); + +/** + * Returns the request url. + * + * @return string + */ +function getUrl(); + +/** + * Sets the request url. + * + * @param string $url + * @return void + */ +function setUrl($url); + +/** + * Returns the absolute url. + * + * @return string + */ +function getAbsoluteUrl(); + +/** + * Sets the absolute url. + * + * @param string $url + * @return void + */ +function setAbsoluteUrl($url); + +/** + * Returns the current base url. + * + * @return string + */ +function getBaseUrl(); + +/** + * Sets a base url. + * + * This url is used for relative path calculations. + * + * The base url should default to / + * + * @param string $url + * @return void + */ +function setBaseUrl($url); + +/** + * Returns the relative path. + * + * This is being calculated using the base url. This path will not start + * with a slash, so it will always return something like + * 'example/path.html'. + * + * If the full path is equal to the base url, this method will return an + * empty string. + * + * This method will also urldecode the path, and if the url was encoded as + * ISO-8859-1, it will convert it to UTF-8. + * + * If the path is outside the base url, a LogicException will be thrown. + * + * @return string + */ +function getPath(); + +/** + * Returns the list of query parameters. + * + * This is equivalent to PHP's $_GET superglobal. + * + * @return array + */ +function getQueryParameters(); + +/** + * Returns the POST data. + * + * This is equivalent to PHP's $_POST superglobal. + * + * @return array + */ +function getPostData(); + +/** + * Sets the post data. + * + * This is equivalent to PHP's $_POST superglobal. + * + * This would not have been needed, if POST data was accessible as + * php://input, but unfortunately we need to special case it. + * + * @param array $postData + * @return void + */ +function setPostData(array $postData); + +/** + * Returns an item from the _SERVER array. + * + * If the value does not exist in the array, null is returned. + * + * @param string $valueName + * @return string|null + */ +function getRawServerValue($valueName); + +/** + * Sets the _SERVER array. + * + * @param array $data + * @return void + */ +function setRawServerData(array $data); + +/** + * Returns the body as a readable stream resource. + * + * Note that the stream may not be rewindable, and therefore may only be + * read once. + * + * @return resource + */ +function getBodyAsStream(); + +/** + * Returns the body as a string. + * + * Note that because the underlying data may be based on a stream, this + * method could only work correctly the first time. + * + * @return string + */ +function getBodyAsString(); + +/** + * Returns the message body, as its internal representation. + * + * This could be either a string or a stream. + * + * @return resource|string + */ +function getBody(); + +/** + * Updates the body resource with a new stream. + * + * @param resource $body + * @return void + */ +function setBody($body); + +/** + * Returns all the HTTP headers as an array. + * + * @return array + */ +function getHeaders(); + +/** + * Returns a specific HTTP header, based on its name. + * + * The name must be treated as case-insensitive. + * + * If the header does not exist, this method must return null. + * + * @param string $name + * @return string|null + */ +function getHeader($name); + +/** + * Updates a HTTP header. + * + * The case-sensitivity of the name value must be retained as-is. + * + * @param string $name + * @param string $value + * @return void + */ +function setHeader($name, $value); + +/** + * Resets HTTP headers + * + * This method overwrites all existing HTTP headers + * + * @param array $headers + * @return void + */ +function setHeaders(array $headers); + +/** + * Adds a new set of HTTP headers. + * + * Any header specified in the array that already exists will be + * overwritten, but any other existing headers will be retained. + * + * @param array $headers + * @return void + */ +function addHeaders(array $headers); + +/** + * Removes a HTTP header. + * + * The specified header name must be treated as case-insensitive. + * This method should return true if the header was successfully deleted, + * and false if the header did not exist. + * + * @return bool + */ +function removeHeader($name); + +/** + * Sets the HTTP version. + * + * Should be 1.0, 1.1 or 2.0. + * + * @param string $version + * @return void + */ +function setHttpVersion($version); + +/** + * Returns the HTTP version. + * + * @return string + */ +function getHttpVersion(); +``` + +### Response + +```php +/** + * Returns the current HTTP status. + * + * This is the status-code as well as the human-readable string. + * + * @return string + */ +function getStatus(); + +/** + * Sets the HTTP status code. + * + * This can be either the full HTTP status code with human-readable string, + * for example: "403 I can't let you do that, Dave". + * + * Or just the code, in which case the appropriate default message will be + * added. + * + * @param string|int $status + * @throws \InvalidArgumentExeption + * @return void + */ +function setStatus($status); + +/** + * Returns the body as a readable stream resource. + * + * Note that the stream may not be rewindable, and therefore may only be + * read once. + * + * @return resource + */ +function getBodyAsStream(); + +/** + * Returns the body as a string. + * + * Note that because the underlying data may be based on a stream, this + * method could only work correctly the first time. + * + * @return string + */ +function getBodyAsString(); + +/** + * Returns the message body, as its internal representation. + * + * This could be either a string or a stream. + * + * @return resource|string + */ +function getBody(); + + +/** + * Updates the body resource with a new stream. + * + * @param resource $body + * @return void + */ +function setBody($body); + +/** + * Returns all the HTTP headers as an array. + * + * @return array + */ +function getHeaders(); + +/** + * Returns a specific HTTP header, based on its name. + * + * The name must be treated as case-insensitive. + * + * If the header does not exist, this method must return null. + * + * @param string $name + * @return string|null + */ +function getHeader($name); + +/** + * Updates a HTTP header. + * + * The case-sensitivity of the name value must be retained as-is. + * + * @param string $name + * @param string $value + * @return void + */ +function setHeader($name, $value); + +/** + * Resets HTTP headers + * + * This method overwrites all existing HTTP headers + * + * @param array $headers + * @return void + */ +function setHeaders(array $headers); + +/** + * Adds a new set of HTTP headers. + * + * Any header specified in the array that already exists will be + * overwritten, but any other existing headers will be retained. + * + * @param array $headers + * @return void + */ +function addHeaders(array $headers); + +/** + * Removes a HTTP header. + * + * The specified header name must be treated as case-insensitive. + * This method should return true if the header was successfully deleted, + * and false if the header did not exist. + * + * @return bool + */ +function removeHeader($name); + +/** + * Sets the HTTP version. + * + * Should be 1.0, 1.1 or 2.0. + * + * @param string $version + * @return void + */ +function setHttpVersion($version); + +/** + * Returns the HTTP version. + * + * @return string + */ +function getHttpVersion(); +``` + +Made at fruux +------------- + +This library is being developed by [fruux](https://fruux.com/). Drop us a line for commercial services or enterprise support. + +[1]: http://getcomposer.org/ +[2]: http://sabre.io/ +[3]: https://github.com/symfony/HttpFoundation +[4]: http://php.net/curl +[5]: https://github.com/fruux/sabre-event +[6]: http://en.wikipedia.org/wiki/Decorator_pattern +[7]: http://guzzlephp.org/ +[8]: http://php.net/curl_multi_init diff --git a/lib/vendor/sabre/http/bin/.empty b/lib/vendor/sabre/http/bin/.empty new file mode 100644 index 0000000..e69de29 diff --git a/lib/vendor/sabre/http/composer.json b/lib/vendor/sabre/http/composer.json new file mode 100644 index 0000000..567dd0d --- /dev/null +++ b/lib/vendor/sabre/http/composer.json @@ -0,0 +1,64 @@ +{ + "name": "sabre/http", + "description" : "The sabre/http library provides utilities for dealing with http requests and responses. ", + "keywords" : [ "HTTP" ], + "homepage" : "https://github.com/fruux/sabre-http", + "license" : "BSD-3-Clause", + "require" : { + "php" : "^7.1 || ^8.0", + "ext-mbstring" : "*", + "ext-ctype" : "*", + "ext-curl" : "*", + "sabre/event" : ">=4.0 <6.0", + "sabre/uri" : "^2.0" + }, + "require-dev" : { + "friendsofphp/php-cs-fixer": "~2.17.1||3.63.2", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit" : "^7.5 || ^8.5 || ^9.6" + }, + "suggest" : { + "ext-curl" : " to make http requests with the Client class" + }, + "authors" : [ + { + "name" : "Evert Pot", + "email" : "me@evertpot.com", + "homepage" : "http://evertpot.com/", + "role" : "Developer" + } + ], + "support" : { + "forum" : "https://groups.google.com/group/sabredav-discuss", + "source" : "https://github.com/fruux/sabre-http" + }, + "autoload" : { + "files" : [ + "lib/functions.php" + ], + "psr-4" : { + "Sabre\\HTTP\\" : "lib/" + } + }, + "autoload-dev" : { + "psr-4" : { + "Sabre\\HTTP\\" : "tests/HTTP" + } + }, + "scripts": { + "phpstan": [ + "phpstan analyse lib tests" + ], + "cs-fixer": [ + "PHP_CS_FIXER_IGNORE_ENV=true php-cs-fixer fix" + ], + "phpunit": [ + "phpunit --configuration tests/phpunit.xml" + ], + "test": [ + "composer phpstan", + "composer cs-fixer", + "composer phpunit" + ] + } +} diff --git a/lib/vendor/sabre/http/examples/asyncclient.php b/lib/vendor/sabre/http/examples/asyncclient.php new file mode 100644 index 0000000..5bcc844 --- /dev/null +++ b/lib/vendor/sabre/http/examples/asyncclient.php @@ -0,0 +1,62 @@ +sendAsync( + $request, + + // This is the 'success' callback + function ($response) use ($i) { + echo "$i -> ".$response->getStatus()."\n"; + }, + + // This is the 'error' callback. It is called for general connection + // problems (such as not being able to connect to a host, dns errors, + // etc.) and also cases where a response was returned, but it had a + // status code of 400 or higher. + function ($error) use ($i) { + if (Client::STATUS_CURLERROR === $error['status']) { + // Curl errors + echo "$i -> curl error: ".$error['curl_errmsg']."\n"; + } else { + // HTTP errors + echo "$i -> ".$error['response']->getStatus()."\n"; + } + } + ); +} + +// After everything is done, we call 'wait'. This causes the client to wait for +// all outstanding http requests to complete. +$client->wait(); diff --git a/lib/vendor/sabre/http/examples/basicauth.php b/lib/vendor/sabre/http/examples/basicauth.php new file mode 100644 index 0000000..9c13da8 --- /dev/null +++ b/lib/vendor/sabre/http/examples/basicauth.php @@ -0,0 +1,50 @@ + 'password', + 'user2' => 'password', +]; + +use Sabre\HTTP\Auth; +use Sabre\HTTP\Response; +use Sabre\HTTP\Sapi; + +// Find the autoloader +$paths = [ + __DIR__.'/../vendor/autoload.php', + __DIR__.'/../../../autoload.php', + __DIR__.'/vendor/autoload.php', +]; + +foreach ($paths as $path) { + if (file_exists($path)) { + include $path; + break; + } +} + +$request = Sapi::getRequest(); +$response = new Response(); + +$basicAuth = new Auth\Basic('Locked down area', $request, $response); +if (!$userPass = $basicAuth->getCredentials()) { + // No username or password given + $basicAuth->requireLogin(); +} elseif (!isset($userList[$userPass[0]]) || $userList[$userPass[0]] !== $userPass[1]) { + // Username or password are incorrect + $basicAuth->requireLogin(); +} else { + // Success ! + $response->setBody('You are logged in!'); +} + +// Sending the response +Sapi::sendResponse($response); diff --git a/lib/vendor/sabre/http/examples/client.php b/lib/vendor/sabre/http/examples/client.php new file mode 100644 index 0000000..fd169a4 --- /dev/null +++ b/lib/vendor/sabre/http/examples/client.php @@ -0,0 +1,37 @@ +addCurlSetting(CURLOPT_PROXY,'localhost:8888'); +$response = $client->send($request); + +echo "Response:\n"; + +echo (string) $response; diff --git a/lib/vendor/sabre/http/examples/digestauth.php b/lib/vendor/sabre/http/examples/digestauth.php new file mode 100644 index 0000000..5594306 --- /dev/null +++ b/lib/vendor/sabre/http/examples/digestauth.php @@ -0,0 +1,51 @@ + 'password', + 'user2' => 'password', +]; + +use Sabre\HTTP\Auth; +use Sabre\HTTP\Response; +use Sabre\HTTP\Sapi; + +// Find the autoloader +$paths = [ + __DIR__.'/../vendor/autoload.php', + __DIR__.'/../../../autoload.php', + __DIR__.'/vendor/autoload.php', +]; + +foreach ($paths as $path) { + if (file_exists($path)) { + include $path; + break; + } +} + +$request = Sapi::getRequest(); +$response = new Response(); + +$digestAuth = new Auth\Digest('Locked down area', $request, $response); +$digestAuth->init(); +if (!$userName = $digestAuth->getUsername()) { + // No username given + $digestAuth->requireLogin(); +} elseif (!isset($userList[$userName]) || !$digestAuth->validatePassword($userList[$userName])) { + // Username or password are incorrect + $digestAuth->requireLogin(); +} else { + // Success ! + $response->setBody('You are logged in!'); +} + +// Sending the response +Sapi::sendResponse($response); diff --git a/lib/vendor/sabre/http/examples/reverseproxy.php b/lib/vendor/sabre/http/examples/reverseproxy.php new file mode 100644 index 0000000..ce51ad4 --- /dev/null +++ b/lib/vendor/sabre/http/examples/reverseproxy.php @@ -0,0 +1,48 @@ +setBaseUrl($myBaseUrl); + +$subRequest = clone $request; + +// Removing the Host header. +$subRequest->removeHeader('Host'); + +// Rewriting the url. +$subRequest->setUrl($remoteUrl.$request->getPath()); + +$client = new Client(); + +// Sends the HTTP request to the server +$response = $client->send($subRequest); + +// Sends the response back to the client that connected to the proxy. +Sapi::sendResponse($response); diff --git a/lib/vendor/sabre/http/examples/stringify.php b/lib/vendor/sabre/http/examples/stringify.php new file mode 100644 index 0000000..8ceefe2 --- /dev/null +++ b/lib/vendor/sabre/http/examples/stringify.php @@ -0,0 +1,50 @@ +setHeaders([ + 'Host' => 'example.org', + 'Content-Type' => 'application/json', +]); + +$request->setBody(json_encode(['foo' => 'bar'])); + +echo $request; +echo "\r\n\r\n"; + +$response = new Response(424); +$response->setHeaders([ + 'Content-Type' => 'text/plain', + 'Connection' => 'close', +]); + +$response->setBody('ABORT! ABORT!'); + +echo $response; + +echo "\r\n"; diff --git a/lib/vendor/sabre/http/lib/Auth/AWS.php b/lib/vendor/sabre/http/lib/Auth/AWS.php new file mode 100644 index 0000000..2690c63 --- /dev/null +++ b/lib/vendor/sabre/http/lib/Auth/AWS.php @@ -0,0 +1,220 @@ +request->getHeader('Authorization'); + + if (null === $authHeader) { + $this->errorCode = self::ERR_NOAWSHEADER; + + return false; + } + $authHeader = explode(' ', $authHeader); + + if ('AWS' !== $authHeader[0] || !isset($authHeader[1])) { + $this->errorCode = self::ERR_NOAWSHEADER; + + return false; + } + + list($this->accessKey, $this->signature) = explode(':', $authHeader[1]); + + return true; + } + + /** + * Returns the username for the request. + */ + public function getAccessKey(): string + { + return $this->accessKey; + } + + /** + * Validates the signature based on the secretKey. + */ + public function validate(string $secretKey): bool + { + $contentMD5 = $this->request->getHeader('Content-MD5'); + + if ($contentMD5) { + // We need to validate the integrity of the request + $body = $this->request->getBody(); + $this->request->setBody($body); + + if ($contentMD5 !== base64_encode(md5((string) $body, true))) { + // content-md5 header did not match md5 signature of body + $this->errorCode = self::ERR_MD5CHECKSUMWRONG; + + return false; + } + } + + if (!$requestDate = $this->request->getHeader('x-amz-date')) { + $requestDate = $this->request->getHeader('Date'); + } + + if (!$this->validateRFC2616Date((string) $requestDate)) { + return false; + } + + $amzHeaders = $this->getAmzHeaders(); + + $signature = base64_encode( + $this->hmacsha1($secretKey, + $this->request->getMethod()."\n". + $contentMD5."\n". + $this->request->getHeader('Content-type')."\n". + $requestDate."\n". + $amzHeaders. + $this->request->getUrl() + ) + ); + + if ($this->signature !== $signature) { + $this->errorCode = self::ERR_INVALIDSIGNATURE; + + return false; + } + + return true; + } + + /** + * Returns an HTTP 401 header, forcing login. + * + * This should be called when username and password are incorrect, or not supplied at all + */ + public function requireLogin() + { + $this->response->addHeader('WWW-Authenticate', 'AWS'); + $this->response->setStatus(401); + } + + /** + * Makes sure the supplied value is a valid RFC2616 date. + * + * If we would just use strtotime to get a valid timestamp, we have no way of checking if a + * user just supplied the word 'now' for the date header. + * + * This function also makes sure the Date header is within 15 minutes of the operating + * system date, to prevent replay attacks. + */ + protected function validateRFC2616Date(string $dateHeader): bool + { + $date = HTTP\parseDate($dateHeader); + + // Unknown format + if (!$date) { + $this->errorCode = self::ERR_INVALIDDATEFORMAT; + + return false; + } + + $min = new \DateTime('-15 minutes'); + $max = new \DateTime('+15 minutes'); + + // We allow 15 minutes around the current date/time + if ($date > $max || $date < $min) { + $this->errorCode = self::ERR_REQUESTTIMESKEWED; + + return false; + } + + return true; + } + + /** + * Returns a list of AMZ headers. + */ + protected function getAmzHeaders(): string + { + $amzHeaders = []; + $headers = $this->request->getHeaders(); + foreach ($headers as $headerName => $headerValue) { + if (0 === strpos(strtolower($headerName), 'x-amz-')) { + $amzHeaders[strtolower($headerName)] = str_replace(["\r\n"], [' '], $headerValue[0])."\n"; + } + } + ksort($amzHeaders); + + $headerStr = ''; + foreach ($amzHeaders as $h => $v) { + $headerStr .= $h.':'.$v; + } + + return $headerStr; + } + + /** + * Generates an HMAC-SHA1 signature. + */ + private function hmacsha1(string $key, string $message): string + { + if (function_exists('hash_hmac')) { + return hash_hmac('sha1', $message, $key, true); + } + + $blocksize = 64; + if (strlen($key) > $blocksize) { + $key = pack('H*', sha1($key)); + } + $key = str_pad($key, $blocksize, chr(0x00)); + $ipad = str_repeat(chr(0x36), $blocksize); + $opad = str_repeat(chr(0x5C), $blocksize); + $hmac = pack('H*', sha1(($key ^ $opad).pack('H*', sha1(($key ^ $ipad).$message)))); + + return $hmac; + } +} diff --git a/lib/vendor/sabre/http/lib/Auth/AbstractAuth.php b/lib/vendor/sabre/http/lib/Auth/AbstractAuth.php new file mode 100644 index 0000000..07f451b --- /dev/null +++ b/lib/vendor/sabre/http/lib/Auth/AbstractAuth.php @@ -0,0 +1,65 @@ +realm = $realm; + $this->request = $request; + $this->response = $response; + } + + /** + * This method sends the needed HTTP header and status code (401) to force + * the user to login. + */ + abstract public function requireLogin(); + + /** + * Returns the HTTP realm. + */ + public function getRealm(): string + { + return $this->realm; + } +} diff --git a/lib/vendor/sabre/http/lib/Auth/Basic.php b/lib/vendor/sabre/http/lib/Auth/Basic.php new file mode 100644 index 0000000..c1bad1a --- /dev/null +++ b/lib/vendor/sabre/http/lib/Auth/Basic.php @@ -0,0 +1,60 @@ +request->getHeader('Authorization'); + + if (!$auth) { + return null; + } + + if ('basic ' !== strtolower(substr($auth, 0, 6))) { + return null; + } + + $credentials = explode(':', base64_decode(substr($auth, 6)), 2); + + if (2 !== count($credentials)) { + return null; + } + + return $credentials; + } + + /** + * This method sends the needed HTTP header and status code (401) to force + * the user to login. + */ + public function requireLogin() + { + $this->response->addHeader('WWW-Authenticate', 'Basic realm="'.$this->realm.'", charset="UTF-8"'); + $this->response->setStatus(401); + } +} diff --git a/lib/vendor/sabre/http/lib/Auth/Bearer.php b/lib/vendor/sabre/http/lib/Auth/Bearer.php new file mode 100644 index 0000000..580e239 --- /dev/null +++ b/lib/vendor/sabre/http/lib/Auth/Bearer.php @@ -0,0 +1,53 @@ +request->getHeader('Authorization'); + + if (!$auth) { + return null; + } + + if ('bearer ' !== strtolower(substr($auth, 0, 7))) { + return null; + } + + return substr($auth, 7); + } + + /** + * This method sends the needed HTTP header and status code (401) to force + * authentication. + */ + public function requireLogin() + { + $this->response->addHeader('WWW-Authenticate', 'Bearer realm="'.$this->realm.'"'); + $this->response->setStatus(401); + } +} diff --git a/lib/vendor/sabre/http/lib/Auth/Digest.php b/lib/vendor/sabre/http/lib/Auth/Digest.php new file mode 100644 index 0000000..08fa34f --- /dev/null +++ b/lib/vendor/sabre/http/lib/Auth/Digest.php @@ -0,0 +1,208 @@ +nonce = uniqid(); + $this->opaque = md5($realm); + parent::__construct($realm, $request, $response); + } + + /** + * Gathers all information from the headers. + * + * This method needs to be called prior to anything else. + */ + public function init() + { + $digest = $this->getDigest(); + $this->digestParts = $this->parseDigest((string) $digest); + } + + /** + * Sets the quality of protection value. + * + * Possible values are: + * Sabre\HTTP\DigestAuth::QOP_AUTH + * Sabre\HTTP\DigestAuth::QOP_AUTHINT + * + * Multiple values can be specified using logical OR. + * + * QOP_AUTHINT ensures integrity of the request body, but this is not + * supported by most HTTP clients. QOP_AUTHINT also requires the entire + * request body to be md5'ed, which can put strains on CPU and memory. + */ + public function setQOP(int $qop) + { + $this->qop = $qop; + } + + /** + * Validates the user. + * + * The A1 parameter should be md5($username . ':' . $realm . ':' . $password); + */ + public function validateA1(string $A1): bool + { + $this->A1 = $A1; + + return $this->validate(); + } + + /** + * Validates authentication through a password. The actual password must be provided here. + * It is strongly recommended not store the password in plain-text and use validateA1 instead. + */ + public function validatePassword(string $password): bool + { + $this->A1 = md5($this->digestParts['username'].':'.$this->realm.':'.$password); + + return $this->validate(); + } + + /** + * Returns the username for the request. + * Returns null if there were none. + * + * @return string|null + */ + public function getUsername() + { + return $this->digestParts['username'] ?? null; + } + + /** + * Validates the digest challenge. + */ + protected function validate(): bool + { + if (!is_array($this->digestParts)) { + return false; + } + + $A2 = $this->request->getMethod().':'.$this->digestParts['uri']; + + if ('auth-int' === $this->digestParts['qop']) { + // Making sure we support this qop value + if (!($this->qop & self::QOP_AUTHINT)) { + return false; + } + // We need to add an md5 of the entire request body to the A2 part of the hash + $body = $this->request->getBody(); + $this->request->setBody($body); + $A2 .= ':'.md5($body); + } elseif (!($this->qop & self::QOP_AUTH)) { + return false; + } + + $A2 = md5($A2); + + $validResponse = md5("{$this->A1}:{$this->digestParts['nonce']}:{$this->digestParts['nc']}:{$this->digestParts['cnonce']}:{$this->digestParts['qop']}:{$A2}"); + + return $this->digestParts['response'] === $validResponse; + } + + /** + * Returns an HTTP 401 header, forcing login. + * + * This should be called when username and password are incorrect, or not supplied at all + */ + public function requireLogin() + { + $qop = ''; + switch ($this->qop) { + case self::QOP_AUTH: + $qop = 'auth'; + break; + case self::QOP_AUTHINT: + $qop = 'auth-int'; + break; + case self::QOP_AUTH | self::QOP_AUTHINT: + $qop = 'auth,auth-int'; + break; + } + + $this->response->addHeader('WWW-Authenticate', 'Digest realm="'.$this->realm.'",qop="'.$qop.'",nonce="'.$this->nonce.'",opaque="'.$this->opaque.'"'); + $this->response->setStatus(401); + } + + /** + * This method returns the full digest string. + * + * It should be compatible with mod_php format and other webservers. + * + * If the header could not be found, null will be returned + */ + public function getDigest() + { + return $this->request->getHeader('Authorization'); + } + + /** + * Parses the different pieces of the digest string into an array. + * + * This method returns false if an incomplete digest was supplied + * + * @return bool|array + */ + protected function parseDigest(string $digest) + { + // protect against missing data + $needed_parts = ['nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1]; + $data = []; + + preg_match_all('@(\w+)=(?:(?:")([^"]+)"|([^\s,$]+))@', $digest, $matches, PREG_SET_ORDER); + + foreach ($matches as $m) { + $data[$m[1]] = $m[2] ?: $m[3]; + unset($needed_parts[$m[1]]); + } + + return $needed_parts ? false : $data; + } +} diff --git a/lib/vendor/sabre/http/lib/Client.php b/lib/vendor/sabre/http/lib/Client.php new file mode 100644 index 0000000..c00f9e1 --- /dev/null +++ b/lib/vendor/sabre/http/lib/Client.php @@ -0,0 +1,620 @@ +curlSettings = [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_NOBODY => false, + CURLOPT_USERAGENT => 'sabre-http/'.Version::VERSION.' (http://sabre.io/)', + ]; + if ($separatedHeaders) { + $this->curlSettings[CURLOPT_HEADERFUNCTION] = [$this, 'receiveCurlHeader']; + } else { + $this->curlSettings[CURLOPT_HEADER] = true; + } + } + + protected function receiveCurlHeader($curlHandle, $headerLine) + { + $this->headerLinesMap[(int) $curlHandle][] = $headerLine; + + return strlen($headerLine); + } + + /** + * Sends a request to a HTTP server, and returns a response. + */ + public function send(RequestInterface $request): ResponseInterface + { + $this->emit('beforeRequest', [$request]); + + $retryCount = 0; + $redirects = 0; + + do { + $doRedirect = false; + $retry = false; + + try { + $response = $this->doRequest($request); + + $code = $response->getStatus(); + + // We are doing in-PHP redirects, because curl's + // FOLLOW_LOCATION throws errors when PHP is configured with + // open_basedir. + // + // https://github.com/fruux/sabre-http/issues/12 + if ($redirects < $this->maxRedirects && in_array($code, [301, 302, 307, 308])) { + $oldLocation = $request->getUrl(); + + // Creating a new instance of the request object. + $request = clone $request; + + // Setting the new location + $request->setUrl(Uri\resolve( + $oldLocation, + $response->getHeader('Location') + )); + + $doRedirect = true; + ++$redirects; + } + + // This was a HTTP error + if ($code >= 400) { + $this->emit('error', [$request, $response, &$retry, $retryCount]); + $this->emit('error:'.$code, [$request, $response, &$retry, $retryCount]); + } + } catch (ClientException $e) { + $this->emit('exception', [$request, $e, &$retry, $retryCount]); + + // If retry was still set to false, it means no event handler + // dealt with the problem. In this case we just re-throw the + // exception. + if (!$retry) { + throw $e; + } + } + + if ($retry) { + ++$retryCount; + } + } while ($retry || $doRedirect); + + $this->emit('afterRequest', [$request, $response]); + + if ($this->throwExceptions && $code >= 400) { + throw new ClientHttpException($response); + } + + return $response; + } + + /** + * Sends a HTTP request asynchronously. + * + * Due to the nature of PHP, you must from time to time poll to see if any + * new responses came in. + * + * After calling sendAsync, you must therefore occasionally call the poll() + * method, or wait(). + */ + public function sendAsync(RequestInterface $request, ?callable $success = null, ?callable $error = null) + { + $this->emit('beforeRequest', [$request]); + $this->sendAsyncInternal($request, $success, $error); + $this->poll(); + } + + /** + * This method checks if any http requests have gotten results, and if so, + * call the appropriate success or error handlers. + * + * This method will return true if there are still requests waiting to + * return, and false if all the work is done. + */ + public function poll(): bool + { + // nothing to do? + if (!$this->curlMultiMap) { + return false; + } + + do { + $r = curl_multi_exec( + $this->curlMultiHandle, + $stillRunning + ); + } while (CURLM_CALL_MULTI_PERFORM === $r); + + $messagesInQueue = 0; + do { + messageQueue: + + $status = curl_multi_info_read( + $this->curlMultiHandle, + $messagesInQueue + ); + + if ($status && CURLMSG_DONE === $status['msg']) { + $resourceId = (int) $status['handle']; + list( + $request, + $successCallback, + $errorCallback, + $retryCount) = $this->curlMultiMap[$resourceId]; + unset($this->curlMultiMap[$resourceId]); + + $curlHandle = $status['handle']; + $curlResult = $this->parseResponse(curl_multi_getcontent($curlHandle), $curlHandle); + $retry = false; + + if (self::STATUS_CURLERROR === $curlResult['status']) { + $e = new ClientException($curlResult['curl_errmsg'], $curlResult['curl_errno']); + $this->emit('exception', [$request, $e, &$retry, $retryCount]); + + if ($retry) { + ++$retryCount; + $this->sendAsyncInternal($request, $successCallback, $errorCallback, $retryCount); + goto messageQueue; + } + + $curlResult['request'] = $request; + + if ($errorCallback) { + $errorCallback($curlResult); + } + } elseif (self::STATUS_HTTPERROR === $curlResult['status']) { + $this->emit('error', [$request, $curlResult['response'], &$retry, $retryCount]); + $this->emit('error:'.$curlResult['http_code'], [$request, $curlResult['response'], &$retry, $retryCount]); + + if ($retry) { + ++$retryCount; + $this->sendAsyncInternal($request, $successCallback, $errorCallback, $retryCount); + goto messageQueue; + } + + $curlResult['request'] = $request; + + if ($errorCallback) { + $errorCallback($curlResult); + } + } else { + $this->emit('afterRequest', [$request, $curlResult['response']]); + + if ($successCallback) { + $successCallback($curlResult['response']); + } + } + } + } while ($messagesInQueue > 0); + + return count($this->curlMultiMap) > 0; + } + + /** + * Processes every HTTP request in the queue, and waits till they are all + * completed. + */ + public function wait() + { + do { + curl_multi_select($this->curlMultiHandle); + $stillRunning = $this->poll(); + } while ($stillRunning); + } + + /** + * If this is set to true, the Client will automatically throw exceptions + * upon HTTP errors. + * + * This means that if a response came back with a status code greater than + * or equal to 400, we will throw a ClientHttpException. + * + * This only works for the send() method. Throwing exceptions for + * sendAsync() is not supported. + */ + public function setThrowExceptions(bool $throwExceptions) + { + $this->throwExceptions = $throwExceptions; + } + + /** + * Adds a CURL setting. + * + * These settings will be included in every HTTP request. + */ + public function addCurlSetting(int $name, $value) + { + $this->curlSettings[$name] = $value; + } + + /** + * This method is responsible for performing a single request. + */ + protected function doRequest(RequestInterface $request): ResponseInterface + { + $settings = $this->createCurlSettingsArray($request); + + if (!$this->curlHandle) { + $this->curlHandle = curl_init(); + } else { + curl_reset($this->curlHandle); + } + + curl_setopt_array($this->curlHandle, $settings); + $response = $this->curlExec($this->curlHandle); + $response = $this->parseResponse($response, $this->curlHandle); + if (self::STATUS_CURLERROR === $response['status']) { + throw new ClientException($response['curl_errmsg'], $response['curl_errno']); + } + + return $response['response']; + } + + /** + * Cached curl handle. + * + * By keeping this resource around for the lifetime of this object, things + * like persistent connections are possible. + * + * @var resource + */ + private $curlHandle; + + /** + * Handler for curl_multi requests. + * + * The first time sendAsync is used, this will be created. + * + * @var resource + */ + private $curlMultiHandle; + + /** + * Has a list of curl handles, as well as their associated success and + * error callbacks. + * + * @var array + */ + private $curlMultiMap = []; + + /** + * Turns a RequestInterface object into an array with settings that can be + * fed to curl_setopt. + */ + protected function createCurlSettingsArray(RequestInterface $request): array + { + $settings = $this->curlSettings; + + switch ($request->getMethod()) { + case 'HEAD': + $settings[CURLOPT_NOBODY] = true; + $settings[CURLOPT_CUSTOMREQUEST] = 'HEAD'; + break; + case 'GET': + $settings[CURLOPT_CUSTOMREQUEST] = 'GET'; + break; + default: + $body = $request->getBody(); + if (is_resource($body)) { + $bodyStat = fstat($body); + + // This needs to be set to PUT, regardless of the actual + // method used. Without it, INFILE will be ignored for some + // reason. + $settings[CURLOPT_PUT] = true; + $settings[CURLOPT_INFILE] = $body; + if (false !== $bodyStat && array_key_exists('size', $bodyStat)) { + $settings[CURLOPT_INFILESIZE] = $bodyStat['size']; + } + } else { + // For security we cast this to a string. If somehow an array could + // be passed here, it would be possible for an attacker to use @ to + // post local files. + $settings[CURLOPT_POSTFIELDS] = (string) $body; + } + $settings[CURLOPT_CUSTOMREQUEST] = $request->getMethod(); + break; + } + + $nHeaders = []; + foreach ($request->getHeaders() as $key => $values) { + foreach ($values as $value) { + $nHeaders[] = $key.': '.$value; + } + } + + if ([] !== $nHeaders) { + $settings[CURLOPT_HTTPHEADER] = $nHeaders; + } + $settings[CURLOPT_URL] = $request->getUrl(); + // FIXME: CURLOPT_PROTOCOLS is currently unsupported by HHVM + if (defined('CURLOPT_PROTOCOLS')) { + $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + } + // FIXME: CURLOPT_REDIR_PROTOCOLS is currently unsupported by HHVM + if (defined('CURLOPT_REDIR_PROTOCOLS')) { + $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + } + + return $settings; + } + + public const STATUS_SUCCESS = 0; + public const STATUS_CURLERROR = 1; + public const STATUS_HTTPERROR = 2; + + private function parseResponse(string $response, $curlHandle): array + { + $settings = $this->curlSettings; + $separatedHeaders = isset($settings[CURLOPT_HEADERFUNCTION]) && (bool) $settings[CURLOPT_HEADERFUNCTION]; + + if ($separatedHeaders) { + $resourceId = (int) $curlHandle; + if (isset($this->headerLinesMap[$resourceId])) { + $headers = $this->headerLinesMap[$resourceId]; + } else { + $headers = []; + } + $response = $this->parseCurlResponse($headers, $response, $curlHandle); + } else { + $response = $this->parseCurlResult($response, $curlHandle); + } + + return $response; + } + + /** + * Parses the result of a curl call in a format that's a bit more + * convenient to work with. + * + * The method returns an array with the following elements: + * * status - one of the 3 STATUS constants. + * * curl_errno - A curl error number. Only set if status is + * STATUS_CURLERROR. + * * curl_errmsg - A current error message. Only set if status is + * STATUS_CURLERROR. + * * response - Response object. Only set if status is STATUS_SUCCESS, or + * STATUS_HTTPERROR. + * * http_code - HTTP status code, as an int. Only set if Only set if + * status is STATUS_SUCCESS, or STATUS_HTTPERROR + * + * @param resource $curlHandle + */ + protected function parseCurlResponse(array $headerLines, string $body, $curlHandle): array + { + list( + $curlInfo, + $curlErrNo, + $curlErrMsg + ) = $this->curlStuff($curlHandle); + + if ($curlErrNo) { + return [ + 'status' => self::STATUS_CURLERROR, + 'curl_errno' => $curlErrNo, + 'curl_errmsg' => $curlErrMsg, + ]; + } + + $response = new Response(); + $response->setStatus($curlInfo['http_code']); + $response->setBody($body); + + foreach ($headerLines as $header) { + $parts = explode(':', $header, 2); + if (2 === count($parts)) { + $response->addHeader(trim($parts[0]), trim($parts[1])); + } + } + + $httpCode = $response->getStatus(); + + return [ + 'status' => $httpCode >= 400 ? self::STATUS_HTTPERROR : self::STATUS_SUCCESS, + 'response' => $response, + 'http_code' => $httpCode, + ]; + } + + /** + * Parses the result of a curl call in a format that's a bit more + * convenient to work with. + * + * The method returns an array with the following elements: + * * status - one of the 3 STATUS constants. + * * curl_errno - A curl error number. Only set if status is + * STATUS_CURLERROR. + * * curl_errmsg - A current error message. Only set if status is + * STATUS_CURLERROR. + * * response - Response object. Only set if status is STATUS_SUCCESS, or + * STATUS_HTTPERROR. + * * http_code - HTTP status code, as an int. Only set if Only set if + * status is STATUS_SUCCESS, or STATUS_HTTPERROR + * + * @deprecated Use parseCurlResponse instead + * + * @param resource $curlHandle + */ + protected function parseCurlResult(string $response, $curlHandle): array + { + list( + $curlInfo, + $curlErrNo, + $curlErrMsg + ) = $this->curlStuff($curlHandle); + + if ($curlErrNo) { + return [ + 'status' => self::STATUS_CURLERROR, + 'curl_errno' => $curlErrNo, + 'curl_errmsg' => $curlErrMsg, + ]; + } + + $headerBlob = substr($response, 0, $curlInfo['header_size']); + // In the case of 204 No Content, strlen($response) == $curlInfo['header_size]. + // This will cause substr($response, $curlInfo['header_size']) return FALSE instead of NULL + // An exception will be thrown when calling getBodyAsString then + $responseBody = substr($response, $curlInfo['header_size']) ?: ''; + + unset($response); + + // In the case of 100 Continue, or redirects we'll have multiple lists + // of headers for each separate HTTP response. We can easily split this + // because they are separated by \r\n\r\n + $headerBlob = explode("\r\n\r\n", trim($headerBlob, "\r\n")); + + // We only care about the last set of headers + $headerBlob = $headerBlob[count($headerBlob) - 1]; + + // Splitting headers + $headerBlob = explode("\r\n", $headerBlob); + + return $this->parseCurlResponse($headerBlob, $responseBody, $curlHandle); + } + + /** + * Sends an asynchronous HTTP request. + * + * We keep this in a separate method, so we can call it without triggering + * the beforeRequest event and don't do the poll(). + */ + protected function sendAsyncInternal(RequestInterface $request, callable $success, callable $error, int $retryCount = 0) + { + if (!$this->curlMultiHandle) { + $this->curlMultiHandle = curl_multi_init(); + } + $curl = curl_init(); + curl_setopt_array( + $curl, + $this->createCurlSettingsArray($request) + ); + curl_multi_add_handle($this->curlMultiHandle, $curl); + + $resourceId = (int) $curl; + $this->headerLinesMap[$resourceId] = []; + $this->curlMultiMap[$resourceId] = [ + $request, + $success, + $error, + $retryCount, + ]; + } + + // @codeCoverageIgnoreStart + + /** + * Calls curl_exec. + * + * This method exists so it can easily be overridden and mocked. + * + * @param resource $curlHandle + */ + protected function curlExec($curlHandle): string + { + $this->headerLinesMap[(int) $curlHandle] = []; + + $result = curl_exec($curlHandle); + if (false === $result) { + $result = ''; + } + + return $result; + } + + /** + * Returns a bunch of information about a curl request. + * + * This method exists so it can easily be overridden and mocked. + * + * @param resource $curlHandle + */ + protected function curlStuff($curlHandle): array + { + return [ + curl_getinfo($curlHandle), + curl_errno($curlHandle), + curl_error($curlHandle), + ]; + } + + // @codeCoverageIgnoreEnd +} diff --git a/lib/vendor/sabre/http/lib/ClientException.php b/lib/vendor/sabre/http/lib/ClientException.php new file mode 100644 index 0000000..2ca4a28 --- /dev/null +++ b/lib/vendor/sabre/http/lib/ClientException.php @@ -0,0 +1,17 @@ +response = $response; + parent::__construct($response->getStatusText(), $response->getStatus()); + } + + /** + * The http status code for the error. + */ + public function getHttpStatus(): int + { + return $this->response->getStatus(); + } + + /** + * Returns the full response object. + */ + public function getResponse(): ResponseInterface + { + return $this->response; + } +} diff --git a/lib/vendor/sabre/http/lib/HttpException.php b/lib/vendor/sabre/http/lib/HttpException.php new file mode 100644 index 0000000..80b3ae6 --- /dev/null +++ b/lib/vendor/sabre/http/lib/HttpException.php @@ -0,0 +1,31 @@ +getBody(); + if (is_callable($this->body)) { + $body = $this->getBodyAsString(); + } + if (is_string($body) || null === $body) { + $stream = fopen('php://temp', 'r+'); + fwrite($stream, (string) $body); + rewind($stream); + + return $stream; + } + + return $body; + } + + /** + * Returns the body as a string. + * + * Note that because the underlying data may be based on a stream, this + * method could only work correctly the first time. + */ + public function getBodyAsString(): string + { + $body = $this->getBody(); + if (is_string($body)) { + return $body; + } + if (null === $body) { + return ''; + } + if (is_callable($body)) { + ob_start(); + $body(); + + return ob_get_clean(); + } + /** + * @var string|int|null + */ + $contentLength = $this->getHeader('Content-Length'); + if (null !== $contentLength && (is_int($contentLength) || ctype_digit($contentLength))) { + return stream_get_contents($body, (int) $contentLength); + } + + return stream_get_contents($body); + } + + /** + * Returns the message body, as its internal representation. + * + * This could be either a string, a stream or a callback writing the body to php://output. + * + * @return resource|string|callable + */ + public function getBody() + { + return $this->body; + } + + /** + * Replaces the body resource with a new stream, string or a callback writing the body to php://output. + * + * @param resource|string|callable $body + */ + public function setBody($body) + { + $this->body = $body; + } + + /** + * Returns all the HTTP headers as an array. + * + * Every header is returned as an array, with one or more values. + */ + public function getHeaders(): array + { + $result = []; + foreach ($this->headers as $headerInfo) { + $result[$headerInfo[0]] = $headerInfo[1]; + } + + return $result; + } + + /** + * Will return true or false, depending on if a HTTP header exists. + */ + public function hasHeader(string $name): bool + { + return isset($this->headers[strtolower($name)]); + } + + /** + * Returns a specific HTTP header, based on its name. + * + * The name must be treated as case-insensitive. + * If the header does not exist, this method must return null. + * + * If a header appeared more than once in a HTTP request, this method will + * concatenate all the values with a comma. + * + * Note that this not make sense for all headers. Some, such as + * `Set-Cookie` cannot be logically combined with a comma. In those cases + * you *should* use getHeaderAsArray(). + * + * @return string|null + */ + public function getHeader(string $name) + { + $name = strtolower($name); + + if (isset($this->headers[$name])) { + return implode(',', $this->headers[$name][1]); + } + + return null; + } + + /** + * Returns a HTTP header as an array. + * + * For every time the HTTP header appeared in the request or response, an + * item will appear in the array. + * + * If the header did not exist, this method will return an empty array. + * + * @return string[] + */ + public function getHeaderAsArray(string $name): array + { + $name = strtolower($name); + + if (isset($this->headers[$name])) { + return $this->headers[$name][1]; + } + + return []; + } + + /** + * Updates a HTTP header. + * + * The case-sensitivity of the name value must be retained as-is. + * + * If the header already existed, it will be overwritten. + * + * @param string|string[] $value + */ + public function setHeader(string $name, $value) + { + $this->headers[strtolower($name)] = [$name, (array) $value]; + } + + /** + * Sets a new set of HTTP headers. + * + * The headers array should contain headernames for keys, and their value + * should be specified as either a string or an array. + * + * Any header that already existed will be overwritten. + */ + public function setHeaders(array $headers) + { + foreach ($headers as $name => $value) { + $this->setHeader($name, $value); + } + } + + /** + * Adds a HTTP header. + * + * This method will not overwrite any existing HTTP header, but instead add + * another value. Individual values can be retrieved with + * getHeadersAsArray. + * + * @param string|string[] $value + */ + public function addHeader(string $name, $value) + { + $lName = strtolower($name); + if (isset($this->headers[$lName])) { + $this->headers[$lName][1] = array_merge( + $this->headers[$lName][1], + (array) $value + ); + } else { + $this->headers[$lName] = [ + $name, + (array) $value, + ]; + } + } + + /** + * Adds a new set of HTTP headers. + * + * Any existing headers will not be overwritten. + */ + public function addHeaders(array $headers) + { + foreach ($headers as $name => $value) { + $this->addHeader($name, $value); + } + } + + /** + * Removes a HTTP header. + * + * The specified header name must be treated as case-insensitive. + * This method should return true if the header was successfully deleted, + * and false if the header did not exist. + */ + public function removeHeader(string $name): bool + { + $name = strtolower($name); + if (!isset($this->headers[$name])) { + return false; + } + unset($this->headers[$name]); + + return true; + } + + /** + * Sets the HTTP version. + * + * Should be 1.0, 1.1 or 2.0. + */ + public function setHttpVersion(string $version) + { + $this->httpVersion = $version; + } + + /** + * Returns the HTTP version. + */ + public function getHttpVersion(): string + { + return $this->httpVersion; + } +} diff --git a/lib/vendor/sabre/http/lib/MessageDecoratorTrait.php b/lib/vendor/sabre/http/lib/MessageDecoratorTrait.php new file mode 100644 index 0000000..191ba0f --- /dev/null +++ b/lib/vendor/sabre/http/lib/MessageDecoratorTrait.php @@ -0,0 +1,206 @@ +inner->getBodyAsStream(); + } + + /** + * Returns the body as a string. + * + * Note that because the underlying data may be based on a stream, this + * method could only work correctly the first time. + */ + public function getBodyAsString(): string + { + return $this->inner->getBodyAsString(); + } + + /** + * Returns the message body, as its internal representation. + * + * This could be either a string or a stream. + * + * @return resource|string + */ + public function getBody() + { + return $this->inner->getBody(); + } + + /** + * Updates the body resource with a new stream. + * + * @param resource|string|callable $body + */ + public function setBody($body) + { + $this->inner->setBody($body); + } + + /** + * Returns all the HTTP headers as an array. + * + * Every header is returned as an array, with one or more values. + */ + public function getHeaders(): array + { + return $this->inner->getHeaders(); + } + + /** + * Will return true or false, depending on if a HTTP header exists. + */ + public function hasHeader(string $name): bool + { + return $this->inner->hasHeader($name); + } + + /** + * Returns a specific HTTP header, based on its name. + * + * The name must be treated as case-insensitive. + * If the header does not exist, this method must return null. + * + * If a header appeared more than once in a HTTP request, this method will + * concatenate all the values with a comma. + * + * Note that this not make sense for all headers. Some, such as + * `Set-Cookie` cannot be logically combined with a comma. In those cases + * you *should* use getHeaderAsArray(). + * + * @return string|null + */ + public function getHeader(string $name) + { + return $this->inner->getHeader($name); + } + + /** + * Returns a HTTP header as an array. + * + * For every time the HTTP header appeared in the request or response, an + * item will appear in the array. + * + * If the header did not exist, this method will return an empty array. + */ + public function getHeaderAsArray(string $name): array + { + return $this->inner->getHeaderAsArray($name); + } + + /** + * Updates a HTTP header. + * + * The case-sensitivity of the name value must be retained as-is. + * + * If the header already existed, it will be overwritten. + * + * @param string|string[] $value + */ + public function setHeader(string $name, $value) + { + $this->inner->setHeader($name, $value); + } + + /** + * Sets a new set of HTTP headers. + * + * The headers array should contain headernames for keys, and their value + * should be specified as either a string or an array. + * + * Any header that already existed will be overwritten. + */ + public function setHeaders(array $headers) + { + $this->inner->setHeaders($headers); + } + + /** + * Adds a HTTP header. + * + * This method will not overwrite any existing HTTP header, but instead add + * another value. Individual values can be retrieved with + * getHeadersAsArray. + * + * @param string|string[] $value + */ + public function addHeader(string $name, $value) + { + $this->inner->addHeader($name, $value); + } + + /** + * Adds a new set of HTTP headers. + * + * Any existing headers will not be overwritten. + */ + public function addHeaders(array $headers) + { + $this->inner->addHeaders($headers); + } + + /** + * Removes a HTTP header. + * + * The specified header name must be treated as case-insensitive. + * This method should return true if the header was successfully deleted, + * and false if the header did not exist. + */ + public function removeHeader(string $name): bool + { + return $this->inner->removeHeader($name); + } + + /** + * Sets the HTTP version. + * + * Should be 1.0, 1.1 or 2.0. + */ + public function setHttpVersion(string $version) + { + $this->inner->setHttpVersion($version); + } + + /** + * Returns the HTTP version. + */ + public function getHttpVersion(): string + { + return $this->inner->getHttpVersion(); + } +} diff --git a/lib/vendor/sabre/http/lib/MessageInterface.php b/lib/vendor/sabre/http/lib/MessageInterface.php new file mode 100644 index 0000000..4531654 --- /dev/null +++ b/lib/vendor/sabre/http/lib/MessageInterface.php @@ -0,0 +1,151 @@ +setMethod($method); + $this->setUrl($url); + $this->setHeaders($headers); + $this->setBody($body); + } + + /** + * Returns the current HTTP method. + */ + public function getMethod(): string + { + return $this->method; + } + + /** + * Sets the HTTP method. + */ + public function setMethod(string $method) + { + $this->method = $method; + } + + /** + * Returns the request url. + */ + public function getUrl(): string + { + return $this->url; + } + + /** + * Sets the request url. + */ + public function setUrl(string $url) + { + $this->url = $url; + } + + /** + * Returns the list of query parameters. + * + * This is equivalent to PHP's $_GET superglobal. + */ + public function getQueryParameters(): array + { + $url = $this->getUrl(); + if (false === ($index = strpos($url, '?'))) { + return []; + } + + parse_str(substr($url, $index + 1), $queryParams); + + return $queryParams; + } + + protected $absoluteUrl; + + /** + * Sets the absolute url. + */ + public function setAbsoluteUrl(string $url) + { + $this->absoluteUrl = $url; + } + + /** + * Returns the absolute url. + */ + public function getAbsoluteUrl(): string + { + if (!$this->absoluteUrl) { + $url = $this->getUrl(); + if (parse_url($url, PHP_URL_SCHEME)) { + // It's already an absolute URL + $this->absoluteUrl = $url; + } else { + $host = $this->getHeader('Host') + ?? parse_url($url, PHP_URL_HOST) + ?? 'localhost'; + // Guessing we're a http endpoint. + $this->absoluteUrl = "http://$host$url"; + } + } + + return $this->absoluteUrl; + } + + /** + * Base url. + * + * @var string + */ + protected $baseUrl = '/'; + + /** + * Sets a base url. + * + * This url is used for relative path calculations. + */ + public function setBaseUrl(string $url) + { + $this->baseUrl = $url; + } + + /** + * Returns the current base url. + */ + public function getBaseUrl(): string + { + return $this->baseUrl; + } + + /** + * Returns the relative path. + * + * This is being calculated using the base url. This path will not start + * with a slash, so it will always return something like + * 'example/path.html'. + * + * If the full path is equal to the base url, this method will return an + * empty string. + * + * This method will also urldecode the path, and if the url was encoded as + * ISO-8859-1, it will convert it to UTF-8. + * + * If the path is outside of the base url, a LogicException will be thrown. + */ + public function getPath(): string + { + // Removing duplicated slashes. + $uri = str_replace('//', '/', $this->getUrl()); + + $uri = Uri\normalize($uri); + $baseUri = Uri\normalize($this->getBaseUrl()); + + if (0 === strpos($uri, $baseUri)) { + // We're not interested in the query part (everything after the ?). + list($uri) = explode('?', $uri); + + return trim(decodePath(substr($uri, strlen($baseUri))), '/'); + } + + if ($uri.'/' === $baseUri) { + return ''; + } + // A special case, if the baseUri was accessed without a trailing + // slash, we'll accept it as well. + + throw new \LogicException('Requested uri ('.$this->getUrl().') is out of base uri ('.$this->getBaseUrl().')'); + } + + /** + * Equivalent of PHP's $_POST. + * + * @var array + */ + protected $postData = []; + + /** + * Sets the post data. + * + * This is equivalent to PHP's $_POST superglobal. + * + * This would not have been needed, if POST data was accessible as + * php://input, but unfortunately we need to special case it. + */ + public function setPostData(array $postData) + { + $this->postData = $postData; + } + + /** + * Returns the POST data. + * + * This is equivalent to PHP's $_POST superglobal. + */ + public function getPostData(): array + { + return $this->postData; + } + + /** + * An array containing the raw _SERVER array. + * + * @var array + */ + protected $rawServerData; + + /** + * Returns an item from the _SERVER array. + * + * If the value does not exist in the array, null is returned. + * + * @return string|null + */ + public function getRawServerValue(string $valueName) + { + return $this->rawServerData[$valueName] ?? null; + } + + /** + * Sets the _SERVER array. + */ + public function setRawServerData(array $data) + { + $this->rawServerData = $data; + } + + /** + * Serializes the request object as a string. + * + * This is useful for debugging purposes. + */ + public function __toString(): string + { + $out = $this->getMethod().' '.$this->getUrl().' HTTP/'.$this->getHttpVersion()."\r\n"; + + foreach ($this->getHeaders() as $key => $value) { + foreach ($value as $v) { + if ('Authorization' === $key) { + list($v) = explode(' ', $v, 2); + $v .= ' REDACTED'; + } + $out .= $key.': '.$v."\r\n"; + } + } + $out .= "\r\n"; + $out .= $this->getBodyAsString(); + + return $out; + } +} diff --git a/lib/vendor/sabre/http/lib/RequestDecorator.php b/lib/vendor/sabre/http/lib/RequestDecorator.php new file mode 100644 index 0000000..23e790e --- /dev/null +++ b/lib/vendor/sabre/http/lib/RequestDecorator.php @@ -0,0 +1,179 @@ +inner = $inner; + } + + /** + * Returns the current HTTP method. + */ + public function getMethod(): string + { + return $this->inner->getMethod(); + } + + /** + * Sets the HTTP method. + */ + public function setMethod(string $method) + { + $this->inner->setMethod($method); + } + + /** + * Returns the request url. + */ + public function getUrl(): string + { + return $this->inner->getUrl(); + } + + /** + * Sets the request url. + */ + public function setUrl(string $url) + { + $this->inner->setUrl($url); + } + + /** + * Returns the absolute url. + */ + public function getAbsoluteUrl(): string + { + return $this->inner->getAbsoluteUrl(); + } + + /** + * Sets the absolute url. + */ + public function setAbsoluteUrl(string $url) + { + $this->inner->setAbsoluteUrl($url); + } + + /** + * Returns the current base url. + */ + public function getBaseUrl(): string + { + return $this->inner->getBaseUrl(); + } + + /** + * Sets a base url. + * + * This url is used for relative path calculations. + * + * The base url should default to / + */ + public function setBaseUrl(string $url) + { + $this->inner->setBaseUrl($url); + } + + /** + * Returns the relative path. + * + * This is being calculated using the base url. This path will not start + * with a slash, so it will always return something like + * 'example/path.html'. + * + * If the full path is equal to the base url, this method will return an + * empty string. + * + * This method will also urldecode the path, and if the url was encoded as + * ISO-8859-1, it will convert it to UTF-8. + * + * If the path is outside of the base url, a LogicException will be thrown. + */ + public function getPath(): string + { + return $this->inner->getPath(); + } + + /** + * Returns the list of query parameters. + * + * This is equivalent to PHP's $_GET superglobal. + */ + public function getQueryParameters(): array + { + return $this->inner->getQueryParameters(); + } + + /** + * Returns the POST data. + * + * This is equivalent to PHP's $_POST superglobal. + */ + public function getPostData(): array + { + return $this->inner->getPostData(); + } + + /** + * Sets the post data. + * + * This is equivalent to PHP's $_POST superglobal. + * + * This would not have been needed, if POST data was accessible as + * php://input, but unfortunately we need to special case it. + */ + public function setPostData(array $postData) + { + $this->inner->setPostData($postData); + } + + /** + * Returns an item from the _SERVER array. + * + * If the value does not exist in the array, null is returned. + * + * @return string|null + */ + public function getRawServerValue(string $valueName) + { + return $this->inner->getRawServerValue($valueName); + } + + /** + * Sets the _SERVER array. + */ + public function setRawServerData(array $data) + { + $this->inner->setRawServerData($data); + } + + /** + * Serializes the request object as a string. + * + * This is useful for debugging purposes. + */ + public function __toString(): string + { + return $this->inner->__toString(); + } +} diff --git a/lib/vendor/sabre/http/lib/RequestInterface.php b/lib/vendor/sabre/http/lib/RequestInterface.php new file mode 100644 index 0000000..5ec7777 --- /dev/null +++ b/lib/vendor/sabre/http/lib/RequestInterface.php @@ -0,0 +1,114 @@ + 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', // RFC 4918 + 208 => 'Already Reported', // RFC 5842 + 226 => 'IM Used', // RFC 3229 + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + 308 => 'Permanent Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + 418 => 'I\'m a teapot', // RFC 2324 + 421 => 'Misdirected Request', // RFC7540 (HTTP/2) + 422 => 'Unprocessable Entity', // RFC 4918 + 423 => 'Locked', // RFC 4918 + 424 => 'Failed Dependency', // RFC 4918 + 426 => 'Upgrade Required', + 428 => 'Precondition Required', // RFC 6585 + 429 => 'Too Many Requests', // RFC 6585 + 431 => 'Request Header Fields Too Large', // RFC 6585 + 451 => 'Unavailable For Legal Reasons', // draft-tbray-http-legally-restricted-status + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version not supported', + 506 => 'Variant Also Negotiates', + 507 => 'Insufficient Storage', // RFC 4918 + 508 => 'Loop Detected', // RFC 5842 + 509 => 'Bandwidth Limit Exceeded', // non-standard + 510 => 'Not extended', + 511 => 'Network Authentication Required', // RFC 6585 + ]; + + /** + * HTTP status code. + * + * @var int + */ + protected $status; + + /** + * HTTP status text. + * + * @var string + */ + protected $statusText; + + /** + * Creates the response object. + * + * @param string|int $status + * @param resource $body + */ + public function __construct($status = 500, ?array $headers = null, $body = null) + { + if (null !== $status) { + $this->setStatus($status); + } + if (null !== $headers) { + $this->setHeaders($headers); + } + if (null !== $body) { + $this->setBody($body); + } + } + + /** + * Returns the current HTTP status code. + */ + public function getStatus(): int + { + return $this->status; + } + + /** + * Returns the human-readable status string. + * + * In the case of a 200, this may for example be 'OK'. + */ + public function getStatusText(): string + { + return $this->statusText; + } + + /** + * Sets the HTTP status code. + * + * This can be either the full HTTP status code with human-readable string, + * for example: "403 I can't let you do that, Dave". + * + * Or just the code, in which case the appropriate default message will be + * added. + * + * @param string|int $status + * + * @throws \InvalidArgumentException + */ + public function setStatus($status) + { + if (is_int($status) || ctype_digit($status)) { + $statusCode = $status; + $statusText = self::$statusCodes[$status] ?? 'Unknown'; + } else { + list( + $statusCode, + $statusText + ) = explode(' ', $status, 2); + $statusCode = (int) $statusCode; + } + if ($statusCode < 100 || $statusCode > 999) { + throw new \InvalidArgumentException('The HTTP status code must be exactly 3 digits'); + } + + $this->status = $statusCode; + $this->statusText = $statusText; + } + + /** + * Serializes the response object as a string. + * + * This is useful for debugging purposes. + */ + public function __toString(): string + { + $str = 'HTTP/'.$this->httpVersion.' '.$this->getStatus().' '.$this->getStatusText()."\r\n"; + foreach ($this->getHeaders() as $key => $value) { + foreach ($value as $v) { + $str .= $key.': '.$v."\r\n"; + } + } + $str .= "\r\n"; + $str .= $this->getBodyAsString(); + + return $str; + } +} diff --git a/lib/vendor/sabre/http/lib/ResponseDecorator.php b/lib/vendor/sabre/http/lib/ResponseDecorator.php new file mode 100644 index 0000000..30b9ec0 --- /dev/null +++ b/lib/vendor/sabre/http/lib/ResponseDecorator.php @@ -0,0 +1,72 @@ +inner = $inner; + } + + /** + * Returns the current HTTP status code. + */ + public function getStatus(): int + { + return $this->inner->getStatus(); + } + + /** + * Returns the human-readable status string. + * + * In the case of a 200, this may for example be 'OK'. + */ + public function getStatusText(): string + { + return $this->inner->getStatusText(); + } + + /** + * Sets the HTTP status code. + * + * This can be either the full HTTP status code with human-readable string, + * for example: "403 I can't let you do that, Dave". + * + * Or just the code, in which case the appropriate default message will be + * added. + * + * @param string|int $status + */ + public function setStatus($status) + { + $this->inner->setStatus($status); + } + + /** + * Serializes the request object as a string. + * + * This is useful for debugging purposes. + */ + public function __toString(): string + { + return $this->inner->__toString(); + } +} diff --git a/lib/vendor/sabre/http/lib/ResponseInterface.php b/lib/vendor/sabre/http/lib/ResponseInterface.php new file mode 100644 index 0000000..fd73d25 --- /dev/null +++ b/lib/vendor/sabre/http/lib/ResponseInterface.php @@ -0,0 +1,42 @@ +setBody(fopen('php://input', 'r')); + $r->setPostData($_POST); + + return $r; + } + + /** + * Sends the HTTP response back to a HTTP client. + * + * This calls php's header() function and streams the body to php://output. + */ + public static function sendResponse(ResponseInterface $response) + { + header('HTTP/'.$response->getHttpVersion().' '.$response->getStatus().' '.$response->getStatusText()); + foreach ($response->getHeaders() as $key => $value) { + foreach ($value as $k => $v) { + if (0 === $k) { + header($key.': '.$v); + } else { + header($key.': '.$v, false); + } + } + } + + $body = $response->getBody(); + if (null === $body) { + return; + } + + if (is_callable($body)) { + $body(); + + return; + } + + $contentLength = $response->getHeader('Content-Length'); + if (null !== $contentLength) { + $output = fopen('php://output', 'wb'); + if (is_resource($body) && 'stream' == get_resource_type($body)) { + // a workaround to make PHP more possible to use mmap based copy, see https://github.com/sabre-io/http/pull/119 + $left = (int) $contentLength; + // copy with 4MiB chunks + $chunk_size = 4 * 1024 * 1024; + stream_set_chunk_size($output, $chunk_size); + // If this is a partial response, flush the beginning bytes until the first position that is a multiple of the page size. + $contentRange = $response->getHeader('Content-Range'); + // Matching "Content-Range: bytes 1234-5678/7890" + if (null !== $contentRange && preg_match('/^bytes\s([0-9]+)-([0-9]+)\//i', $contentRange, $matches)) { + // 4kB should be the default page size on most architectures + $pageSize = 4096; + $offset = (int) $matches[1]; + $delta = ($offset % $pageSize) > 0 ? ($pageSize - $offset % $pageSize) : 0; + if ($delta > 0) { + $left -= stream_copy_to_stream($body, $output, min($delta, $left)); + } + } + while ($left > 0) { + $copied = stream_copy_to_stream($body, $output, min($left, $chunk_size)); + // stream_copy_to_stream($src, $dest, $maxLength) must return the number of bytes copied or false in case of failure + // But when the $maxLength is greater than the total number of bytes remaining in the stream, + // It returns the negative number of bytes copied + // So break the loop in such cases. + if ($copied <= 0) { + break; + } + // Abort on client disconnect. + // With ignore_user_abort(true), the script is not aborted on client disconnect. + // To avoid reading the entire stream and dismissing the data afterward, check between the chunks if the client is still there. + if (1 === ignore_user_abort() && 1 === connection_aborted()) { + break; + } + $left -= $copied; + } + } else { + fwrite($output, $body, (int) $contentLength); + } + } else { + file_put_contents('php://output', $body); + } + + if (is_resource($body)) { + fclose($body); + } + } + + /** + * This static method will create a new Request object, based on a PHP + * $_SERVER array. + * + * REQUEST_URI and REQUEST_METHOD are required. + */ + public static function createFromServerArray(array $serverArray): Request + { + $headers = []; + $method = null; + $url = null; + $httpVersion = '1.1'; + + $protocol = 'http'; + $hostName = 'localhost'; + + foreach ($serverArray as $key => $value) { + $key = (string) $key; + switch ($key) { + case 'SERVER_PROTOCOL': + if ('HTTP/1.0' === $value) { + $httpVersion = '1.0'; + } elseif ('HTTP/2.0' === $value) { + $httpVersion = '2.0'; + } + break; + case 'REQUEST_METHOD': + $method = $value; + break; + case 'REQUEST_URI': + $url = $value; + break; + + // These sometimes show up without a HTTP_ prefix + case 'CONTENT_TYPE': + $headers['Content-Type'] = $value; + break; + case 'CONTENT_LENGTH': + $headers['Content-Length'] = $value; + break; + + // mod_php on apache will put credentials in these variables. + // (fast)cgi does not usually do this, however. + case 'PHP_AUTH_USER': + if (isset($serverArray['PHP_AUTH_PW'])) { + $headers['Authorization'] = 'Basic '.base64_encode($value.':'.$serverArray['PHP_AUTH_PW']); + } + break; + + // Similarly, mod_php may also screw around with digest auth. + case 'PHP_AUTH_DIGEST': + $headers['Authorization'] = 'Digest '.$value; + break; + + // Apache may prefix the HTTP_AUTHORIZATION header with + // REDIRECT_, if mod_rewrite was used. + case 'REDIRECT_HTTP_AUTHORIZATION': + $headers['Authorization'] = $value; + break; + + case 'HTTP_HOST': + $hostName = $value; + $headers['Host'] = $value; + break; + + case 'HTTPS': + if (!empty($value) && 'off' !== $value) { + $protocol = 'https'; + } + break; + + default: + if ('HTTP_' === substr($key, 0, 5)) { + // It's a HTTP header + + // Normalizing it to be prettier + $header = strtolower(substr($key, 5)); + + // Transforming dashes into spaces, and upper-casing + // every first letter. + $header = ucwords(str_replace('_', ' ', $header)); + + // Turning spaces into dashes. + $header = str_replace(' ', '-', $header); + $headers[$header] = $value; + } + break; + } + } + + if (null === $url) { + throw new \InvalidArgumentException('The _SERVER array must have a REQUEST_URI key'); + } + + if (null === $method) { + throw new \InvalidArgumentException('The _SERVER array must have a REQUEST_METHOD key'); + } + $r = new Request($method, $url, $headers); + $r->setHttpVersion($httpVersion); + $r->setRawServerData($serverArray); + $r->setAbsoluteUrl($protocol.'://'.$hostName.$url); + + return $r; + } +} diff --git a/lib/vendor/sabre/http/lib/Version.php b/lib/vendor/sabre/http/lib/Version.php new file mode 100644 index 0000000..84c877b --- /dev/null +++ b/lib/vendor/sabre/http/lib/Version.php @@ -0,0 +1,20 @@ +setTimezone(new \DateTimeZone('GMT')); + + return $dateTime->format('D, d M Y H:i:s \G\M\T'); +} + +/** + * This function can be used to aid with content negotiation. + * + * It takes 2 arguments, the $acceptHeaderValue, which usually comes from + * an Accept header, and $availableOptions, which contains an array of + * items that the server can support. + * + * The result of this function will be the 'best possible option'. If no + * best possible option could be found, null is returned. + * + * When it's null you can according to the spec either return a default, or + * you can choose to emit 406 Not Acceptable. + * + * The method also accepts sending 'null' for the $acceptHeaderValue, + * implying that no accept header was sent. + * + * @param string|null $acceptHeaderValue + * + * @return string|null + */ +function negotiateContentType($acceptHeaderValue, array $availableOptions) +{ + if (!$acceptHeaderValue) { + // Grabbing the first in the list. + return reset($availableOptions); + } + + $proposals = array_map( + 'Sabre\HTTP\parseMimeType', + explode(',', $acceptHeaderValue) + ); + + // Ensuring array keys are reset. + $availableOptions = array_values($availableOptions); + + $options = array_map( + 'Sabre\HTTP\parseMimeType', + $availableOptions + ); + + $lastQuality = 0; + $lastSpecificity = 0; + $lastOptionIndex = 0; + $lastChoice = null; + + foreach ($proposals as $proposal) { + // Ignoring broken values. + if (null === $proposal) { + continue; + } + + // If the quality is lower we don't have to bother comparing. + if ($proposal['quality'] < $lastQuality) { + continue; + } + + foreach ($options as $optionIndex => $option) { + if ('*' !== $proposal['type'] && $proposal['type'] !== $option['type']) { + // no match on type. + continue; + } + if ('*' !== $proposal['subType'] && $proposal['subType'] !== $option['subType']) { + // no match on subtype. + continue; + } + + // Any parameters appearing on the options must appear on + // proposals. + foreach ($option['parameters'] as $paramName => $paramValue) { + if (!array_key_exists($paramName, $proposal['parameters'])) { + continue 2; + } + if ($paramValue !== $proposal['parameters'][$paramName]) { + continue 2; + } + } + + // If we got here, we have a match on parameters, type and + // subtype. We need to calculate a score for how specific the + // match was. + $specificity = + ('*' !== $proposal['type'] ? 20 : 0) + + ('*' !== $proposal['subType'] ? 10 : 0) + + count($option['parameters']); + + // Does this entry win? + if ( + ($proposal['quality'] > $lastQuality) + || ($proposal['quality'] === $lastQuality && $specificity > $lastSpecificity) + || ($proposal['quality'] === $lastQuality && $specificity === $lastSpecificity && $optionIndex < $lastOptionIndex) + ) { + $lastQuality = $proposal['quality']; + $lastSpecificity = $specificity; + $lastOptionIndex = $optionIndex; + $lastChoice = $availableOptions[$optionIndex]; + } + } + } + + return $lastChoice; +} + +/** + * Parses the Prefer header, as defined in RFC7240. + * + * Input can be given as a single header value (string) or multiple headers + * (array of string). + * + * This method will return a key->value array with the various Prefer + * parameters. + * + * Prefer: return=minimal will result in: + * + * [ 'return' => 'minimal' ] + * + * Prefer: foo, wait=10 will result in: + * + * [ 'foo' => true, 'wait' => '10'] + * + * This method also supports the formats from older drafts of RFC7240, and + * it will automatically map them to the new values, as the older values + * are still pretty common. + * + * Parameters are currently discarded. There's no known prefer value that + * uses them. + * + * @param string|string[] $input + */ +function parsePrefer($input): array +{ + $token = '[!#$%&\'*+\-.^_`~A-Za-z0-9]+'; + + // Work in progress + $word = '(?: [a-zA-Z0-9]+ | "[a-zA-Z0-9]*" )'; + + $regex = << $token) # Prefer property name +\s* # Optional space +(?: = \s* # Prefer property value + (? $word) +)? +(?: \s* ; (?: .*))? # Prefer parameters (ignored) +$ +/x +REGEX; + + $output = []; + foreach (getHeaderValues($input) as $value) { + if (!preg_match($regex, $value, $matches)) { + // Ignore + continue; + } + + // Mapping old values to their new counterparts + switch ($matches['name']) { + case 'return-asynch': + $output['respond-async'] = true; + break; + case 'return-representation': + $output['return'] = 'representation'; + break; + case 'return-minimal': + $output['return'] = 'minimal'; + break; + case 'strict': + $output['handling'] = 'strict'; + break; + case 'lenient': + $output['handling'] = 'lenient'; + break; + default: + if (isset($matches['value'])) { + $value = trim($matches['value'], '"'); + } else { + $value = true; + } + $output[strtolower($matches['name'])] = empty($value) ? true : $value; + break; + } + } + + return $output; +} + +/** + * This method splits up headers into all their individual values. + * + * A HTTP header may have more than one header, such as this: + * Cache-Control: private, no-store + * + * Header values are always split with a comma. + * + * You can pass either a string, or an array. The resulting value is always + * an array with each spliced value. + * + * If the second headers argument is set, this value will simply be merged + * in. This makes it quicker to merge an old list of values with a new set. + * + * @param string|string[] $values + * @param string|string[] $values2 + */ +function getHeaderValues($values, $values2 = null): array +{ + $values = (array) $values; + if ($values2) { + $values = array_merge($values, (array) $values2); + } + + $result = []; + foreach ($values as $l1) { + foreach (explode(',', $l1) as $l2) { + $result[] = trim($l2); + } + } + + return $result; +} + +/** + * Parses a mime-type and splits it into:. + * + * 1. type + * 2. subtype + * 3. quality + * 4. parameters + */ +function parseMimeType(string $str): array +{ + $parameters = []; + // If no q= parameter appears, then quality = 1. + $quality = 1; + + $parts = explode(';', $str); + + // The first part is the mime-type. + $mimeType = trim(array_shift($parts)); + + if ('*' === $mimeType) { + $mimeType = '*/*'; + } + + $mimeType = explode('/', $mimeType); + if (2 !== count($mimeType)) { + // Illegal value + var_dump($mimeType); + exit; + // throw new InvalidArgumentException('Not a valid mime-type: '.$str); + } + list($type, $subType) = $mimeType; + + foreach ($parts as $part) { + $part = trim($part); + if (strpos($part, '=')) { + list($partName, $partValue) = + explode('=', $part, 2); + } else { + $partName = $part; + $partValue = null; + } + + // The quality parameter, if it appears, also marks the end of + // the parameter list. Anything after the q= counts as an + // 'accept extension' and could introduce new semantics in + // content-negotiation. + if ('q' !== $partName) { + $parameters[$partName] = $part; + } else { + $quality = (float) $partValue; + break; // Stop parsing parts + } + } + + return [ + 'type' => $type, + 'subType' => $subType, + 'quality' => $quality, + 'parameters' => $parameters, + ]; +} + +/** + * Encodes the path of a url. + * + * slashes (/) are treated as path-separators. + */ +function encodePath(string $path): string +{ + return preg_replace_callback('/([^A-Za-z0-9_\-\.~\(\)\/:@])/', function ($match) { + return '%'.sprintf('%02x', ord($match[0])); + }, $path); +} + +/** + * Encodes a 1 segment of a path. + * + * Slashes are considered part of the name, and are encoded as %2f + */ +function encodePathSegment(string $pathSegment): string +{ + return preg_replace_callback('/([^A-Za-z0-9_\-\.~\(\):@])/', function ($match) { + return '%'.sprintf('%02x', ord($match[0])); + }, $pathSegment); +} + +/** + * Decodes a url-encoded path. + */ +function decodePath(string $path): string +{ + return decodePathSegment($path); +} + +/** + * Decodes a url-encoded path segment. + */ +function decodePathSegment(string $path): string +{ + $path = rawurldecode($path); + + if (!mb_check_encoding($path, 'UTF-8') && mb_check_encoding($path, 'ISO-8859-1')) { + $path = mb_convert_encoding($path, 'UTF-8', 'ISO-8859-1'); + } + + return $path; +} diff --git a/lib/vendor/sabre/http/phpstan.neon b/lib/vendor/sabre/http/phpstan.neon new file mode 100644 index 0000000..213da6d --- /dev/null +++ b/lib/vendor/sabre/http/phpstan.neon @@ -0,0 +1,2 @@ +parameters: + level: 1 diff --git a/lib/vendor/sabre/http/tests/HTTP/Auth/AWSTest.php b/lib/vendor/sabre/http/tests/HTTP/Auth/AWSTest.php new file mode 100644 index 0000000..a019adc --- /dev/null +++ b/lib/vendor/sabre/http/tests/HTTP/Auth/AWSTest.php @@ -0,0 +1,236 @@ +response = new Response(); + $this->request = new Request('GET', '/'); + $this->auth = new AWS(self::REALM, $this->request, $this->response); + } + + public function testNoHeader() + { + $this->request->setMethod('GET'); + $result = $this->auth->init(); + + $this->assertFalse($result, 'No AWS Authorization header was supplied, so we should have gotten false'); + $this->assertEquals(AWS::ERR_NOAWSHEADER, $this->auth->errorCode); + } + + public function testInvalidAuthorizationHeader() + { + $this->request->setMethod('GET'); + $this->request->setHeader('Authorization', 'Invalid Auth Header'); + + $this->assertFalse($this->auth->init(), 'The Invalid AWS authorization header'); + } + + public function testIncorrectContentMD5() + { + $accessKey = 'accessKey'; + $secretKey = 'secretKey'; + + $this->request->setMethod('GET'); + $this->request->setHeaders([ + 'Authorization' => "AWS $accessKey:sig", + 'Content-MD5' => 'garbage', + ]); + $this->request->setUrl('/'); + + $this->auth->init(); + $result = $this->auth->validate($secretKey); + + $this->assertFalse($result); + $this->assertEquals(AWS::ERR_MD5CHECKSUMWRONG, $this->auth->errorCode); + } + + public function testNoDate() + { + $accessKey = 'accessKey'; + $secretKey = 'secretKey'; + $content = 'thisisthebody'; + $contentMD5 = base64_encode(md5($content, true)); + + $this->request->setMethod('POST'); + $this->request->setHeaders([ + 'Authorization' => "AWS $accessKey:sig", + 'Content-MD5' => $contentMD5, + ]); + $this->request->setUrl('/'); + $this->request->setBody($content); + + $this->auth->init(); + $result = $this->auth->validate($secretKey); + + $this->assertFalse($result); + $this->assertEquals(AWS::ERR_INVALIDDATEFORMAT, $this->auth->errorCode); + } + + public function testFutureDate() + { + $accessKey = 'accessKey'; + $secretKey = 'secretKey'; + $content = 'thisisthebody'; + $contentMD5 = base64_encode(md5($content, true)); + + $date = new \DateTime('@'.(time() + (60 * 20))); + $date->setTimeZone(new \DateTimeZone('GMT')); + $date = $date->format('D, d M Y H:i:s \\G\\M\\T'); + + $this->request->setMethod('POST'); + $this->request->setHeaders([ + 'Authorization' => "AWS $accessKey:sig", + 'Content-MD5' => $contentMD5, + 'Date' => $date, + ]); + + $this->request->setBody($content); + + $this->auth->init(); + $result = $this->auth->validate($secretKey); + + $this->assertFalse($result); + $this->assertEquals(AWS::ERR_REQUESTTIMESKEWED, $this->auth->errorCode); + } + + public function testPastDate() + { + $accessKey = 'accessKey'; + $secretKey = 'secretKey'; + $content = 'thisisthebody'; + $contentMD5 = base64_encode(md5($content, true)); + + $date = new \DateTime('@'.(time() - (60 * 20))); + $date->setTimeZone(new \DateTimeZone('GMT')); + $date = $date->format('D, d M Y H:i:s \\G\\M\\T'); + + $this->request->setMethod('POST'); + $this->request->setHeaders([ + 'Authorization' => "AWS $accessKey:sig", + 'Content-MD5' => $contentMD5, + 'Date' => $date, + ]); + + $this->request->setBody($content); + + $this->auth->init(); + $result = $this->auth->validate($secretKey); + + $this->assertFalse($result); + $this->assertEquals(AWS::ERR_REQUESTTIMESKEWED, $this->auth->errorCode); + } + + public function testIncorrectSignature() + { + $accessKey = 'accessKey'; + $secretKey = 'secretKey'; + $content = 'thisisthebody'; + + $contentMD5 = base64_encode(md5($content, true)); + + $date = new \DateTime('now'); + $date->setTimeZone(new \DateTimeZone('GMT')); + $date = $date->format('D, d M Y H:i:s \\G\\M\\T'); + + $this->request->setUrl('/'); + $this->request->setMethod('POST'); + $this->request->setHeaders([ + 'Authorization' => "AWS $accessKey:sig", + 'Content-MD5' => $contentMD5, + 'X-amz-date' => $date, + ]); + $this->request->setBody($content); + + $this->auth->init(); + $result = $this->auth->validate($secretKey); + + $this->assertFalse($result); + $this->assertEquals(AWS::ERR_INVALIDSIGNATURE, $this->auth->errorCode); + } + + public function testValidRequest() + { + $accessKey = 'accessKey'; + $secretKey = 'secretKey'; + $content = 'thisisthebody'; + $contentMD5 = base64_encode(md5($content, true)); + + $date = new \DateTime('now'); + $date->setTimeZone(new \DateTimeZone('GMT')); + $date = $date->format('D, d M Y H:i:s \\G\\M\\T'); + + $sig = base64_encode($this->hmacsha1($secretKey, + "POST\n$contentMD5\n\n$date\nx-amz-date:$date\n/evert" + )); + + $this->request->setUrl('/evert'); + $this->request->setMethod('POST'); + $this->request->setHeaders([ + 'Authorization' => "AWS $accessKey:$sig", + 'Content-MD5' => $contentMD5, + 'X-amz-date' => $date, + ]); + + $this->request->setBody($content); + + $this->auth->init(); + $result = $this->auth->validate($secretKey); + + $this->assertTrue($result, 'Signature did not validate, got errorcode '.$this->auth->errorCode); + $this->assertEquals($accessKey, $this->auth->getAccessKey()); + } + + public function test401() + { + $this->auth->requireLogin(); + $test = preg_match('/^AWS$/', $this->response->getHeader('WWW-Authenticate'), $matches); + $this->assertTrue(true == $test, 'The WWW-Authenticate response didn\'t match our pattern'); + } + + /** + * Generates an HMAC-SHA1 signature. + * + * @param string $key + * @param string $message + * + * @return string + */ + private function hmacsha1($key, $message) + { + $blocksize = 64; + if (strlen($key) > $blocksize) { + $key = pack('H*', sha1($key)); + } + $key = str_pad($key, $blocksize, chr(0x00)); + $ipad = str_repeat(chr(0x36), $blocksize); + $opad = str_repeat(chr(0x5C), $blocksize); + $hmac = pack('H*', sha1(($key ^ $opad).pack('H*', sha1(($key ^ $ipad).$message)))); + + return $hmac; + } +} diff --git a/lib/vendor/sabre/http/tests/HTTP/Auth/BasicTest.php b/lib/vendor/sabre/http/tests/HTTP/Auth/BasicTest.php new file mode 100644 index 0000000..ef333eb --- /dev/null +++ b/lib/vendor/sabre/http/tests/HTTP/Auth/BasicTest.php @@ -0,0 +1,67 @@ + 'Basic '.base64_encode('user:pass:bla'), + ]); + + $basic = new Basic('Dagger', $request, new Response()); + + $this->assertEquals([ + 'user', + 'pass:bla', + ], $basic->getCredentials()); + } + + public function testGetInvalidCredentialsColonMissing() + { + $request = new Request('GET', '/', [ + 'Authorization' => 'Basic '.base64_encode('userpass'), + ]); + + $basic = new Basic('Dagger', $request, new Response()); + + $this->assertNull($basic->getCredentials()); + } + + public function testGetCredentialsNoHeader() + { + $request = new Request('GET', '/', []); + $basic = new Basic('Dagger', $request, new Response()); + + $this->assertNull($basic->getCredentials()); + } + + public function testGetCredentialsNotBasic() + { + $request = new Request('GET', '/', [ + 'Authorization' => 'QBasic '.base64_encode('user:pass:bla'), + ]); + $basic = new Basic('Dagger', $request, new Response()); + + $this->assertNull($basic->getCredentials()); + } + + public function testRequireLogin() + { + $response = new Response(); + $request = new Request('GET', '/'); + + $basic = new Basic('Dagger', $request, $response); + + $basic->requireLogin(); + + $this->assertEquals('Basic realm="Dagger", charset="UTF-8"', $response->getHeader('WWW-Authenticate')); + $this->assertEquals(401, $response->getStatus()); + } +} diff --git a/lib/vendor/sabre/http/tests/HTTP/Auth/BearerTest.php b/lib/vendor/sabre/http/tests/HTTP/Auth/BearerTest.php new file mode 100644 index 0000000..82e18d2 --- /dev/null +++ b/lib/vendor/sabre/http/tests/HTTP/Auth/BearerTest.php @@ -0,0 +1,55 @@ + 'Bearer 12345', + ]); + + $bearer = new Bearer('Dagger', $request, new Response()); + + $this->assertEquals( + '12345', + $bearer->getToken() + ); + } + + public function testGetCredentialsNoHeader() + { + $request = new Request('GET', '/', []); + $bearer = new Bearer('Dagger', $request, new Response()); + + $this->assertNull($bearer->getToken()); + } + + public function testGetCredentialsNotBearer() + { + $request = new Request('GET', '/', [ + 'Authorization' => 'QBearer 12345', + ]); + $bearer = new Bearer('Dagger', $request, new Response()); + + $this->assertNull($bearer->getToken()); + } + + public function testRequireLogin() + { + $response = new Response(); + $request = new Request('GET', '/'); + $bearer = new Bearer('Dagger', $request, $response); + + $bearer->requireLogin(); + + $this->assertEquals('Bearer realm="Dagger"', $response->getHeader('WWW-Authenticate')); + $this->assertEquals(401, $response->getStatus()); + } +} diff --git a/lib/vendor/sabre/http/tests/HTTP/Auth/DigestTest.php b/lib/vendor/sabre/http/tests/HTTP/Auth/DigestTest.php new file mode 100644 index 0000000..f3be054 --- /dev/null +++ b/lib/vendor/sabre/http/tests/HTTP/Auth/DigestTest.php @@ -0,0 +1,185 @@ +response = new Response(); + $this->request = new Request('GET', '/'); + $this->auth = new Digest(self::REALM, $this->request, $this->response); + } + + public function testDigest() + { + list($nonce, $opaque) = $this->getServerTokens(); + + $username = 'admin'; + $password = '12345'; + $nc = '00002'; + $cnonce = uniqid(); + + $digestHash = md5( + md5($username.':'.self::REALM.':'.$password).':'. + $nonce.':'. + $nc.':'. + $cnonce.':'. + 'auth:'. + md5('GET:/') + ); + + $this->request->setMethod('GET'); + $this->request->setHeader('Authorization', 'Digest username="'.$username.'", realm="'.self::REALM.'", nonce="'.$nonce.'", uri="/", response="'.$digestHash.'", opaque="'.$opaque.'", qop=auth,nc='.$nc.',cnonce="'.$cnonce.'"'); + + $this->auth->init(); + + $this->assertEquals($username, $this->auth->getUsername()); + $this->assertEquals(self::REALM, $this->auth->getRealm()); + $this->assertTrue($this->auth->validateA1(md5($username.':'.self::REALM.':'.$password)), 'Authentication is deemed invalid through validateA1'); + $this->assertTrue($this->auth->validatePassword($password), 'Authentication is deemed invalid through validatePassword'); + } + + public function testInvalidDigest() + { + list($nonce, $opaque) = $this->getServerTokens(); + + $username = 'admin'; + $password = 12345; + $nc = '00002'; + $cnonce = uniqid(); + + $digestHash = md5( + md5($username.':'.self::REALM.':'.$password).':'. + $nonce.':'. + $nc.':'. + $cnonce.':'. + 'auth:'. + md5('GET:/') + ); + + $this->request->setMethod('GET'); + $this->request->setHeader('Authorization', 'Digest username="'.$username.'", realm="'.self::REALM.'", nonce="'.$nonce.'", uri="/", response="'.$digestHash.'", opaque="'.$opaque.'", qop=auth,nc='.$nc.',cnonce="'.$cnonce.'"'); + + $this->auth->init(); + + $this->assertFalse($this->auth->validateA1(md5($username.':'.self::REALM.':'.($password.'randomness'))), 'Authentication is deemed invalid through validateA1'); + } + + public function testInvalidDigest2() + { + $this->request->setMethod('GET'); + $this->request->setHeader('Authorization', 'basic blablabla'); + + $this->auth->init(); + $this->assertFalse($this->auth->validateA1(md5('user:realm:password'))); + } + + public function testDigestAuthInt() + { + $this->auth->setQOP(Digest::QOP_AUTHINT); + list($nonce, $opaque) = $this->getServerTokens(Digest::QOP_AUTHINT); + + $username = 'admin'; + $password = 12345; + $nc = '00003'; + $cnonce = uniqid(); + + $digestHash = md5( + md5($username.':'.self::REALM.':'.$password).':'. + $nonce.':'. + $nc.':'. + $cnonce.':'. + 'auth-int:'. + md5('POST:/:'.md5('body')) + ); + + $this->request->setMethod('POST'); + $this->request->setHeader('Authorization', 'Digest username="'.$username.'", realm="'.self::REALM.'", nonce="'.$nonce.'", uri="/", response="'.$digestHash.'", opaque="'.$opaque.'", qop=auth-int,nc='.$nc.',cnonce="'.$cnonce.'"'); + $this->request->setBody('body'); + + $this->auth->init(); + + $this->assertTrue($this->auth->validateA1(md5($username.':'.self::REALM.':'.$password)), 'Authentication is deemed invalid through validateA1'); + } + + public function testDigestAuthBoth() + { + $this->auth->setQOP(Digest::QOP_AUTHINT | Digest::QOP_AUTH); + list($nonce, $opaque) = $this->getServerTokens(Digest::QOP_AUTHINT | Digest::QOP_AUTH); + + $username = 'admin'; + $password = 12345; + $nc = '00003'; + $cnonce = uniqid(); + + $digestHash = md5( + md5($username.':'.self::REALM.':'.$password).':'. + $nonce.':'. + $nc.':'. + $cnonce.':'. + 'auth-int:'. + md5('POST:/:'.md5('body')) + ); + + $this->request->setMethod('POST'); + $this->request->setHeader('Authorization', 'Digest username="'.$username.'", realm="'.self::REALM.'", nonce="'.$nonce.'", uri="/", response="'.$digestHash.'", opaque="'.$opaque.'", qop=auth-int,nc='.$nc.',cnonce="'.$cnonce.'"'); + $this->request->setBody('body'); + + $this->auth->init(); + + $this->assertTrue($this->auth->validateA1(md5($username.':'.self::REALM.':'.$password)), 'Authentication is deemed invalid through validateA1'); + } + + private function getServerTokens($qop = Digest::QOP_AUTH) + { + $this->auth->requireLogin(); + + switch ($qop) { + case Digest::QOP_AUTH: $qopstr = 'auth'; + break; + case Digest::QOP_AUTHINT: $qopstr = 'auth-int'; + break; + default: $qopstr = 'auth,auth-int'; + break; + } + + $test = preg_match('/Digest realm="'.self::REALM.'",qop="'.$qopstr.'",nonce="([0-9a-f]*)",opaque="([0-9a-f]*)"/', + $this->response->getHeader('WWW-Authenticate'), $matches); + + $this->assertTrue(true == $test, 'The WWW-Authenticate response didn\'t match our pattern. We received: '.$this->response->getHeader('WWW-Authenticate')); + + $nonce = $matches[1]; + $opaque = $matches[2]; + + // Reset our environment + $this->setUp(); + $this->auth->setQOP($qop); + + return [$nonce, $opaque]; + } +} diff --git a/lib/vendor/sabre/http/tests/HTTP/ClientTest.php b/lib/vendor/sabre/http/tests/HTTP/ClientTest.php new file mode 100644 index 0000000..8e7efef --- /dev/null +++ b/lib/vendor/sabre/http/tests/HTTP/ClientTest.php @@ -0,0 +1,605 @@ +addCurlSetting(CURLOPT_POSTREDIR, 0); + + $request = new Request('GET', 'http://example.org/', ['X-Foo' => 'bar']); + + $settings = [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADER => true, + CURLOPT_POSTREDIR => 0, + CURLOPT_HTTPHEADER => ['X-Foo: bar'], + CURLOPT_NOBODY => false, + CURLOPT_URL => 'http://example.org/', + CURLOPT_CUSTOMREQUEST => 'GET', + CURLOPT_USERAGENT => 'sabre-http/'.Version::VERSION.' (http://sabre.io/)', + ]; + + // FIXME: CURLOPT_PROTOCOLS and CURLOPT_REDIR_PROTOCOLS are currently unsupported by HHVM + // at least if this unit test fails in the future we know it is :) + if (false === defined('HHVM_VERSION')) { + $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + } + + $this->assertEquals($settings, $client->createCurlSettingsArray($request)); + } + + public function testCreateCurlSettingsHTTPHeader(): void + { + $client = new ClientMock(); + $header = [ + 'Authorization: Bearer 12345', + ]; + $client->addCurlSetting(CURLOPT_POSTREDIR, 0); + $client->addCurlSetting(CURLOPT_HTTPHEADER, $header); + + $request = new Request('GET', 'http://example.org/'); + + $settings = [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADER => true, + CURLOPT_POSTREDIR => 0, + CURLOPT_HTTPHEADER => ['Authorization: Bearer 12345'], + CURLOPT_NOBODY => false, + CURLOPT_URL => 'http://example.org/', + CURLOPT_CUSTOMREQUEST => 'GET', + CURLOPT_USERAGENT => 'sabre-http/'.Version::VERSION.' (http://sabre.io/)', + CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS, + CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS, + ]; + + self::assertEquals($settings, $client->createCurlSettingsArray($request)); + } + + public function testCreateCurlSettingsArrayHEAD() + { + $client = new ClientMock(); + $request = new Request('HEAD', 'http://example.org/', ['X-Foo' => 'bar']); + + $settings = [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADER => true, + CURLOPT_NOBODY => true, + CURLOPT_CUSTOMREQUEST => 'HEAD', + CURLOPT_HTTPHEADER => ['X-Foo: bar'], + CURLOPT_URL => 'http://example.org/', + CURLOPT_USERAGENT => 'sabre-http/'.Version::VERSION.' (http://sabre.io/)', + ]; + + // FIXME: CURLOPT_PROTOCOLS and CURLOPT_REDIR_PROTOCOLS are currently unsupported by HHVM + // at least if this unit test fails in the future we know it is :) + if (false === defined('HHVM_VERSION')) { + $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + } + + $this->assertEquals($settings, $client->createCurlSettingsArray($request)); + } + + public function testCreateCurlSettingsArrayGETAfterHEAD() + { + $client = new ClientMock(); + $request = new Request('HEAD', 'http://example.org/', ['X-Foo' => 'bar']); + + // Parsing the settings for this method, and discarding the result. + // This will cause the client to automatically persist previous + // settings and will help us detect problems. + $client->createCurlSettingsArray($request); + + // This is the real request. + $request = new Request('GET', 'http://example.org/', ['X-Foo' => 'bar']); + + $settings = [ + CURLOPT_CUSTOMREQUEST => 'GET', + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADER => true, + CURLOPT_HTTPHEADER => ['X-Foo: bar'], + CURLOPT_NOBODY => false, + CURLOPT_URL => 'http://example.org/', + CURLOPT_USERAGENT => 'sabre-http/'.Version::VERSION.' (http://sabre.io/)', + ]; + + // FIXME: CURLOPT_PROTOCOLS and CURLOPT_REDIR_PROTOCOLS are currently unsupported by HHVM + // at least if this unit test fails in the future we know it is :) + if (false === defined('HHVM_VERSION')) { + $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + } + + $this->assertEquals($settings, $client->createCurlSettingsArray($request)); + } + + public function testCreateCurlSettingsArrayPUTStream() + { + $client = new ClientMock(); + + $fileContent = 'booh'; + $h = fopen('php://memory', 'r+'); + fwrite($h, $fileContent); + $request = new Request('PUT', 'http://example.org/', ['X-Foo' => 'bar'], $h); + + $settings = [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADER => true, + CURLOPT_PUT => true, + CURLOPT_INFILE => $h, + CURLOPT_INFILESIZE => strlen($fileContent), + CURLOPT_NOBODY => false, + CURLOPT_CUSTOMREQUEST => 'PUT', + CURLOPT_HTTPHEADER => ['X-Foo: bar'], + CURLOPT_URL => 'http://example.org/', + CURLOPT_USERAGENT => 'sabre-http/'.Version::VERSION.' (http://sabre.io/)', + ]; + + // FIXME: CURLOPT_PROTOCOLS and CURLOPT_REDIR_PROTOCOLS are currently unsupported by HHVM + // at least if this unit test fails in the future we know it is :) + if (false === defined('HHVM_VERSION')) { + $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + } + + $this->assertEquals($settings, $client->createCurlSettingsArray($request)); + } + + public function testCreateCurlSettingsArrayPUTString() + { + $client = new ClientMock(); + $request = new Request('PUT', 'http://example.org/', ['X-Foo' => 'bar'], 'boo'); + + $settings = [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADER => true, + CURLOPT_NOBODY => false, + CURLOPT_POSTFIELDS => 'boo', + CURLOPT_CUSTOMREQUEST => 'PUT', + CURLOPT_HTTPHEADER => ['X-Foo: bar'], + CURLOPT_URL => 'http://example.org/', + CURLOPT_USERAGENT => 'sabre-http/'.Version::VERSION.' (http://sabre.io/)', + ]; + + // FIXME: CURLOPT_PROTOCOLS and CURLOPT_REDIR_PROTOCOLS are currently unsupported by HHVM + // at least if this unit test fails in the future we know it is :) + if (false === defined('HHVM_VERSION')) { + $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + } + + $this->assertEquals($settings, $client->createCurlSettingsArray($request)); + } + + public function testIssue89MultiplePutInfileGivesWarning() + { + $client = new ClientMock(); + $tmpFile = tmpfile(); + $request = new Request('POST', 'http://example.org/', ['X-Foo' => 'bar'], 'body'); + + $settings = $client->createCurlSettingsArray($request); + $this->assertArrayNotHasKey(CURLOPT_PUT, $settings); + $this->assertArrayNotHasKey(CURLOPT_INFILE, $settings); + + $request = new Request('POST', 'http://example.org/', ['X-Foo' => 'bar'], $tmpFile); + + $settings = $client->createCurlSettingsArray($request); + $this->assertEquals(true, $settings[CURLOPT_PUT]); + $this->assertEquals($tmpFile, $settings[CURLOPT_INFILE]); + + $request = new Request('POST', 'http://example.org/', ['X-Foo' => 'bar'], 'body'); + + $settings = $client->createCurlSettingsArray($request); + $this->assertArrayNotHasKey(CURLOPT_PUT, $settings); + $this->assertArrayNotHasKey(CURLOPT_INFILE, $settings); + } + + public function testSend() + { + $client = new ClientMock(); + $request = new Request('GET', 'http://example.org/'); + + $client->on('doRequest', function ($request, &$response) { + $response = new Response(200); + }); + + $response = $client->send($request); + + $this->assertEquals(200, $response->getStatus()); + } + + protected function getAbsoluteUrl($path) + { + $baseUrl = getenv('BASEURL'); + if ($baseUrl) { + $path = ltrim($path, '/'); + + return "$baseUrl/$path"; + } + + return false; + } + + /** + * @group ci + */ + public function testSendToGetLargeContent() + { + $url = $this->getAbsoluteUrl('/large.php'); + if (!$url) { + $this->markTestSkipped('Set an environment value BASEURL to continue'); + } + + // Allow the peak memory usage limit to be specified externally, if needed. + // When running this test in different environments it may be appropriate to set a different limit. + $maxPeakMemoryUsageEnvVariable = 'SABRE_HTTP_TEST_GET_LARGE_CONTENT_MAX_PEAK_MEMORY_USAGE'; + $maxPeakMemoryUsage = \getenv($maxPeakMemoryUsageEnvVariable); + if (false === $maxPeakMemoryUsage) { + $maxPeakMemoryUsage = 60 * pow(1024, 2); + } + + $request = new Request('GET', $url); + $client = new Client(); + $response = $client->send($request); + + $this->assertEquals(200, $response->getStatus()); + $this->assertLessThan( + (int) $maxPeakMemoryUsage, + memory_get_peak_usage(), + "Hint: you can adjust the max peak memory usage allowed for this test by defining env variable $maxPeakMemoryUsageEnvVariable to be the desired max bytes" + ); + } + + /** + * @group ci + */ + public function testSendAsync() + { + $url = $this->getAbsoluteUrl('/foo'); + if (!$url) { + $this->markTestSkipped('Set an environment value BASEURL to continue'); + } + + $client = new Client(); + + $request = new Request('GET', $url); + $client->sendAsync($request, function (ResponseInterface $response) { + $this->assertEquals("foo\n", $response->getBody()); + $this->assertEquals(200, $response->getStatus()); + $this->assertEquals(4, $response->getHeader('Content-Length')); + }, function ($error) use ($request) { + $url = $request->getUrl(); + $this->fail("Failed to GET $url"); + }); + + $client->wait(); + } + + /** + * @group ci + */ + public function testSendAsynConsecutively() + { + $url = $this->getAbsoluteUrl('/foo'); + if (!$url) { + $this->markTestSkipped('Set an environment value BASEURL to continue'); + } + + $client = new Client(); + + $request = new Request('GET', $url); + $client->sendAsync($request, function (ResponseInterface $response) { + $this->assertEquals("foo\n", $response->getBody()); + $this->assertEquals(200, $response->getStatus()); + $this->assertEquals(4, $response->getHeader('Content-Length')); + }, function ($error) use ($request) { + $url = $request->getUrl(); + $this->fail("Failed to get $url"); + }); + + $url = $this->getAbsoluteUrl('/bar.php'); + $request = new Request('GET', $url); + $client->sendAsync($request, function (ResponseInterface $response) { + $this->assertEquals("bar\n", $response->getBody()); + $this->assertEquals(200, $response->getStatus()); + $this->assertEquals('Bar', $response->getHeader('X-Test')); + }, function ($error) use ($request) { + $url = $request->getUrl(); + $this->fail("Failed to get $url"); + }); + + $client->wait(); + } + + public function testSendClientError() + { + $client = new ClientMock(); + $request = new Request('GET', 'http://example.org/'); + + $client->on('doRequest', function ($request, &$response) { + throw new ClientException('aaah', 1); + }); + $called = false; + $client->on('exception', function () use (&$called) { + $called = true; + }); + + try { + $client->send($request); + $this->fail('send() should have thrown an exception'); + } catch (ClientException $e) { + } + $this->assertTrue($called); + } + + public function testSendHttpError() + { + $client = new ClientMock(); + $request = new Request('GET', 'http://example.org/'); + + $client->on('doRequest', function ($request, &$response) { + $response = new Response(404); + }); + $called = 0; + $client->on('error', function () use (&$called) { + ++$called; + }); + $client->on('error:404', function () use (&$called) { + ++$called; + }); + + $client->send($request); + $this->assertEquals(2, $called); + } + + public function testSendRetry() + { + $client = new ClientMock(); + $request = new Request('GET', 'http://example.org/'); + + $called = 0; + $client->on('doRequest', function ($request, &$response) use (&$called) { + ++$called; + if ($called < 3) { + $response = new Response(404); + } else { + $response = new Response(200); + } + }); + + $errorCalled = 0; + $client->on('error', function ($request, $response, &$retry, $retryCount) use (&$errorCalled) { + ++$errorCalled; + $retry = true; + }); + + $response = $client->send($request); + $this->assertEquals(3, $called); + $this->assertEquals(2, $errorCalled); + $this->assertEquals(200, $response->getStatus()); + } + + public function testHttpErrorException() + { + $client = new ClientMock(); + $client->setThrowExceptions(true); + $request = new Request('GET', 'http://example.org/'); + + $client->on('doRequest', function ($request, &$response) { + $response = new Response(404); + }); + + try { + $client->send($request); + $this->fail('An exception should have been thrown'); + } catch (ClientHttpException $e) { + $this->assertEquals(404, $e->getHttpStatus()); + $this->assertInstanceOf('Sabre\HTTP\Response', $e->getResponse()); + } + } + + public function testParseCurlResult() + { + $client = new ClientMock(); + $client->on('curlStuff', function (&$return) { + $return = [ + [ + 'header_size' => 33, + 'http_code' => 200, + ], + 0, + '', + ]; + }); + + $body = "HTTP/1.1 200 OK\r\nHeader1:Val1\r\n\r\nFoo"; + $result = $client->parseCurlResult($body, 'foobar'); + + $this->assertEquals(Client::STATUS_SUCCESS, $result['status']); + $this->assertEquals(200, $result['http_code']); + $this->assertEquals(200, $result['response']->getStatus()); + $this->assertEquals(['Header1' => ['Val1']], $result['response']->getHeaders()); + $this->assertEquals('Foo', $result['response']->getBodyAsString()); + } + + public function testParseCurlResultEmptyBody() + { + $client = new ClientMock(); + $client->on('curlStuff', function (&$return) { + $return = [ + [ + 'header_size' => 33, + 'http_code' => 200, + ], + 0, + '', + ]; + }); + + $body = "HTTP/1.1 200 OK\r\nHeader1:Val1\r\n\r\n"; + $result = $client->parseCurlResult($body, 'foobar'); + + $this->assertEquals(Client::STATUS_SUCCESS, $result['status']); + $this->assertEquals(200, $result['http_code']); + $this->assertEquals(200, $result['response']->getStatus()); + $this->assertEquals(['Header1' => ['Val1']], $result['response']->getHeaders()); + $this->assertEquals('', $result['response']->getBodyAsString()); + } + + public function testParseCurlError() + { + $client = new ClientMock(); + $client->on('curlStuff', function (&$return) { + $return = [ + [], + 1, + 'Curl error', + ]; + }); + + $body = "HTTP/1.1 200 OK\r\nHeader1:Val1\r\n\r\nFoo"; + $result = $client->parseCurlResult($body, 'foobar'); + + $this->assertEquals(Client::STATUS_CURLERROR, $result['status']); + $this->assertEquals(1, $result['curl_errno']); + $this->assertEquals('Curl error', $result['curl_errmsg']); + } + + public function testDoRequest() + { + $client = new ClientMock(); + $request = new Request('GET', 'http://example.org/'); + $client->on('curlExec', function (&$return) { + $return = "HTTP/1.1 200 OK\r\nHeader1:Val1\r\n\r\nFoo"; + }); + $client->on('curlStuff', function (&$return) { + $return = [ + [ + 'header_size' => 33, + 'http_code' => 200, + ], + 0, + '', + ]; + }); + $response = $client->doRequest($request); + $this->assertEquals(200, $response->getStatus()); + $this->assertEquals(['Header1' => ['Val1']], $response->getHeaders()); + $this->assertEquals('Foo', $response->getBodyAsString()); + } + + public function testDoRequestCurlError() + { + $client = new ClientMock(); + $request = new Request('GET', 'http://example.org/'); + $client->on('curlExec', function (&$return) { + $return = ''; + }); + $client->on('curlStuff', function (&$return) { + $return = [ + [], + 1, + 'Curl error', + ]; + }); + + try { + $response = $client->doRequest($request); + $this->fail('This should have thrown an exception'); + } catch (ClientException $e) { + $this->assertEquals(1, $e->getCode()); + $this->assertEquals('Curl error', $e->getMessage()); + } + } +} + +class ClientMock extends Client +{ + protected $persistedSettings = []; + + /** + * Making this method public. + */ + public function receiveCurlHeader($curlHandle, $headerLine) + { + return parent::receiveCurlHeader($curlHandle, $headerLine); + } + + /** + * Making this method public. + */ + public function createCurlSettingsArray(RequestInterface $request): array + { + return parent::createCurlSettingsArray($request); + } + + /** + * Making this method public. + */ + public function parseCurlResult(string $response, $curlHandle): array + { + return parent::parseCurlResult($response, $curlHandle); + } + + /** + * This method is responsible for performing a single request. + */ + public function doRequest(RequestInterface $request): ResponseInterface + { + $response = null; + $this->emit('doRequest', [$request, &$response]); + + // If nothing modified $response, we're using the default behavior. + if (is_null($response)) { + return parent::doRequest($request); + } else { + return $response; + } + } + + /** + * Returns a bunch of information about a curl request. + * + * This method exists so it can easily be overridden and mocked. + * + * @param resource $curlHandle + */ + protected function curlStuff($curlHandle): array + { + $return = null; + $this->emit('curlStuff', [&$return]); + + // If nothing modified $return, we're using the default behavior. + if (is_null($return)) { + return parent::curlStuff($curlHandle); + } else { + return $return; + } + } + + /** + * Calls curl_exec. + * + * This method exists so it can easily be overridden and mocked. + * + * @param resource $curlHandle + */ + protected function curlExec($curlHandle): string + { + $return = null; + $this->emit('curlExec', [&$return]); + + // If nothing modified $return, we're using the default behavior. + if (is_null($return)) { + return parent::curlExec($curlHandle); + } else { + return $return; + } + } +} diff --git a/lib/vendor/sabre/http/tests/HTTP/FunctionsTest.php b/lib/vendor/sabre/http/tests/HTTP/FunctionsTest.php new file mode 100644 index 0000000..3bdbb60 --- /dev/null +++ b/lib/vendor/sabre/http/tests/HTTP/FunctionsTest.php @@ -0,0 +1,212 @@ +assertEquals($result, getHeaderValues($values1, $values2)); + } + + public function getHeaderValuesDataOnValues2() + { + return [ + [ + ['a', 'b'], + ['a'], + ['b'], + ], + [ + ['a', 'b', 'c', 'd', 'e'], + ['a', 'b', 'c'], + ['d', 'e'], + ], + ]; + } + + /** + * @dataProvider getHeaderValuesData + */ + public function testGetHeaderValues($input, $output) + { + $this->assertEquals( + $output, + getHeaderValues($input) + ); + } + + public function getHeaderValuesData() + { + return [ + [ + 'a', + ['a'], + ], + [ + 'a,b', + ['a', 'b'], + ], + [ + 'a, b', + ['a', 'b'], + ], + [ + ['a, b'], + ['a', 'b'], + ], + [ + ['a, b', 'c', 'd,e'], + ['a', 'b', 'c', 'd', 'e'], + ], + ]; + } + + /** + * @dataProvider preferData + */ + public function testPrefer($input, $output) + { + $this->assertEquals( + $output, + parsePrefer($input) + ); + } + + public function preferData() + { + return [ + [ + 'foo; bar', + ['foo' => true], + ], + [ + 'foo; bar=""', + ['foo' => true], + ], + [ + 'foo=""; bar', + ['foo' => true], + ], + [ + 'FOO', + ['foo' => true], + ], + [ + 'respond-async', + ['respond-async' => true], + ], + [ + ['respond-async, wait=100', 'handling=lenient'], + ['respond-async' => true, 'wait' => 100, 'handling' => 'lenient'], + ], + [ + ['respond-async, wait=100, handling=lenient'], + ['respond-async' => true, 'wait' => 100, 'handling' => 'lenient'], + ], + // Old values + [ + 'return-asynch, return-representation', + ['respond-async' => true, 'return' => 'representation'], + ], + [ + 'return-minimal', + ['return' => 'minimal'], + ], + [ + 'strict', + ['handling' => 'strict'], + ], + [ + 'lenient', + ['handling' => 'lenient'], + ], + // Invalid token + [ + ['foo=%bar%'], + [], + ], + ]; + } + + public function testParseHTTPDate() + { + $times = [ + 'Wed, 13 Oct 2010 10:26:00 GMT', + 'Wednesday, 13-Oct-10 10:26:00 GMT', + 'Wed Oct 13 10:26:00 2010', + ]; + + $expected = 1286965560; + + foreach ($times as $time) { + $result = parseDate($time); + $this->assertEquals($expected, $result->format('U')); + } + + $result = parseDate('Wed Oct 6 10:26:00 2010'); + $this->assertEquals(1286360760, $result->format('U')); + } + + public function testParseHTTPDateFail() + { + $times = [ + // random string + 'NOW', + // not-GMT timezone + 'Wednesday, 13-Oct-10 10:26:00 UTC', + // No space before the 6 + 'Wed Oct 6 10:26:00 2010', + // Invalid day + 'Wed Oct 0 10:26:00 2010', + 'Wed Oct 32 10:26:00 2010', + 'Wed, 0 Oct 2010 10:26:00 GMT', + 'Wed, 32 Oct 2010 10:26:00 GMT', + 'Wednesday, 32-Oct-10 10:26:00 GMT', + // Invalid hour + 'Wed, 13 Oct 2010 24:26:00 GMT', + 'Wednesday, 13-Oct-10 24:26:00 GMT', + 'Wed Oct 13 24:26:00 2010', + ]; + + foreach ($times as $time) { + $this->assertFalse(parseDate($time), 'We used the string: '.$time); + } + } + + public function testTimezones() + { + $default = date_default_timezone_get(); + date_default_timezone_set('Europe/Amsterdam'); + + $this->testParseHTTPDate(); + + date_default_timezone_set($default); + } + + public function testToHTTPDate() + { + $dt = new \DateTime('2011-12-10 12:00:00 +0200'); + + $this->assertEquals( + 'Sat, 10 Dec 2011 10:00:00 GMT', + toDate($dt) + ); + } + + public function testParseMimeTypeOnInvalidMimeType() + { + if (false === \getenv('EXECUTE_INVALID_MIME_TYPE_TEST')) { + $this->markTestSkipped('Test skipped because parseMimeType with an invalid mime type will exit in 5.x'); + } + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Not a valid mime-type: invalid_mime_type'); + + parseMimeType('invalid_mime_type'); + } +} diff --git a/lib/vendor/sabre/http/tests/HTTP/MessageDecoratorTest.php b/lib/vendor/sabre/http/tests/HTTP/MessageDecoratorTest.php new file mode 100644 index 0000000..78293eb --- /dev/null +++ b/lib/vendor/sabre/http/tests/HTTP/MessageDecoratorTest.php @@ -0,0 +1,91 @@ +inner = new Request('GET', '/'); + $this->outer = new RequestDecorator($this->inner); + } + + public function testBody() + { + $this->outer->setBody('foo'); + $this->assertEquals('foo', stream_get_contents($this->inner->getBodyAsStream())); + $this->assertEquals('foo', stream_get_contents($this->outer->getBodyAsStream())); + $this->assertEquals('foo', $this->inner->getBodyAsString()); + $this->assertEquals('foo', $this->outer->getBodyAsString()); + $this->assertEquals('foo', $this->inner->getBody()); + $this->assertEquals('foo', $this->outer->getBody()); + } + + public function testHeaders() + { + $this->outer->setHeaders([ + 'a' => 'b', + ]); + + $this->assertEquals(['a' => ['b']], $this->inner->getHeaders()); + $this->assertEquals(['a' => ['b']], $this->outer->getHeaders()); + + $this->outer->setHeaders([ + 'c' => 'd', + ]); + + $this->assertEquals(['a' => ['b'], 'c' => ['d']], $this->inner->getHeaders()); + $this->assertEquals(['a' => ['b'], 'c' => ['d']], $this->outer->getHeaders()); + + $this->outer->addHeaders([ + 'e' => 'f', + ]); + + $this->assertEquals(['a' => ['b'], 'c' => ['d'], 'e' => ['f']], $this->inner->getHeaders()); + $this->assertEquals(['a' => ['b'], 'c' => ['d'], 'e' => ['f']], $this->outer->getHeaders()); + } + + public function testHeader() + { + $this->assertFalse($this->outer->hasHeader('a')); + $this->assertFalse($this->inner->hasHeader('a')); + $this->outer->setHeader('a', 'c'); + $this->assertTrue($this->outer->hasHeader('a')); + $this->assertTrue($this->inner->hasHeader('a')); + + $this->assertEquals('c', $this->inner->getHeader('A')); + $this->assertEquals('c', $this->outer->getHeader('A')); + + $this->outer->addHeader('A', 'd'); + + $this->assertEquals( + ['c', 'd'], + $this->inner->getHeaderAsArray('A') + ); + $this->assertEquals( + ['c', 'd'], + $this->outer->getHeaderAsArray('A') + ); + + $success = $this->outer->removeHeader('a'); + + $this->assertTrue($success); + $this->assertNull($this->inner->getHeader('A')); + $this->assertNull($this->outer->getHeader('A')); + + $this->assertFalse($this->outer->removeHeader('i-dont-exist')); + } + + public function testHttpVersion() + { + $this->outer->setHttpVersion('1.0'); + + $this->assertEquals('1.0', $this->inner->getHttpVersion()); + $this->assertEquals('1.0', $this->outer->getHttpVersion()); + } +} diff --git a/lib/vendor/sabre/http/tests/HTTP/MessageTest.php b/lib/vendor/sabre/http/tests/HTTP/MessageTest.php new file mode 100644 index 0000000..597067e --- /dev/null +++ b/lib/vendor/sabre/http/tests/HTTP/MessageTest.php @@ -0,0 +1,281 @@ +assertInstanceOf('Sabre\HTTP\Message', $message); + } + + public function testStreamBody() + { + $body = 'foo'; + $h = fopen('php://memory', 'r+'); + fwrite($h, $body); + rewind($h); + + $message = new MessageMock(); + $message->setBody($h); + + $this->assertEquals($body, $message->getBodyAsString()); + rewind($h); + $this->assertEquals($body, stream_get_contents($message->getBodyAsStream())); + rewind($h); + $this->assertEquals($body, stream_get_contents($message->getBody())); + } + + public function testStringBody() + { + $body = 'foo'; + + $message = new MessageMock(); + $message->setBody($body); + + $this->assertEquals($body, $message->getBodyAsString()); + $this->assertEquals($body, stream_get_contents($message->getBodyAsStream())); + $this->assertEquals($body, $message->getBody()); + } + + public function testCallbackBodyAsString() + { + $body = $this->createCallback('foo'); + + $message = new MessageMock(); + $message->setBody($body); + + $string = $message->getBodyAsString(); + + $this->assertSame('foo', $string); + } + + public function testCallbackBodyAsStream() + { + $body = $this->createCallback('foo'); + + $message = new MessageMock(); + $message->setBody($body); + + $stream = $message->getBodyAsStream(); + + $this->assertSame('foo', stream_get_contents($stream)); + } + + public function testGetBodyWhenCallback() + { + $callback = $this->createCallback('foo'); + + $message = new MessageMock(); + $message->setBody($callback); + + $this->assertSame($callback, $message->getBody()); + } + + /** + * It's possible that streams contains more data than the Content-Length. + * + * The request object should make sure to never emit more than + * Content-Length, if Content-Length is set. + * + * This is in particular useful when responding to range requests with + * streams that represent files on the filesystem, as it's possible to just + * seek the stream to a certain point, set the content-length and let the + * request object do the rest. + */ + public function testLongStreamToStringBody() + { + $body = fopen('php://memory', 'r+'); + fwrite($body, 'abcdefg'); + fseek($body, 2); + + $message = new MessageMock(); + $message->setBody($body); + $message->setHeader('Content-Length', '4'); + + $this->assertEquals( + 'cdef', + $message->getBodyAsString() + ); + } + + /** + * Some clients include a content-length header, but the header is empty. + * This is definitely broken behavior, but we should support it. + */ + public function testEmptyContentLengthHeader() + { + $body = fopen('php://memory', 'r+'); + fwrite($body, 'abcdefg'); + fseek($body, 2); + + $message = new MessageMock(); + $message->setBody($body); + $message->setHeader('Content-Length', ''); + + $this->assertEquals( + 'cdefg', + $message->getBodyAsString() + ); + } + + public function testGetEmptyBodyStream() + { + $message = new MessageMock(); + $body = $message->getBodyAsStream(); + + $this->assertEquals('', stream_get_contents($body)); + } + + public function testGetEmptyBodyString() + { + $message = new MessageMock(); + $body = $message->getBodyAsString(); + + $this->assertEquals('', $body); + } + + public function testHeaders() + { + $message = new MessageMock(); + $message->setHeader('X-Foo', 'bar'); + + // Testing caselessness + $this->assertEquals('bar', $message->getHeader('X-Foo')); + $this->assertEquals('bar', $message->getHeader('x-fOO')); + + $this->assertTrue( + $message->removeHeader('X-FOO') + ); + $this->assertNull($message->getHeader('X-Foo')); + $this->assertFalse( + $message->removeHeader('X-FOO') + ); + } + + public function testSetHeaders() + { + $message = new MessageMock(); + + $headers = [ + 'X-Foo' => ['1'], + 'X-Bar' => ['2'], + ]; + + $message->setHeaders($headers); + $this->assertEquals($headers, $message->getHeaders()); + + $message->setHeaders([ + 'X-Foo' => ['3', '4'], + 'X-Bar' => '5', + ]); + + $expected = [ + 'X-Foo' => ['3', '4'], + 'X-Bar' => ['5'], + ]; + + $this->assertEquals($expected, $message->getHeaders()); + } + + public function testAddHeaders() + { + $message = new MessageMock(); + + $headers = [ + 'X-Foo' => ['1'], + 'X-Bar' => ['2'], + ]; + + $message->addHeaders($headers); + $this->assertEquals($headers, $message->getHeaders()); + + $message->addHeaders([ + 'X-Foo' => ['3', '4'], + 'X-Bar' => '5', + ]); + + $expected = [ + 'X-Foo' => ['1', '3', '4'], + 'X-Bar' => ['2', '5'], + ]; + + $this->assertEquals($expected, $message->getHeaders()); + } + + public function testSendBody() + { + $message = new MessageMock(); + + // String + $message->setBody('foo'); + + // Stream + $h = fopen('php://memory', 'r+'); + fwrite($h, 'bar'); + rewind($h); + $message->setBody($h); + + $body = $message->getBody(); + rewind($body); + + $this->assertEquals('bar', stream_get_contents($body)); + } + + public function testMultipleHeaders() + { + $message = new MessageMock(); + $message->setHeader('a', '1'); + $message->addHeader('A', '2'); + + $this->assertEquals( + '1,2', + $message->getHeader('A') + ); + $this->assertEquals( + '1,2', + $message->getHeader('a') + ); + + $this->assertEquals( + ['1', '2'], + $message->getHeaderAsArray('a') + ); + $this->assertEquals( + ['1', '2'], + $message->getHeaderAsArray('A') + ); + $this->assertEquals( + [], + $message->getHeaderAsArray('B') + ); + } + + public function testHasHeaders() + { + $message = new MessageMock(); + + $this->assertFalse($message->hasHeader('X-Foo')); + $message->setHeader('X-Foo', 'Bar'); + $this->assertTrue($message->hasHeader('X-Foo')); + } + + /** + * @param string $content + * + * @return \Closure Returns a callback printing $content to php://output stream + */ + private function createCallback($content) + { + return function () use ($content) { + echo $content; + }; + } +} + +class MessageMock extends Message +{ +} diff --git a/lib/vendor/sabre/http/tests/HTTP/NegotiateTest.php b/lib/vendor/sabre/http/tests/HTTP/NegotiateTest.php new file mode 100644 index 0000000..82f14ea --- /dev/null +++ b/lib/vendor/sabre/http/tests/HTTP/NegotiateTest.php @@ -0,0 +1,135 @@ +assertEquals( + $expected, + negotiateContentType($acceptHeader, $available) + ); + } + + public function negotiateData() + { + return [ + [ // simple + 'application/xml', + ['application/xml'], + 'application/xml', + ], + [ // no header + null, + ['application/xml'], + 'application/xml', + ], + [ // 2 options + 'application/json', + ['application/xml', 'application/json'], + 'application/json', + ], + [ // 2 choices + 'application/json, application/xml', + ['application/xml'], + 'application/xml', + ], + [ // quality + 'application/xml;q=0.2, application/json', + ['application/xml', 'application/json'], + 'application/json', + ], + [ // wildcard + 'image/jpeg, image/png, */*', + ['application/xml', 'application/json'], + 'application/xml', + ], + [ // wildcard + quality + 'image/jpeg, image/png; q=0.5, */*', + ['application/xml', 'application/json', 'image/png'], + 'application/xml', + ], + [ // no match + 'image/jpeg', + ['application/xml'], + null, + ], + [ // This is used in sabre/dav + 'text/vcard; version=4.0', + [ + // 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', + ], + 'text/vcard; version=4.0', + ], + [ // rfc7231 example 1 + 'audio/*; q=0.2, audio/basic', + [ + 'audio/pcm', + 'audio/basic', + ], + 'audio/basic', + ], + [ // Lower quality after + 'audio/pcm; q=0.2, audio/basic; q=0.1', + [ + 'audio/pcm', + 'audio/basic', + ], + 'audio/pcm', + ], + [ // Random parameter, should be ignored + 'audio/pcm; hello; q=0.2, audio/basic; q=0.1', + [ + 'audio/pcm', + 'audio/basic', + ], + 'audio/pcm', + ], + [ // No whitespace after type, should pick the one that is the most specific. + 'text/vcard;version=3.0, text/vcard', + [ + 'text/vcard', + 'text/vcard; version=3.0', + ], + 'text/vcard; version=3.0', + ], + [ // Same as last one, but order is different + 'text/vcard, text/vcard;version=3.0', + [ + 'text/vcard; version=3.0', + 'text/vcard', + ], + 'text/vcard; version=3.0', + ], + [ // Charset should be ignored here. + 'text/vcard; charset=utf-8; version=3.0, text/vcard', + [ + 'text/vcard', + 'text/vcard; version=3.0', + ], + 'text/vcard; version=3.0', + ], + [ // Undefined offset issue. + 'text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2', + ['application/xml', 'application/json', 'image/png'], + 'application/xml', + ], + ]; + } +} diff --git a/lib/vendor/sabre/http/tests/HTTP/RequestDecoratorTest.php b/lib/vendor/sabre/http/tests/HTTP/RequestDecoratorTest.php new file mode 100644 index 0000000..f6f3a72 --- /dev/null +++ b/lib/vendor/sabre/http/tests/HTTP/RequestDecoratorTest.php @@ -0,0 +1,103 @@ +inner = new Request('GET', '/'); + $this->outer = new RequestDecorator($this->inner); + } + + public function testMethod() + { + $this->outer->setMethod('FOO'); + $this->assertEquals('FOO', $this->inner->getMethod()); + $this->assertEquals('FOO', $this->outer->getMethod()); + } + + public function testUrl() + { + $this->outer->setUrl('/foo'); + $this->assertEquals('/foo', $this->inner->getUrl()); + $this->assertEquals('/foo', $this->outer->getUrl()); + } + + public function testAbsoluteUrl() + { + $this->outer->setAbsoluteUrl('http://example.org/foo'); + $this->assertEquals('http://example.org/foo', $this->inner->getAbsoluteUrl()); + $this->assertEquals('http://example.org/foo', $this->outer->getAbsoluteUrl()); + } + + public function testBaseUrl() + { + $this->outer->setBaseUrl('/foo'); + $this->assertEquals('/foo', $this->inner->getBaseUrl()); + $this->assertEquals('/foo', $this->outer->getBaseUrl()); + } + + public function testPath() + { + $this->outer->setBaseUrl('/foo'); + $this->outer->setUrl('/foo/bar'); + $this->assertEquals('bar', $this->inner->getPath()); + $this->assertEquals('bar', $this->outer->getPath()); + } + + public function testQueryParams() + { + $this->outer->setUrl('/foo?a=b&c=d&e'); + $expected = [ + 'a' => 'b', + 'c' => 'd', + 'e' => null, + ]; + + $this->assertEquals($expected, $this->inner->getQueryParameters()); + $this->assertEquals($expected, $this->outer->getQueryParameters()); + } + + public function testPostData() + { + $postData = [ + 'a' => 'b', + 'c' => 'd', + 'e' => null, + ]; + + $this->outer->setPostData($postData); + $this->assertEquals($postData, $this->inner->getPostData()); + $this->assertEquals($postData, $this->outer->getPostData()); + } + + public function testServerData() + { + $serverData = [ + 'HTTPS' => 'On', + ]; + + $this->outer->setRawServerData($serverData); + $this->assertEquals('On', $this->inner->getRawServerValue('HTTPS')); + $this->assertEquals('On', $this->outer->getRawServerValue('HTTPS')); + + $this->assertNull($this->inner->getRawServerValue('FOO')); + $this->assertNull($this->outer->getRawServerValue('FOO')); + } + + public function testToString() + { + $this->inner->setMethod('POST'); + $this->inner->setUrl('/foo/bar/'); + $this->inner->setBody('foo'); + $this->inner->setHeader('foo', 'bar'); + + $this->assertEquals((string) $this->inner, (string) $this->outer); + } +} diff --git a/lib/vendor/sabre/http/tests/HTTP/RequestTest.php b/lib/vendor/sabre/http/tests/HTTP/RequestTest.php new file mode 100644 index 0000000..e432a78 --- /dev/null +++ b/lib/vendor/sabre/http/tests/HTTP/RequestTest.php @@ -0,0 +1,151 @@ + 'Evert', + ]); + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('/foo', $request->getUrl()); + $this->assertEquals([ + 'User-Agent' => ['Evert'], + ], $request->getHeaders()); + } + + public function testGetQueryParameters() + { + $request = new Request('GET', '/foo?a=b&c&d=e'); + $this->assertEquals([ + 'a' => 'b', + 'c' => null, + 'd' => 'e', + ], $request->getQueryParameters()); + } + + public function testGetQueryParametersNoData() + { + $request = new Request('GET', '/foo'); + $this->assertEquals([], $request->getQueryParameters()); + } + + /** + * @backupGlobals + */ + public function testCreateFromPHPRequest() + { + $_SERVER['REQUEST_URI'] = '/'; + $_SERVER['REQUEST_METHOD'] = 'PUT'; + + $request = Sapi::getRequest(); + $this->assertEquals('PUT', $request->getMethod()); + } + + public function testGetAbsoluteUrl() + { + $r = new Request('GET', '/foo', [ + 'Host' => 'sabredav.org', + ]); + + $this->assertEquals('http://sabredav.org/foo', $r->getAbsoluteUrl()); + + $s = [ + 'HTTP_HOST' => 'sabredav.org', + 'REQUEST_URI' => '/foo', + 'REQUEST_METHOD' => 'GET', + 'HTTPS' => 'on', + ]; + + $r = Sapi::createFromServerArray($s); + + $this->assertEquals('https://sabredav.org/foo', $r->getAbsoluteUrl()); + } + + public function testGetPostData() + { + $post = [ + 'bla' => 'foo', + ]; + $r = new Request('POST', '/'); + $r->setPostData($post); + $this->assertEquals($post, $r->getPostData()); + } + + public function testGetPath() + { + $request = new Request('GET', '/foo/bar/'); + $request->setBaseUrl('/foo'); + $request->setUrl('/foo/bar/'); + + $this->assertEquals('bar', $request->getPath()); + } + + public function testGetPathStrippedQuery() + { + $request = new Request('GET', '/foo/bar?a=B'); + $request->setBaseUrl('/foo'); + + $this->assertEquals('bar', $request->getPath()); + } + + public function testGetPathMissingSlash() + { + $request = new Request('GET', '/foo'); + $request->setBaseUrl('/foo/'); + + $this->assertEquals('', $request->getPath()); + } + + public function testGetPathOutsideBaseUrl() + { + $this->expectException('LogicException'); + $request = new Request('GET', '/bar/'); + $request->setBaseUrl('/foo/'); + + $request->getPath(); + } + + public function testToString() + { + $request = new Request('PUT', '/foo/bar', ['Content-Type' => 'text/xml']); + $request->setBody('foo'); + + $expected = "PUT /foo/bar HTTP/1.1\r\n" + ."Content-Type: text/xml\r\n" + ."\r\n" + .'foo'; + $this->assertEquals($expected, (string) $request); + } + + public function testToStringAuthorization() + { + $request = new Request('PUT', '/foo/bar', ['Content-Type' => 'text/xml', 'Authorization' => 'Basic foobar']); + $request->setBody('foo'); + + $expected = "PUT /foo/bar HTTP/1.1\r\n" + ."Content-Type: text/xml\r\n" + ."Authorization: Basic REDACTED\r\n" + ."\r\n" + .'foo'; + $this->assertEquals($expected, (string) $request); + } + + public function testAbsoluteUrlHttp(): void + { + $request = new Request('GET', 'http://example.com/foo/bar?a=b&c=d'); + self::assertEquals('http://example.com/foo/bar?a=b&c=d', $request->getAbsoluteUrl()); + } + + public function testAbsoluteUrlHttpHostPrevalence(): void + { + $request = new Request('GET', 'http://example.com/foo/bar?a=b&c=d', [ + 'Host' => 'example.org', + ]); + self::assertEquals('http://example.com/foo/bar?a=b&c=d', $request->getAbsoluteUrl()); + } +} diff --git a/lib/vendor/sabre/http/tests/HTTP/ResponseDecoratorTest.php b/lib/vendor/sabre/http/tests/HTTP/ResponseDecoratorTest.php new file mode 100644 index 0000000..a4ef31c --- /dev/null +++ b/lib/vendor/sabre/http/tests/HTTP/ResponseDecoratorTest.php @@ -0,0 +1,35 @@ +inner = new Response(); + $this->outer = new ResponseDecorator($this->inner); + } + + public function testStatus() + { + $this->outer->setStatus(201); + $this->assertEquals(201, $this->inner->getStatus()); + $this->assertEquals(201, $this->outer->getStatus()); + $this->assertEquals('Created', $this->inner->getStatusText()); + $this->assertEquals('Created', $this->outer->getStatusText()); + } + + public function testToString() + { + $this->inner->setStatus(201); + $this->inner->setBody('foo'); + $this->inner->setHeader('foo', 'bar'); + + $this->assertEquals((string) $this->inner, (string) $this->outer); + } +} diff --git a/lib/vendor/sabre/http/tests/HTTP/ResponseTest.php b/lib/vendor/sabre/http/tests/HTTP/ResponseTest.php new file mode 100644 index 0000000..ef21294 --- /dev/null +++ b/lib/vendor/sabre/http/tests/HTTP/ResponseTest.php @@ -0,0 +1,41 @@ + 'text/xml']); + $this->assertEquals(200, $response->getStatus()); + $this->assertEquals('OK', $response->getStatusText()); + } + + public function testSetStatus() + { + $response = new Response(); + $response->setStatus('402 Where\'s my money?'); + $this->assertEquals(402, $response->getStatus()); + $this->assertEquals('Where\'s my money?', $response->getStatusText()); + } + + public function testInvalidStatus() + { + $this->expectException('InvalidArgumentException'); + $response = new Response(1000); + } + + public function testToString() + { + $response = new Response(200, ['Content-Type' => 'text/xml']); + $response->setBody('foo'); + + $expected = "HTTP/1.1 200 OK\r\n" + ."Content-Type: text/xml\r\n" + ."\r\n" + .'foo'; + $this->assertEquals($expected, (string) $response); + } +} diff --git a/lib/vendor/sabre/http/tests/HTTP/SapiTest.php b/lib/vendor/sabre/http/tests/HTTP/SapiTest.php new file mode 100644 index 0000000..a0b4c6f --- /dev/null +++ b/lib/vendor/sabre/http/tests/HTTP/SapiTest.php @@ -0,0 +1,326 @@ + '/foo', + 'REQUEST_METHOD' => 'GET', + 'HTTP_USER_AGENT' => 'Evert', + 'CONTENT_TYPE' => 'text/xml', + 'CONTENT_LENGTH' => '400', + 'SERVER_PROTOCOL' => 'HTTP/1.0', + ]); + + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('/foo', $request->getUrl()); + $this->assertEquals([ + 'User-Agent' => ['Evert'], + 'Content-Type' => ['text/xml'], + 'Content-Length' => ['400'], + ], $request->getHeaders()); + + $this->assertEquals('1.0', $request->getHttpVersion()); + + $this->assertEquals('400', $request->getRawServerValue('CONTENT_LENGTH')); + $this->assertNull($request->getRawServerValue('FOO')); + } + + public function testConstructFromServerArrayOnNullUrl() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The _SERVER array must have a REQUEST_URI key'); + + $request = Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'GET', + 'HTTP_USER_AGENT' => 'Evert', + 'CONTENT_TYPE' => 'text/xml', + 'CONTENT_LENGTH' => '400', + 'SERVER_PROTOCOL' => 'HTTP/1.0', + ]); + } + + public function testConstructFromServerArrayOnNullMethod() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The _SERVER array must have a REQUEST_METHOD key'); + + $request = Sapi::createFromServerArray([ + 'REQUEST_URI' => '/foo', + 'HTTP_USER_AGENT' => 'Evert', + 'CONTENT_TYPE' => 'text/xml', + 'CONTENT_LENGTH' => '400', + 'SERVER_PROTOCOL' => 'HTTP/1.0', + ]); + } + + public function testConstructPHPAuth() + { + $request = Sapi::createFromServerArray([ + 'REQUEST_URI' => '/foo', + 'REQUEST_METHOD' => 'GET', + 'PHP_AUTH_USER' => 'user', + 'PHP_AUTH_PW' => 'pass', + ]); + + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('/foo', $request->getUrl()); + $this->assertEquals([ + 'Authorization' => ['Basic '.base64_encode('user:pass')], + ], $request->getHeaders()); + } + + public function testConstructPHPAuthDigest() + { + $request = Sapi::createFromServerArray([ + 'REQUEST_URI' => '/foo', + 'REQUEST_METHOD' => 'GET', + 'PHP_AUTH_DIGEST' => 'blabla', + ]); + + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('/foo', $request->getUrl()); + $this->assertEquals([ + 'Authorization' => ['Digest blabla'], + ], $request->getHeaders()); + } + + public function testConstructRedirectAuth() + { + $request = Sapi::createFromServerArray([ + 'REQUEST_URI' => '/foo', + 'REQUEST_METHOD' => 'GET', + 'REDIRECT_HTTP_AUTHORIZATION' => 'Basic bla', + ]); + + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('/foo', $request->getUrl()); + $this->assertEquals([ + 'Authorization' => ['Basic bla'], + ], $request->getHeaders()); + } + + /** + * @runInSeparateProcess + * + * Unfortunately we have no way of testing if the HTTP response code got + * changed. + */ + public function testSend() + { + if (!function_exists('xdebug_get_headers')) { + $this->markTestSkipped('XDebug needs to be installed for this test to run'); + } + + $response = new Response(204, ['Content-Type' => 'text/xml;charset=UTF-8']); + + // Second Content-Type header. Normally this doesn't make sense. + $response->addHeader('Content-Type', 'application/xml'); + $response->setBody('foo'); + + ob_start(); + + Sapi::sendResponse($response); + $headers = xdebug_get_headers(); + + $result = ob_get_clean(); + header_remove(); + + $this->assertEquals( + [ + 'Content-Type: text/xml;charset=UTF-8', + 'Content-Type: application/xml', + ], + $headers + ); + + $this->assertEquals('foo', $result); + } + + /** + * @runInSeparateProcess + * + * @depends testSend + */ + public function testSendLimitedByContentLengthString() + { + $response = new Response(200); + + $response->addHeader('Content-Length', 19); + $response->setBody('Send this sentence. Ignore this one.'); + + ob_start(); + + Sapi::sendResponse($response); + + $result = ob_get_clean(); + header_remove(); + + $this->assertEquals('Send this sentence.', $result); + } + + /** + * Tests whether http2 is recognized. + */ + public function testRecognizeHttp2() + { + $request = Sapi::createFromServerArray([ + 'SERVER_PROTOCOL' => 'HTTP/2.0', + 'REQUEST_URI' => 'bla', + 'REQUEST_METHOD' => 'GET', + ]); + + $this->assertEquals('2.0', $request->getHttpVersion()); + } + + /** + * @runInSeparateProcess + * + * @depends testSend + */ + public function testSendLimitedByContentLengthStream() + { + $response = new Response(200, ['Content-Length' => 19]); + + $body = fopen('php://memory', 'w'); + fwrite($body, 'Ignore this. Send this sentence. Ignore this too.'); + rewind($body); + fread($body, 13); + $response->setBody($body); + + ob_start(); + + Sapi::sendResponse($response); + + $result = ob_get_clean(); + header_remove(); + + $this->assertEquals('Send this sentence.', $result); + } + + /** + * @runInSeparateProcess + * + * @depends testSend + * + * @dataProvider sendContentRangeStreamData + */ + public function testSendContentRangeStream($ignoreAtStart, $sendText, $multiplier, $ignoreAtEnd, $contentLength) + { + $partial = str_repeat($sendText, $multiplier); + $ignoreAtStartLength = strlen($ignoreAtStart); + $ignoreAtEndLength = strlen($ignoreAtEnd); + $body = fopen('php://memory', 'w'); + if (!$contentLength) { + $contentLength = strlen($partial); + } + fwrite($body, $ignoreAtStart); + fwrite($body, $partial); + if ($ignoreAtEndLength > 0) { + fwrite($body, $ignoreAtEnd); + } + rewind($body); + if ($ignoreAtStartLength > 0) { + fread($body, $ignoreAtStartLength); + } + $response = new Response(200, [ + 'Content-Length' => $contentLength, + 'Content-Range' => sprintf('bytes %d-%d/%d', $ignoreAtStartLength, $ignoreAtStartLength + strlen($partial) - 1, $ignoreAtStartLength + strlen($partial) + $ignoreAtEndLength), + ]); + $response->setBody($body); + + ob_start(); + + Sapi::sendResponse($response); + + $result = ob_get_clean(); + header_remove(); + + $this->assertEquals($partial, $result); + } + + public function sendContentRangeStreamData() + { + return [ + ['Ignore this. ', 'Send this.', 10, ' Ignore this at end.'], + ['Ignore this. ', 'Send this.', 1000, ' Ignore this at end.'], + ['Ignore this. ', 'S', 4096, ' Ignore this at end.'], + ['I', 'S', 4094, 'E'], + ['', 'Send this.', 10, ' Ignore this at end.'], + ['', 'Send this.', 1000, ' Ignore this at end.'], + ['', 'S', 4096, ' Ignore this at end.'], + ['', 'S', 4094, 'En'], + ['Ignore this. ', 'Send this.', 10, ''], + ['Ignore this. ', 'Send this.', 1000, ''], + ['Ignore this. ', 'S', 4096, ''], + ['Ig', 'S', 4094, ''], + + // Provide contentLength greater than the bytes remaining in the stream. + ['Ignore this. ', 'Send this.', 10, '', 101], + ['Ignore this. ', 'Send this.', 1000, '', 10001], + ['Ignore this. ', 'S', 4096, '', 5000000], + ['I', 'S', 4094, '', 8095], + // Provide contentLength equal to the bytes remaining in the stream. + ['', 'Send this.', 10, '', 100], + ['Ignore this. ', 'Send this.', 1000, '', 10000], + ]; + } + + /** + * @runInSeparateProcess + * + * @depends testSend + */ + public function testSendWorksWithCallbackAsBody() + { + $response = new Response(200, [], function () { + $fd = fopen('php://output', 'r+'); + fwrite($fd, 'foo'); + fclose($fd); + }); + + ob_start(); + + Sapi::sendResponse($response); + + $result = ob_get_clean(); + + $this->assertEquals('foo', $result); + } + + public function testSendConnectionAborted(): void + { + $baseUrl = getenv('BASEURL'); + if (!$baseUrl) { + $this->markTestSkipped('Set an environment value BASEURL to continue'); + } + + $url = rtrim($baseUrl, '/').'/connection_aborted.php'; + $chunk_size = 4 * 1024 * 1024; + $fetch_size = 6 * 1024 * 1024; + + $stream = fopen($url, 'r'); + $size = 0; + + while ($size <= $fetch_size) { + $temp = fread($stream, 8192); + if (false === $temp) { + break; + } + $size += strlen($temp); + } + + fclose($stream); + + sleep(5); + + $bytes_read = file_get_contents(sys_get_temp_dir().'/dummy_stream_read_counter'); + $this->assertEquals($chunk_size * 2, $bytes_read); + $this->assertGreaterThanOrEqual($fetch_size, $bytes_read); + } +} diff --git a/lib/vendor/sabre/http/tests/HTTP/URLUtilTest.php b/lib/vendor/sabre/http/tests/HTTP/URLUtilTest.php new file mode 100644 index 0000000..bf0503e --- /dev/null +++ b/lib/vendor/sabre/http/tests/HTTP/URLUtilTest.php @@ -0,0 +1,114 @@ +assertEquals( + '%00%01%02%03%04%05%06%07%08%09%0a%0b%0c%0d%0e%0f'. + '%10%11%12%13%14%15%16%17%18%19%1a%1b%1c%1d%1e%1f'. + '%20%21%22%23%24%25%26%27()%2a%2b%2c-./'. + '0123456789:%3b%3c%3d%3e%3f'. + '@ABCDEFGHIJKLMNO'. + 'PQRSTUVWXYZ%5b%5c%5d%5e_'. + '%60abcdefghijklmno'. + 'pqrstuvwxyz%7b%7c%7d~%7f', + $newStr); + + $this->assertEquals($str, decodePath($newStr)); + } + + public function testEncodePathSegment() + { + $str = ''; + for ($i = 0; $i < 128; ++$i) { + $str .= chr($i); + } + + $newStr = encodePathSegment($str); + + // Note: almost exactly the same as the last test, with the + // exception of the encoding of / (ascii code 2f) + $this->assertEquals( + '%00%01%02%03%04%05%06%07%08%09%0a%0b%0c%0d%0e%0f'. + '%10%11%12%13%14%15%16%17%18%19%1a%1b%1c%1d%1e%1f'. + '%20%21%22%23%24%25%26%27()%2a%2b%2c-.%2f'. + '0123456789:%3b%3c%3d%3e%3f'. + '@ABCDEFGHIJKLMNO'. + 'PQRSTUVWXYZ%5b%5c%5d%5e_'. + '%60abcdefghijklmno'. + 'pqrstuvwxyz%7b%7c%7d~%7f', + $newStr); + + $this->assertEquals($str, decodePathSegment($newStr)); + } + + public function testDecode() + { + $str = 'Hello%20Test+Test2.txt'; + $newStr = decodePath($str); + $this->assertEquals('Hello Test+Test2.txt', $newStr); + } + + /** + * @depends testDecode + */ + public function testDecodeUmlaut() + { + $str = 'Hello%C3%BC.txt'; + $newStr = decodePath($str); + $this->assertEquals("Hello\xC3\xBC.txt", $newStr); + } + + /** + * @depends testDecode + */ + public function testDecodeSlavicWords() + { + $words = [ + 'Ostroměr', + 'Šventaragis', + 'Świętopełk', + 'Dušan', + 'Živko', + ]; + foreach ($words as $word) { + $str = rawurlencode($word); + $newStr = decodePath($str); + $this->assertEquals($word, $newStr); + } + } + + /** + * @depends testDecodeUmlaut + */ + public function testDecodeUmlautLatin1() + { + $str = 'Hello%FC.txt'; + $newStr = decodePath($str); + $this->assertEquals("Hello\xC3\xBC.txt", $newStr); + } + + /** + * This testcase was sent by a bug reporter. + * + * @depends testDecode + */ + public function testDecodeAccentsWindows7() + { + $str = '/webdav/%C3%A0fo%C3%B3'; + $newStr = decodePath($str); + $this->assertEquals(strtolower($str), encodePath($newStr)); + } +} diff --git a/lib/vendor/sabre/http/tests/bootstrap.php b/lib/vendor/sabre/http/tests/bootstrap.php new file mode 100644 index 0000000..b47f704 --- /dev/null +++ b/lib/vendor/sabre/http/tests/bootstrap.php @@ -0,0 +1,17 @@ + + + + HTTP/ + + + + + + ../lib/ + + + + + + + diff --git a/lib/vendor/sabre/http/tests/www/bar.php b/lib/vendor/sabre/http/tests/www/bar.php new file mode 100644 index 0000000..05de39e --- /dev/null +++ b/lib/vendor/sabre/http/tests/www/bar.php @@ -0,0 +1,5 @@ + +bar diff --git a/lib/vendor/sabre/http/tests/www/connection_aborted.php b/lib/vendor/sabre/http/tests/www/connection_aborted.php new file mode 100644 index 0000000..724ad2d --- /dev/null +++ b/lib/vendor/sabre/http/tests/www/connection_aborted.php @@ -0,0 +1,69 @@ +position = 0; + + return true; + } + + public function stream_read(int $count): string + { + $this->position += $count; + + return random_bytes($count); + } + + public function stream_tell(): int + { + return $this->position; + } + + public function stream_eof(): bool + { + return $this->position > 25 * 1024 * 1024; + } + + public function stream_close(): void + { + file_put_contents(sys_get_temp_dir().'/dummy_stream_read_counter', $this->position); + } +} + +/* + * The DummyStream wrapper has two functions: + * - Provide dummy data. + * - Count how many bytes have been read. + */ +stream_wrapper_register('dummy', DummyStream::class); + +/* + * Overwrite default connection handling. + * The default behaviour is however for your script to be aborted when the remote client disconnects. + * + * Nextcloud/ownCloud set ignore_user_abort(true) on purpose to work around + * some edge cases where the default behavior would end a script too early. + * + * https://github.com/owncloud/core/issues/22370 + * https://github.com/owncloud/core/pull/26775 + */ +ignore_user_abort(true); + +$body = fopen('dummy://hello', 'r'); + +$response = new HTTP\Response(); +$response->setStatus(200); +$response->addHeader('Content-Length', 25 * 1024 * 1024); +$response->setBody($body); + +HTTP\Sapi::sendResponse($response); diff --git a/lib/vendor/sabre/http/tests/www/foo b/lib/vendor/sabre/http/tests/www/foo new file mode 100644 index 0000000..257cc56 --- /dev/null +++ b/lib/vendor/sabre/http/tests/www/foo @@ -0,0 +1 @@ +foo diff --git a/lib/vendor/sabre/http/tests/www/large.php b/lib/vendor/sabre/http/tests/www/large.php new file mode 100644 index 0000000..65fefb5 --- /dev/null +++ b/lib/vendor/sabre/http/tests/www/large.php @@ -0,0 +1,7 @@ +exclude('vendor') + ->in(__DIR__); + +$config = new PhpCsFixer\Config(); +$config->setRules([ + '@PSR1' => true, + '@Symfony' => true, + 'nullable_type_declaration' => [ + 'syntax' => 'question_mark', + ], + 'nullable_type_declaration_for_default_null_value' => true, +]); +$config->setFinder($finder); +return $config; \ No newline at end of file diff --git a/lib/vendor/sabre/uri/LICENSE b/lib/vendor/sabre/uri/LICENSE new file mode 100644 index 0000000..ae2c992 --- /dev/null +++ b/lib/vendor/sabre/uri/LICENSE @@ -0,0 +1,27 @@ +Copyright (C) 2014-2019 fruux GmbH (https://fruux.com/) + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name Sabre nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/vendor/sabre/uri/composer.json b/lib/vendor/sabre/uri/composer.json new file mode 100644 index 0000000..ba4a8e0 --- /dev/null +++ b/lib/vendor/sabre/uri/composer.json @@ -0,0 +1,68 @@ +{ + "name": "sabre/uri", + "description": "Functions for making sense out of URIs.", + "keywords": [ + "URI", + "URL", + "rfc3986" + ], + "homepage": "http://sabre.io/uri/", + "license": "BSD-3-Clause", + "require": { + "php": "^7.4 || ^8.0" + }, + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "source": "https://github.com/fruux/sabre-uri" + }, + "autoload": { + "files" : [ + "lib/functions.php" + ], + "psr-4" : { + "Sabre\\Uri\\" : "lib/" + } + }, + "autoload-dev": { + "psr-4": { + "Sabre\\Uri\\": "tests/Uri" + } + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.63", + "phpstan/phpstan": "^1.12", + "phpstan/phpstan-phpunit": "^1.4", + "phpstan/phpstan-strict-rules": "^1.6", + "phpstan/extension-installer": "^1.4", + "phpunit/phpunit" : "^9.6" + }, + "scripts": { + "phpstan": [ + "phpstan analyse lib tests" + ], + "cs-fixer": [ + "php-cs-fixer fix" + ], + "phpunit": [ + "phpunit --configuration tests/phpunit.xml" + ], + "test": [ + "composer phpstan", + "composer cs-fixer", + "composer phpunit" + ] + }, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true + } + } +} diff --git a/lib/vendor/sabre/uri/lib/InvalidUriException.php b/lib/vendor/sabre/uri/lib/InvalidUriException.php new file mode 100644 index 0000000..7f37ca5 --- /dev/null +++ b/lib/vendor/sabre/uri/lib/InvalidUriException.php @@ -0,0 +1,19 @@ + 0) { + // If the path starts with a slash + if ('/' === $delta['path'][0]) { + $path = $delta['path']; + } else { + // Removing last component from base path. + $path = (string) $base['path']; + $length = strrpos($path, '/'); + if (false !== $length) { + $path = substr($path, 0, $length); + } + $path .= '/'.$delta['path']; + } + } else { + $path = $base['path'] ?? '/'; + if ('' === $path) { + $path = '/'; + } + } + // Removing .. and . + $pathParts = explode('/', $path); + $newPathParts = []; + foreach ($pathParts as $pathPart) { + switch ($pathPart) { + // case '' : + case '.': + break; + case '..': + array_pop($newPathParts); + break; + default: + $newPathParts[] = $pathPart; + break; + } + } + + $path = implode('/', $newPathParts); + + // If the source url ended with a /, we want to preserve that. + $newParts['path'] = 0 === strpos($path, '/') ? $path : '/'.$path; + // From PHP 8, no "?" query at all causes 'query' to be null. + // An empty query "http://example.com/foo?" causes 'query' to be the empty string + if (null !== $delta['query'] && '' !== $delta['query']) { + $newParts['query'] = $delta['query']; + } elseif (isset($base['query']) && null === $delta['host'] && null === $delta['path']) { + // Keep the old query if host and path didn't change + $newParts['query'] = $base['query']; + } + // From PHP 8, no "#" fragment at all causes 'fragment' to be null. + // An empty fragment "http://example.com/foo#" causes 'fragment' to be the empty string + if (null !== $delta['fragment'] && '' !== $delta['fragment']) { + $newParts['fragment'] = $delta['fragment']; + } + + return build($newParts); +} + +/** + * Takes a URI or partial URI as its argument, and normalizes it. + * + * After normalizing a URI, you can safely compare it to other URIs. + * This function will for instance convert a %7E into a tilde, according to + * rfc3986. + * + * It will also change a %3a into a %3A. + * + * @throws InvalidUriException + */ +function normalize(string $uri): string +{ + $parts = parse($uri); + + if (null !== $parts['path']) { + $pathParts = explode('/', ltrim($parts['path'], '/')); + $newPathParts = []; + foreach ($pathParts as $pathPart) { + switch ($pathPart) { + case '.': + // skip + break; + case '..': + // One level up in the hierarchy + array_pop($newPathParts); + break; + default: + // Ensuring that everything is correctly percent-encoded. + $newPathParts[] = rawurlencode(rawurldecode($pathPart)); + break; + } + } + $parts['path'] = '/'.implode('/', $newPathParts); + } + + if (null !== $parts['scheme']) { + $parts['scheme'] = strtolower($parts['scheme']); + $defaultPorts = [ + 'http' => '80', + 'https' => '443', + ]; + + if (null !== $parts['port'] && isset($defaultPorts[$parts['scheme']]) && $defaultPorts[$parts['scheme']] == $parts['port']) { + // Removing default ports. + unset($parts['port']); + } + // A few HTTP specific rules. + switch ($parts['scheme']) { + case 'http': + case 'https': + if (null === $parts['path']) { + // An empty path is equivalent to / in http. + $parts['path'] = '/'; + } + break; + } + } + + if (null !== $parts['host']) { + $parts['host'] = strtolower($parts['host']); + } + + return build($parts); +} + +/** + * Parses a URI and returns its individual components. + * + * This method largely behaves the same as PHP's parse_url, except that it will + * return an array with all the array keys, including the ones that are not + * set by parse_url, which makes it a bit easier to work with. + * + * Unlike PHP's parse_url, it will also convert any non-ascii characters to + * percent-encoded strings. PHP's parse_url corrupts these characters on OS X. + * + * In the return array, key "port" is an int value. Other keys have a string value. + * "Unused" keys have value null. + * + * @return array{scheme: string|null, host: string|null, path: string|null, port: positive-int|null, user: string|null, query: string|null, fragment: string|null} + * + * @throws InvalidUriException + */ +function parse(string $uri): array +{ + // Normally a URI must be ASCII. However, often it's not and + // parse_url might corrupt these strings. + // + // For that reason we take any non-ascii characters from the uri and + // uriencode them first. + $uri = preg_replace_callback( + '/[^[:ascii:]]/u', + function ($matches) { + return rawurlencode($matches[0]); + }, + $uri + ); + + if (null === $uri) { + throw new InvalidUriException('Invalid, or could not parse URI'); + } + + $result = parse_url($uri); + if (false === $result) { + $result = _parse_fallback($uri); + } + + /* + * phpstan is not able to process all the things that happen while this function + * constructs the result array. It only understands the $result is + * non-empty-array + * + * But the detail of the returned array is correctly specified in the PHPdoc + * above the function call. + * + * @phpstan-ignore-next-line + */ + return + $result + [ + 'scheme' => null, + 'host' => null, + 'path' => null, + 'port' => null, + 'user' => null, + 'query' => null, + 'fragment' => null, + ]; +} + +/** + * This function takes the components returned from PHP's parse_url, and uses + * it to generate a new uri. + * + * @param array $parts + */ +function build(array $parts): string +{ + $uri = ''; + + $authority = ''; + if (isset($parts['host'])) { + $authority = $parts['host']; + if (isset($parts['user'])) { + $authority = $parts['user'].'@'.$authority; + } + if (isset($parts['port'])) { + $authority = $authority.':'.$parts['port']; + } + } + + if (isset($parts['scheme'])) { + // If there's a scheme, there's also a host. + $uri = $parts['scheme'].':'; + } + if ('' !== $authority || (isset($parts['scheme']) && 'file' === $parts['scheme'])) { + // No scheme, but there is a host. + $uri .= '//'.$authority; + } + + if (isset($parts['path'])) { + $uri .= $parts['path']; + } + if (isset($parts['query'])) { + $uri .= '?'.$parts['query']; + } + if (isset($parts['fragment'])) { + $uri .= '#'.$parts['fragment']; + } + + return $uri; +} + +/** + * Returns the 'dirname' and 'basename' for a path. + * + * The reason there is a custom function for this purpose, is because + * basename() is locale aware (behaviour changes if C locale or a UTF-8 locale + * is used) and we need a method that just operates on UTF-8 characters. + * + * In addition basename and dirname are platform aware, and will treat + * backslash (\) as a directory separator on Windows. + * + * This method returns the 2 components as an array. + * + * If there is no dirname, it will return an empty string. Any / appearing at + * the end of the string is stripped off. + * + * @return array + */ +function split(string $path): array +{ + $matches = []; + if (1 === preg_match('/^(?:(?:(.*)(?:\/+))?([^\/]+))(?:\/?)$/u', $path, $matches)) { + return [$matches[1], $matches[2]]; + } + + return [null, null]; +} + +/** + * This function is another implementation of parse_url, except this one is + * fully written in PHP. + * + * The reason is that the PHP bug team is not willing to admit that there are + * bugs in the parse_url implementation. + * + * This function is only called if the main parse method fails. It's pretty + * crude and probably slow, so the original parse_url is usually preferred. + * + * @return array{scheme: string|null, host: string|null, path: string|null, port: positive-int|null, user: string|null, query: string|null, fragment: string|null} + * + * @throws InvalidUriException + */ +function _parse_fallback(string $uri): array +{ + // Normally a URI must be ASCII, however. However, often it's not and + // parse_url might corrupt these strings. + // + // For that reason we take any non-ascii characters from the uri and + // uriencode them first. + $uri = preg_replace_callback( + '/[^[:ascii:]]/u', + function ($matches) { + return rawurlencode($matches[0]); + }, + $uri + ); + + if (null === $uri) { + throw new InvalidUriException('Invalid, or could not parse URI'); + } + + $result = [ + 'scheme' => null, + 'host' => null, + 'port' => null, + 'user' => null, + 'path' => null, + 'fragment' => null, + 'query' => null, + ]; + + if (1 === preg_match('% ^([A-Za-z][A-Za-z0-9+-\.]+): %x', $uri, $matches)) { + $result['scheme'] = $matches[1]; + // Take what's left. + $uri = substr($uri, strlen($result['scheme']) + 1); + if (false === $uri) { + // There was nothing left. + $uri = ''; + } + } + + // Taking off a fragment part + if (false !== strpos($uri, '#')) { + list($uri, $result['fragment']) = explode('#', $uri, 2); + } + // Taking off the query part + if (false !== strpos($uri, '?')) { + list($uri, $result['query']) = explode('?', $uri, 2); + } + + if ('///' === substr($uri, 0, 3)) { + // The triple slash uris are a bit unusual, but we have special handling + // for them. + $path = substr($uri, 2); + if (false === $path) { + throw new \RuntimeException('The string cannot be false'); + } + $result['path'] = $path; + $result['host'] = ''; + } elseif ('//' === substr($uri, 0, 2)) { + // Uris that have an authority part. + $regex = '%^ + // + (?: (? [^:@]+) (: (? [^@]+)) @)? + (? ( [^:/]* | \[ [^\]]+ \] )) + (?: : (? [0-9]+))? + (? / .*)? + $%x'; + if (1 !== preg_match($regex, $uri, $matches)) { + throw new InvalidUriException('Invalid, or could not parse URI'); + } + if (isset($matches['host']) && '' !== $matches['host']) { + $result['host'] = $matches['host']; + } + if (isset($matches['port'])) { + $port = (int) $matches['port']; + if ($port > 0) { + $result['port'] = $port; + } + } + if (isset($matches['path'])) { + $result['path'] = $matches['path']; + } + if (isset($matches['user']) && '' !== $matches['user']) { + $result['user'] = $matches['user']; + } + if (isset($matches['pass']) && '' !== $matches['pass']) { + $result['pass'] = $matches['pass']; + } + } else { + $result['path'] = $uri; + } + + return $result; +} diff --git a/lib/vendor/sabre/vobject/LICENSE b/lib/vendor/sabre/vobject/LICENSE new file mode 100644 index 0000000..a99c8da --- /dev/null +++ b/lib/vendor/sabre/vobject/LICENSE @@ -0,0 +1,27 @@ +Copyright (C) 2011-2016 fruux GmbH (https://fruux.com/) + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name Sabre nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/vendor/sabre/vobject/README.md b/lib/vendor/sabre/vobject/README.md new file mode 100644 index 0000000..659e3fa --- /dev/null +++ b/lib/vendor/sabre/vobject/README.md @@ -0,0 +1,55 @@ +sabre/vobject +============= + +The VObject library allows you to easily parse and manipulate [iCalendar](https://tools.ietf.org/html/rfc5545) +and [vCard](https://tools.ietf.org/html/rfc6350) objects using PHP. + +The goal of the VObject library is to create a very complete library, with an easy-to-use API. + + +Installation +------------ + +Make sure you have [Composer][1] installed, and then run: + + composer require sabre/vobject "^4.0" + +This package requires PHP 5.5. If you need the PHP 5.3/5.4 version of this package instead, use: + + + composer require sabre/vobject "^3.4" + + +Usage +----- + +* [Working with vCards](http://sabre.io/vobject/vcard/) +* [Working with iCalendar](http://sabre.io/vobject/icalendar/) + + + +Build status +------------ + +| branch | status | +| ------ | ------ | +| master | [![Build Status](https://travis-ci.org/sabre-io/vobject.svg?branch=master)](https://travis-ci.org/sabre-io/vobject) | +| 3.5 | [![Build Status](https://travis-ci.org/sabre-io/vobject.svg?branch=3.5)](https://travis-ci.org/sabre-io/vobject) | +| 3.4 | [![Build Status](https://travis-ci.org/sabre-io/vobject.svg?branch=3.4)](https://travis-ci.org/sabre-io/vobject) | +| 3.1 | [![Build Status](https://travis-ci.org/sabre-io/vobject.svg?branch=3.1)](https://travis-ci.org/sabre-io/vobject) | +| 2.1 | [![Build Status](https://travis-ci.org/sabre-io/vobject.svg?branch=2.1)](https://travis-ci.org/sabre-io/vobject) | +| 2.0 | [![Build Status](https://travis-ci.org/sabre-io/vobject.svg?branch=2.0)](https://travis-ci.org/sabre-io/vobject) | + + + +Support +------- + +Head over to the [SabreDAV mailing list](http://groups.google.com/group/sabredav-discuss) for any questions. + +Made at fruux +------------- + +This library is being developed by [fruux](https://fruux.com/). Drop us a line for commercial services or enterprise support. + +[1]: https://getcomposer.org/ diff --git a/lib/vendor/sabre/vobject/bin/bench.php b/lib/vendor/sabre/vobject/bin/bench.php new file mode 100755 index 0000000..0a2736f --- /dev/null +++ b/lib/vendor/sabre/vobject/bin/bench.php @@ -0,0 +1,12 @@ +#!/usr/bin/env php +parse->start(); + +$vcal = Sabre\VObject\Reader::read(fopen($inputFile, 'r')); + +$bench->parse->stop(); + +$repeat = 100; +$start = new \DateTime('2000-01-01'); +$end = new \DateTime('2020-01-01'); +$timeZone = new \DateTimeZone('America/Toronto'); + +$bench->fb->start(); + +for ($i = 0; $i < $repeat; ++$i) { + $fb = new Sabre\VObject\FreeBusyGenerator($start, $end, $vcal, $timeZone); + $results = $fb->getResult(); +} +$bench->fb->stop(); + +echo $bench,"\n"; + +function formatMemory($input) +{ + if (strlen($input) > 6) { + return round($input / (1024 * 1024)).'M'; + } elseif (strlen($input) > 3) { + return round($input / 1024).'K'; + } +} + +unset($input, $splitter); + +echo 'peak memory usage: '.formatMemory(memory_get_peak_usage()), "\n"; +echo 'current memory usage: '.formatMemory(memory_get_usage()), "\n"; diff --git a/lib/vendor/sabre/vobject/bin/bench_manipulatevcard.php b/lib/vendor/sabre/vobject/bin/bench_manipulatevcard.php new file mode 100644 index 0000000..df6d9f2 --- /dev/null +++ b/lib/vendor/sabre/vobject/bin/bench_manipulatevcard.php @@ -0,0 +1,64 @@ +parse->start(); + $vcard = $splitter->getNext(); + $bench->parse->pause(); + + if (!$vcard) { + break; + } + + $bench->manipulate->start(); + $vcard->{'X-FOO'} = 'Random new value!'; + $emails = []; + if (isset($vcard->EMAIL)) { + foreach ($vcard->EMAIL as $email) { + $emails[] = (string) $email; + } + } + $bench->manipulate->pause(); + + $bench->serialize->start(); + $vcard2 = $vcard->serialize(); + $bench->serialize->pause(); + + $vcard->destroy(); +} + +echo $bench,"\n"; + +function formatMemory($input) +{ + if (strlen($input) > 6) { + return round($input / (1024 * 1024)).'M'; + } elseif (strlen($input) > 3) { + return round($input / 1024).'K'; + } +} + +unset($input, $splitter); + +echo 'peak memory usage: '.formatMemory(memory_get_peak_usage()), "\n"; +echo 'current memory usage: '.formatMemory(memory_get_usage()), "\n"; diff --git a/lib/vendor/sabre/vobject/bin/fetch_windows_zones.php b/lib/vendor/sabre/vobject/bin/fetch_windows_zones.php new file mode 100755 index 0000000..2361dc3 --- /dev/null +++ b/lib/vendor/sabre/vobject/bin/fetch_windows_zones.php @@ -0,0 +1,48 @@ +#!/usr/bin/env php +xpath('//mapZone') as $mapZone) { + $from = (string) $mapZone['other']; + $to = (string) $mapZone['type']; + + list($to) = explode(' ', $to, 2); + + if (!isset($map[$from])) { + $map[$from] = $to; + } +} + +ksort($map); +echo "Writing to: $outputFile\n"; + +$f = fopen($outputFile, 'w'); +fwrite($f, " testdata.vcf + +HI; + + fwrite(STDERR, $help); + exit(2); +} + +$count = (int)$argv[1]; +if ($count < 1) { + fwrite(STDERR, "Count must be at least 1\n"); + exit(2); +} + +fwrite(STDERR, "sabre/vobject " . Version::VERSION . "\n"); +fwrite(STDERR, "Generating " . $count . " vcards in vCard 4.0 format\n"); + +/** + * The following list is just some random data we compiled from various + * sources online. + * + * Very little thought went into compiling this list, and certainly nothing + * political or ethical. + * + * We would _love_ more additions to this to add more variation to this list. + * + * Send us PR's and don't be shy adding your own first and last name for fun. + */ + +$sets = array( + "nl" => array( + "country" => "Netherlands", + "boys" => array( + "Anno", + "Bram", + "Daan", + "Evert", + "Finn", + "Jayden", + "Jens", + "Jesse", + "Levi", + "Lucas", + "Luuk", + "Milan", + "René", + "Sem", + "Sibrand", + "Willem", + ), + "girls" => array( + "Celia", + "Emma", + "Fenna", + "Geke", + "Inge", + "Julia", + "Lisa", + "Lotte", + "Mila", + "Sara", + "Sophie", + "Tess", + "Zoë", + ), + "last" => array( + "Bakker", + "Bos", + "De Boer", + "De Groot", + "De Jong", + "De Vries", + "Jansen", + "Janssen", + "Meyer", + "Mulder", + "Peters", + "Smit", + "Van Dijk", + "Van den Berg", + "Visser", + "Vos", + ), + ), + "us" => array( + "country" => "United States", + "boys" => array( + "Aiden", + "Alexander", + "Charles", + "David", + "Ethan", + "Jacob", + "James", + "Jayden", + "John", + "Joseph", + "Liam", + "Mason", + "Michael", + "Noah", + "Richard", + "Robert", + "Thomas", + "William", + ), + "girls" => array( + "Ava", + "Barbara", + "Chloe", + "Dorothy", + "Elizabeth", + "Emily", + "Emma", + "Isabella", + "Jennifer", + "Lily", + "Linda", + "Margaret", + "Maria", + "Mary", + "Mia", + "Olivia", + "Patricia", + "Roxy", + "Sophia", + "Susan", + "Zoe", + ), + "last" => array( + "Smith", + "Johnson", + "Williams", + "Jones", + "Brown", + "Davis", + "Miller", + "Wilson", + "Moore", + "Taylor", + "Anderson", + "Thomas", + "Jackson", + "White", + "Harris", + "Martin", + "Thompson", + "Garcia", + "Martinez", + "Robinson", + ), + ), +); + +$current = 0; + +$r = function($arr) { + + return $arr[mt_rand(0,count($arr)-1)]; + +}; + +$bdayStart = strtotime('-85 years'); +$bdayEnd = strtotime('-20 years'); + +while($current < $count) { + + $current++; + fwrite(STDERR, "\033[100D$current/$count"); + + $country = array_rand($sets); + $gender = mt_rand(0,1)?'girls':'boys'; + + $vcard = new Component\VCard(array( + 'VERSION' => '4.0', + 'FN' => $r($sets[$country][$gender]) . ' ' . $r($sets[$country]['last']), + 'UID' => UUIDUtil::getUUID(), + )); + + $bdayRatio = mt_rand(0,9); + + if($bdayRatio < 2) { + // 20% has a birthday property with a full date + $dt = new \DateTime('@' . mt_rand($bdayStart, $bdayEnd)); + $vcard->add('BDAY', $dt->format('Ymd')); + + } elseif ($bdayRatio < 3) { + // 10% we only know the month and date of + $dt = new \DateTime('@' . mt_rand($bdayStart, $bdayEnd)); + $vcard->add('BDAY', '--' . $dt->format('md')); + } + if ($result = $vcard->validate()) { + ob_start(); + echo "\nWe produced an invalid vcard somehow!\n"; + foreach($result as $message) { + echo " " . $message['message'] . "\n"; + } + fwrite(STDERR, ob_get_clean()); + } + echo $vcard->serialize(); + +} + +fwrite(STDERR,"\nDone.\n"); diff --git a/lib/vendor/sabre/vobject/bin/generateicalendardata.php b/lib/vendor/sabre/vobject/bin/generateicalendardata.php new file mode 100755 index 0000000..019ed97 --- /dev/null +++ b/lib/vendor/sabre/vobject/bin/generateicalendardata.php @@ -0,0 +1,87 @@ +#!/usr/bin/env php +add('VEVENT'); + $event->DTSTART = 'bla'; + $event->SUMMARY = 'Event #'.$ii; + $event->UID = md5(microtime(true)); + + $doctorRandom = mt_rand(1, 1000); + + switch ($doctorRandom) { + // All-day event + case 1: + $event->DTEND = 'bla'; + $dtStart = clone $currentDate; + $dtEnd = clone $currentDate; + $dtEnd->modify('+'.mt_rand(1, 3).' days'); + $event->DTSTART->setDateTime($dtStart); + $event->DTSTART['VALUE'] = 'DATE'; + $event->DTEND->setDateTime($dtEnd); + break; + case 2: + $event->RRULE = 'FREQ=DAILY;COUNT='.mt_rand(1, 10); + // no break intentional + default: + $dtStart = clone $currentDate; + $dtStart->setTime(mt_rand(1, 23), mt_rand(0, 59), mt_rand(0, 59)); + $event->DTSTART->setDateTime($dtStart); + $event->DURATION = 'PT'.mt_rand(1, 3).'H'; + break; + } + + $currentDate->modify('+ '.mt_rand(0, 3).' days'); +} +fwrite(STDERR, "Validating\n"); + +$result = $calendar->validate(); +if ($result) { + fwrite(STDERR, "Errors!\n"); + fwrite(STDERR, print_r($result, true)); + exit(-1); +} + +fwrite(STDERR, "Serializing this beast\n"); + +echo $calendar->serialize(); + +fwrite(STDERR, "done.\n"); diff --git a/lib/vendor/sabre/vobject/bin/mergeduplicates.php b/lib/vendor/sabre/vobject/bin/mergeduplicates.php new file mode 100755 index 0000000..31b2c14 --- /dev/null +++ b/lib/vendor/sabre/vobject/bin/mergeduplicates.php @@ -0,0 +1,160 @@ +#!/usr/bin/env php + 0, + 'No FN property' => 0, + 'Ignored duplicates' => 0, + 'Merged values' => 0, + 'Error' => 0, + 'Unique cards' => 0, + 'Total written' => 0, +]; + +function writeStats() +{ + global $stats; + foreach ($stats as $name => $value) { + echo str_pad($name, 23, ' ', STR_PAD_RIGHT), str_pad($value, 6, ' ', STR_PAD_LEFT), "\n"; + } + // Moving cursor back a few lines. + echo "\033[".count($stats).'A'; +} + +function write($vcard) +{ + global $stats, $output; + + ++$stats['Total written']; + fwrite($output, $vcard->serialize()."\n"); +} + +while ($vcard = $splitter->getNext()) { + ++$stats['Total vcards']; + writeStats(); + + $fn = isset($vcard->FN) ? (string) $vcard->FN : null; + + if (empty($fn)) { + // Immediately write this vcard, we don't compare it. + ++$stats['No FN property']; + ++$stats['Unique cards']; + write($vcard); + $vcard->destroy(); + continue; + } + + if (!isset($collectedNames[$fn])) { + $collectedNames[$fn] = $vcard; + ++$stats['Unique cards']; + continue; + } else { + // Starting comparison for all properties. We only check if properties + // in the current vcard exactly appear in the earlier vcard as well. + foreach ($vcard->children() as $newProp) { + if (in_array($newProp->name, $ignoredProperties)) { + // We don't care about properties such as UID and REV. + continue; + } + $ok = false; + foreach ($collectedNames[$fn]->select($newProp->name) as $compareProp) { + if ($compareProp->serialize() === $newProp->serialize()) { + $ok = true; + break; + } + } + + if (!$ok) { + if ('EMAIL' === $newProp->name || 'TEL' === $newProp->name) { + // We're going to make another attempt to find this + // property, this time just by value. If we find it, we + // consider it a success. + foreach ($collectedNames[$fn]->select($newProp->name) as $compareProp) { + if ($compareProp->getValue() === $newProp->getValue()) { + $ok = true; + break; + } + } + + if (!$ok) { + // Merging the new value in the old vcard. + $collectedNames[$fn]->add(clone $newProp); + $ok = true; + ++$stats['Merged values']; + } + } + } + + if (!$ok) { + // echo $newProp->serialize() . " does not appear in earlier vcard!\n"; + ++$stats['Error']; + if ($debug) { + fwrite($debug, "Missing '".$newProp->name."' property in duplicate. Earlier vcard:\n".$collectedNames[$fn]->serialize()."\n\nLater:\n".$vcard->serialize()."\n\n"); + } + + $vcard->destroy(); + continue 2; + } + } + } + + $vcard->destroy(); + ++$stats['Ignored duplicates']; +} + +foreach ($collectedNames as $vcard) { + // Overwriting any old PRODID + $vcard->PRODID = '-//Sabre//Sabre VObject '.Version::VERSION.'//EN'; + write($vcard); + writeStats(); +} + +echo str_repeat("\n", count($stats)), "\nDone.\n"; diff --git a/lib/vendor/sabre/vobject/bin/rrulebench.php b/lib/vendor/sabre/vobject/bin/rrulebench.php new file mode 100644 index 0000000..6900800 --- /dev/null +++ b/lib/vendor/sabre/vobject/bin/rrulebench.php @@ -0,0 +1,32 @@ +parse->start(); + +echo "Parsing.\n"; +$vobj = Sabre\VObject\Reader::read(fopen($inputFile, 'r')); + +$bench->parse->stop(); + +echo "Expanding.\n"; +$bench->expand->start(); + +$vobj->expand(new DateTime($startDate), new DateTime($endDate)); + +$bench->expand->stop(); + +echo $bench,"\n"; diff --git a/lib/vendor/sabre/vobject/bin/vobject b/lib/vendor/sabre/vobject/bin/vobject new file mode 100755 index 0000000..2aca7e7 --- /dev/null +++ b/lib/vendor/sabre/vobject/bin/vobject @@ -0,0 +1,27 @@ +#!/usr/bin/env php +main($argv)); + diff --git a/lib/vendor/sabre/vobject/composer.json b/lib/vendor/sabre/vobject/composer.json new file mode 100644 index 0000000..9d1b426 --- /dev/null +++ b/lib/vendor/sabre/vobject/composer.json @@ -0,0 +1,107 @@ +{ + "name": "sabre/vobject", + "description" : "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects", + "keywords" : [ + "iCalendar", + "iCal", + "vCalendar", + "vCard", + "jCard", + "jCal", + "ics", + "vcf", + "xCard", + "xCal", + "freebusy", + "recurrence", + "availability", + "rfc2425", + "rfc2426", + "rfc2739", + "rfc4770", + "rfc5545", + "rfc5546", + "rfc6321", + "rfc6350", + "rfc6351", + "rfc6474", + "rfc6638", + "rfc6715", + "rfc6868" + ], + "homepage" : "http://sabre.io/vobject/", + "license" : "BSD-3-Clause", + "require" : { + "php" : "^7.1 || ^8.0", + "ext-mbstring" : "*", + "sabre/xml" : "^2.1 || ^3.0 || ^4.0" + }, + "require-dev" : { + "friendsofphp/php-cs-fixer": "~2.17.1", + "phpunit/phpunit" : "^7.5 || ^8.5 || ^9.6", + "phpunit/php-invoker" : "^2.0 || ^3.1", + "phpstan/phpstan": "^0.12 || ^1.12 || ^2.0" + }, + "suggest" : { + "hoa/bench" : "If you would like to run the benchmark scripts" + }, + "authors" : [ + { + "name" : "Evert Pot", + "email" : "me@evertpot.com", + "homepage" : "http://evertpot.com/", + "role" : "Developer" + }, + { + "name" : "Dominik Tobschall", + "email" : "dominik@fruux.com", + "homepage" : "http://tobschall.de/", + "role" : "Developer" + }, + { + "name" : "Ivan Enderlin", + "email" : "ivan.enderlin@hoa-project.net", + "homepage" : "http://mnt.io/", + "role" : "Developer" + } + ], + "support" : { + "forum" : "https://groups.google.com/group/sabredav-discuss", + "source" : "https://github.com/fruux/sabre-vobject" + }, + "autoload" : { + "psr-4" : { + "Sabre\\VObject\\" : "lib/" + } + }, + "autoload-dev" : { + "psr-4" : { + "Sabre\\VObject\\" : "tests/VObject" + } + }, + "bin" : [ + "bin/vobject", + "bin/generate_vcards" + ], + "extra" : { + "branch-alias" : { + "dev-master" : "4.0.x-dev" + } + }, + "scripts": { + "phpstan": [ + "phpstan analyse lib tests" + ], + "cs-fixer": [ + "php-cs-fixer fix" + ], + "phpunit": [ + "phpunit --configuration tests/phpunit.xml" + ], + "test": [ + "composer phpstan", + "composer cs-fixer", + "composer phpunit" + ] + } +} diff --git a/lib/vendor/sabre/vobject/lib/BirthdayCalendarGenerator.php b/lib/vendor/sabre/vobject/lib/BirthdayCalendarGenerator.php new file mode 100644 index 0000000..fade50e --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/BirthdayCalendarGenerator.php @@ -0,0 +1,172 @@ +setObjects($objects); + } + } + + /** + * Sets the input objects. + * + * You must either supply a vCard as a string or as a Component/VCard object. + * It's also possible to supply an array of strings or objects. + * + * @param mixed $objects + */ + public function setObjects($objects) + { + if (!is_array($objects)) { + $objects = [$objects]; + } + + $this->objects = []; + foreach ($objects as $object) { + if (is_string($object)) { + $vObj = Reader::read($object); + if (!$vObj instanceof Component\VCard) { + throw new \InvalidArgumentException('String could not be parsed as \\Sabre\\VObject\\Component\\VCard by setObjects'); + } + + $this->objects[] = $vObj; + } elseif ($object instanceof Component\VCard) { + $this->objects[] = $object; + } else { + throw new \InvalidArgumentException('You can only pass strings or \\Sabre\\VObject\\Component\\VCard arguments to setObjects'); + } + } + } + + /** + * Sets the output format for the SUMMARY. + * + * @param string $format + */ + public function setFormat($format) + { + $this->format = $format; + } + + /** + * Parses the input data and returns a VCALENDAR. + * + * @return Component/VCalendar + */ + public function getResult() + { + $calendar = new VCalendar(); + + foreach ($this->objects as $object) { + // Skip if there is no BDAY property. + if (!$object->select('BDAY')) { + continue; + } + + // We've seen clients (ez-vcard) putting "BDAY:" properties + // without a value into vCards. If we come across those, we'll + // skip them. + if (empty($object->BDAY->getValue())) { + continue; + } + + // We're always converting to vCard 4.0 so we can rely on the + // VCardConverter handling the X-APPLE-OMIT-YEAR property for us. + $object = $object->convert(Document::VCARD40); + + // Skip if the card has no FN property. + if (!isset($object->FN)) { + continue; + } + + // Skip if the BDAY property is not of the right type. + if (!$object->BDAY instanceof Property\VCard\DateAndOrTime) { + continue; + } + + // Skip if we can't parse the BDAY value. + try { + $dateParts = DateTimeParser::parseVCardDateTime($object->BDAY->getValue()); + } catch (InvalidDataException $e) { + continue; + } + + // Set a year if it's not set. + $unknownYear = false; + + if (!$dateParts['year']) { + $object->BDAY = self::DEFAULT_YEAR.'-'.$dateParts['month'].'-'.$dateParts['date']; + + $unknownYear = true; + } + + // Create event. + $event = $calendar->add('VEVENT', [ + 'SUMMARY' => sprintf($this->format, $object->FN->getValue()), + 'DTSTART' => new \DateTime($object->BDAY->getValue()), + 'RRULE' => 'FREQ=YEARLY', + 'TRANSP' => 'TRANSPARENT', + ]); + + // add VALUE=date + $event->DTSTART['VALUE'] = 'DATE'; + + // Add X-SABRE-BDAY property. + if ($unknownYear) { + $event->add('X-SABRE-BDAY', 'BDAY', [ + 'X-SABRE-VCARD-UID' => $object->UID->getValue(), + 'X-SABRE-VCARD-FN' => $object->FN->getValue(), + 'X-SABRE-OMIT-YEAR' => self::DEFAULT_YEAR, + ]); + } else { + $event->add('X-SABRE-BDAY', 'BDAY', [ + 'X-SABRE-VCARD-UID' => $object->UID->getValue(), + 'X-SABRE-VCARD-FN' => $object->FN->getValue(), + ]); + } + } + + return $calendar; + } +} diff --git a/lib/vendor/sabre/vobject/lib/Cli.php b/lib/vendor/sabre/vobject/lib/Cli.php new file mode 100644 index 0000000..3bde16f --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Cli.php @@ -0,0 +1,705 @@ +stderr) { + $this->stderr = fopen('php://stderr', 'w'); + } + if (!$this->stdout) { + $this->stdout = fopen('php://stdout', 'w'); + } + if (!$this->stdin) { + $this->stdin = fopen('php://stdin', 'r'); + } + + // @codeCoverageIgnoreEnd + + try { + list($options, $positional) = $this->parseArguments($argv); + + if (isset($options['q'])) { + $this->quiet = true; + } + $this->log($this->colorize('green', 'sabre/vobject ').$this->colorize('yellow', Version::VERSION)); + + foreach ($options as $name => $value) { + switch ($name) { + case 'q': + // Already handled earlier. + break; + case 'h': + case 'help': + $this->showHelp(); + + return 0; + break; + case 'format': + switch ($value) { + // jcard/jcal documents + case 'jcard': + case 'jcal': + // specific document versions + case 'vcard21': + case 'vcard30': + case 'vcard40': + case 'icalendar20': + // specific formats + case 'json': + case 'mimedir': + // icalendar/vcad + case 'icalendar': + case 'vcard': + $this->format = $value; + break; + + default: + throw new InvalidArgumentException('Unknown format: '.$value); + } + break; + case 'pretty': + if (version_compare(PHP_VERSION, '5.4.0') >= 0) { + $this->pretty = true; + } + break; + case 'forgiving': + $this->forgiving = true; + break; + case 'inputformat': + switch ($value) { + // json formats + case 'jcard': + case 'jcal': + case 'json': + $this->inputFormat = 'json'; + break; + + // mimedir formats + case 'mimedir': + case 'icalendar': + case 'vcard': + case 'vcard21': + case 'vcard30': + case 'vcard40': + case 'icalendar20': + $this->inputFormat = 'mimedir'; + break; + + default: + throw new InvalidArgumentException('Unknown format: '.$value); + } + break; + default: + throw new InvalidArgumentException('Unknown option: '.$name); + } + } + + if (0 === count($positional)) { + $this->showHelp(); + + return 1; + } + + if (1 === count($positional)) { + throw new InvalidArgumentException('Inputfile is a required argument'); + } + + if (count($positional) > 3) { + throw new InvalidArgumentException('Too many arguments'); + } + + if (!in_array($positional[0], ['validate', 'repair', 'convert', 'color'])) { + throw new InvalidArgumentException('Unknown command: '.$positional[0]); + } + } catch (InvalidArgumentException $e) { + $this->showHelp(); + $this->log('Error: '.$e->getMessage(), 'red'); + + return 1; + } + + $command = $positional[0]; + + $this->inputPath = $positional[1]; + $this->outputPath = isset($positional[2]) ? $positional[2] : '-'; + + if ('-' !== $this->outputPath) { + $this->stdout = fopen($this->outputPath, 'w'); + } + + if (!$this->inputFormat) { + if ('.json' === substr($this->inputPath, -5)) { + $this->inputFormat = 'json'; + } else { + $this->inputFormat = 'mimedir'; + } + } + if (!$this->format) { + if ('.json' === substr($this->outputPath, -5)) { + $this->format = 'json'; + } else { + $this->format = 'mimedir'; + } + } + + $realCode = 0; + + try { + while ($input = $this->readInput()) { + $returnCode = $this->$command($input); + if (0 !== $returnCode) { + $realCode = $returnCode; + } + } + } catch (EofException $e) { + // end of file + } catch (\Exception $e) { + $this->log('Error: '.$e->getMessage(), 'red'); + + return 2; + } + + return $realCode; + } + + /** + * Shows the help message. + */ + protected function showHelp() + { + $this->log('Usage:', 'yellow'); + $this->log(' vobject [options] command [arguments]'); + $this->log(''); + $this->log('Options:', 'yellow'); + $this->log($this->colorize('green', ' -q ')."Don't output anything."); + $this->log($this->colorize('green', ' -help -h ').'Display this help message.'); + $this->log($this->colorize('green', ' --format ').'Convert to a specific format. Must be one of: vcard, vcard21,'); + $this->log($this->colorize('green', ' --forgiving ').'Makes the parser less strict.'); + $this->log(' vcard30, vcard40, icalendar20, jcal, jcard, json, mimedir.'); + $this->log($this->colorize('green', ' --inputformat ').'If the input format cannot be guessed from the extension, it'); + $this->log(' must be specified here.'); + // Only PHP 5.4 and up + if (version_compare(PHP_VERSION, '5.4.0') >= 0) { + $this->log($this->colorize('green', ' --pretty ').'json pretty-print.'); + } + $this->log(''); + $this->log('Commands:', 'yellow'); + $this->log($this->colorize('green', ' validate').' source_file Validates a file for correctness.'); + $this->log($this->colorize('green', ' repair').' source_file [output_file] Repairs a file.'); + $this->log($this->colorize('green', ' convert').' source_file [output_file] Converts a file.'); + $this->log($this->colorize('green', ' color').' source_file Colorize a file, useful for debugging.'); + $this->log( + <<log('Examples:', 'yellow'); + $this->log(' vobject convert contact.vcf contact.json'); + $this->log(' vobject convert --format=vcard40 old.vcf new.vcf'); + $this->log(' vobject convert --inputformat=json --format=mimedir - -'); + $this->log(' vobject color calendar.ics'); + $this->log(''); + $this->log('https://github.com/fruux/sabre-vobject', 'purple'); + } + + /** + * Validates a VObject file. + * + * @return int + */ + protected function validate(Component $vObj) + { + $returnCode = 0; + + switch ($vObj->name) { + case 'VCALENDAR': + $this->log('iCalendar: '.(string) $vObj->VERSION); + break; + case 'VCARD': + $this->log('vCard: '.(string) $vObj->VERSION); + break; + } + + $warnings = $vObj->validate(); + if (!count($warnings)) { + $this->log(' No warnings!'); + } else { + $levels = [ + 1 => 'REPAIRED', + 2 => 'WARNING', + 3 => 'ERROR', + ]; + $returnCode = 2; + foreach ($warnings as $warn) { + $extra = ''; + if ($warn['node'] instanceof Property) { + $extra = ' (property: "'.$warn['node']->name.'")'; + } + $this->log(' ['.$levels[$warn['level']].'] '.$warn['message'].$extra); + } + } + + return $returnCode; + } + + /** + * Repairs a VObject file. + * + * @return int + */ + protected function repair(Component $vObj) + { + $returnCode = 0; + + switch ($vObj->name) { + case 'VCALENDAR': + $this->log('iCalendar: '.(string) $vObj->VERSION); + break; + case 'VCARD': + $this->log('vCard: '.(string) $vObj->VERSION); + break; + } + + $warnings = $vObj->validate(Node::REPAIR); + if (!count($warnings)) { + $this->log(' No warnings!'); + } else { + $levels = [ + 1 => 'REPAIRED', + 2 => 'WARNING', + 3 => 'ERROR', + ]; + $returnCode = 2; + foreach ($warnings as $warn) { + $extra = ''; + if ($warn['node'] instanceof Property) { + $extra = ' (property: "'.$warn['node']->name.'")'; + } + $this->log(' ['.$levels[$warn['level']].'] '.$warn['message'].$extra); + } + } + fwrite($this->stdout, $vObj->serialize()); + + return $returnCode; + } + + /** + * Converts a vObject file to a new format. + * + * @param Component $vObj + * + * @return int + */ + protected function convert($vObj) + { + $json = false; + $convertVersion = null; + $forceInput = null; + + switch ($this->format) { + case 'json': + $json = true; + if ('VCARD' === $vObj->name) { + $convertVersion = Document::VCARD40; + } + break; + case 'jcard': + $json = true; + $forceInput = 'VCARD'; + $convertVersion = Document::VCARD40; + break; + case 'jcal': + $json = true; + $forceInput = 'VCALENDAR'; + break; + case 'mimedir': + case 'icalendar': + case 'icalendar20': + case 'vcard': + break; + case 'vcard21': + $convertVersion = Document::VCARD21; + break; + case 'vcard30': + $convertVersion = Document::VCARD30; + break; + case 'vcard40': + $convertVersion = Document::VCARD40; + break; + } + + if ($forceInput && $vObj->name !== $forceInput) { + throw new \Exception('You cannot convert a '.strtolower($vObj->name).' to '.$this->format); + } + if ($convertVersion) { + $vObj = $vObj->convert($convertVersion); + } + if ($json) { + $jsonOptions = 0; + if ($this->pretty) { + $jsonOptions = JSON_PRETTY_PRINT; + } + fwrite($this->stdout, json_encode($vObj->jsonSerialize(), $jsonOptions)); + } else { + fwrite($this->stdout, $vObj->serialize()); + } + + return 0; + } + + /** + * Colorizes a file. + * + * @param Component $vObj + */ + protected function color($vObj) + { + $this->serializeComponent($vObj); + } + + /** + * Returns an ansi color string for a color name. + * + * @param string $color + * + * @return string + */ + protected function colorize($color, $str, $resetTo = 'default') + { + $colors = [ + 'cyan' => '1;36', + 'red' => '1;31', + 'yellow' => '1;33', + 'blue' => '0;34', + 'green' => '0;32', + 'default' => '0', + 'purple' => '0;35', + ]; + + return "\033[".$colors[$color].'m'.$str."\033[".$colors[$resetTo].'m'; + } + + /** + * Writes out a string in specific color. + * + * @param string $color + * @param string $str + */ + protected function cWrite($color, $str) + { + fwrite($this->stdout, $this->colorize($color, $str)); + } + + protected function serializeComponent(Component $vObj) + { + $this->cWrite('cyan', 'BEGIN'); + $this->cWrite('red', ':'); + $this->cWrite('yellow', $vObj->name."\n"); + + /** + * Gives a component a 'score' for sorting purposes. + * + * This is solely used by the childrenSort method. + * + * A higher score means the item will be lower in the list. + * To avoid score collisions, each "score category" has a reasonable + * space to accommodate elements. The $key is added to the $score to + * preserve the original relative order of elements. + * + * @param int $key + * @param array $array + * + * @return int + */ + $sortScore = function ($key, $array) { + if ($array[$key] instanceof Component) { + // We want to encode VTIMEZONE first, this is a personal + // preference. + if ('VTIMEZONE' === $array[$key]->name) { + $score = 300000000; + + return $score + $key; + } else { + $score = 400000000; + + return $score + $key; + } + } else { + // Properties get encoded first + // VCARD version 4.0 wants the VERSION property to appear first + if ($array[$key] instanceof Property) { + if ('VERSION' === $array[$key]->name) { + $score = 100000000; + + return $score + $key; + } else { + // All other properties + $score = 200000000; + + return $score + $key; + } + } + } + }; + + $children = $vObj->children(); + $tmp = $children; + uksort( + $children, + function ($a, $b) use ($sortScore, $tmp) { + $sA = $sortScore($a, $tmp); + $sB = $sortScore($b, $tmp); + + return $sA - $sB; + } + ); + + foreach ($children as $child) { + if ($child instanceof Component) { + $this->serializeComponent($child); + } else { + $this->serializeProperty($child); + } + } + + $this->cWrite('cyan', 'END'); + $this->cWrite('red', ':'); + $this->cWrite('yellow', $vObj->name."\n"); + } + + /** + * Colorizes a property. + */ + protected function serializeProperty(Property $property) + { + if ($property->group) { + $this->cWrite('default', $property->group); + $this->cWrite('red', '.'); + } + + $this->cWrite('yellow', $property->name); + + foreach ($property->parameters as $param) { + $this->cWrite('red', ';'); + $this->cWrite('blue', $param->serialize()); + } + $this->cWrite('red', ':'); + + if ($property instanceof Property\Binary) { + $this->cWrite('default', 'embedded binary stripped. ('.strlen($property->getValue()).' bytes)'); + } else { + $parts = $property->getParts(); + $first1 = true; + // Looping through property values + foreach ($parts as $part) { + if ($first1) { + $first1 = false; + } else { + $this->cWrite('red', $property->delimiter); + } + $first2 = true; + // Looping through property sub-values + foreach ((array) $part as $subPart) { + if ($first2) { + $first2 = false; + } else { + // The sub-value delimiter is always comma + $this->cWrite('red', ','); + } + + $subPart = strtr( + $subPart, + [ + '\\' => $this->colorize('purple', '\\\\', 'green'), + ';' => $this->colorize('purple', '\;', 'green'), + ',' => $this->colorize('purple', '\,', 'green'), + "\n" => $this->colorize('purple', "\\n\n\t", 'green'), + "\r" => '', + ] + ); + + $this->cWrite('green', $subPart); + } + } + } + $this->cWrite('default', "\n"); + } + + /** + * Parses the list of arguments. + */ + protected function parseArguments(array $argv) + { + $positional = []; + $options = []; + + for ($ii = 0; $ii < count($argv); ++$ii) { + // Skipping the first argument. + if (0 === $ii) { + continue; + } + + $v = $argv[$ii]; + + if ('--' === substr($v, 0, 2)) { + // This is a long-form option. + $optionName = substr($v, 2); + $optionValue = true; + if (strpos($optionName, '=')) { + list($optionName, $optionValue) = explode('=', $optionName); + } + $options[$optionName] = $optionValue; + } elseif ('-' === substr($v, 0, 1) && strlen($v) > 1) { + // This is a short-form option. + foreach (str_split(substr($v, 1)) as $option) { + $options[$option] = true; + } + } else { + $positional[] = $v; + } + } + + return [$options, $positional]; + } + + protected $parser; + + /** + * Reads the input file. + * + * @return Component + */ + protected function readInput() + { + if (!$this->parser) { + if ('-' !== $this->inputPath) { + $this->stdin = fopen($this->inputPath, 'r'); + } + + if ('mimedir' === $this->inputFormat) { + $this->parser = new Parser\MimeDir($this->stdin, ($this->forgiving ? Reader::OPTION_FORGIVING : 0)); + } else { + $this->parser = new Parser\Json($this->stdin, ($this->forgiving ? Reader::OPTION_FORGIVING : 0)); + } + } + + return $this->parser->parse(); + } + + /** + * Sends a message to STDERR. + * + * @param string $msg + */ + protected function log($msg, $color = 'default') + { + if (!$this->quiet) { + if ('default' !== $color) { + $msg = $this->colorize($color, $msg); + } + fwrite($this->stderr, $msg."\n"); + } + } +} diff --git a/lib/vendor/sabre/vobject/lib/Component.php b/lib/vendor/sabre/vobject/lib/Component.php new file mode 100644 index 0000000..ca82ad4 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Component.php @@ -0,0 +1,672 @@ + + */ + protected $children = []; + + /** + * Creates a new component. + * + * You can specify the children either in key=>value syntax, in which case + * properties will automatically be created, or you can just pass a list of + * Component and Property object. + * + * By default, a set of sensible values will be added to the component. For + * an iCalendar object, this may be something like CALSCALE:GREGORIAN. To + * ensure that this does not happen, set $defaults to false. + * + * @param string|null $name such as VCALENDAR, VEVENT + * @param bool $defaults + */ + public function __construct(Document $root, $name, array $children = [], $defaults = true) + { + $this->name = isset($name) ? strtoupper($name) : ''; + $this->root = $root; + + if ($defaults) { + // This is a terribly convoluted way to do this, but this ensures + // that the order of properties as they are specified in both + // defaults and the childrens list, are inserted in the object in a + // natural way. + $list = $this->getDefaults(); + $nodes = []; + foreach ($children as $key => $value) { + if ($value instanceof Node) { + if (isset($list[$value->name])) { + unset($list[$value->name]); + } + $nodes[] = $value; + } else { + $list[$key] = $value; + } + } + foreach ($list as $key => $value) { + $this->add($key, $value); + } + foreach ($nodes as $node) { + $this->add($node); + } + } else { + foreach ($children as $k => $child) { + if ($child instanceof Node) { + // Component or Property + $this->add($child); + } else { + // Property key=>value + $this->add($k, $child); + } + } + } + } + + /** + * Adds a new property or component, and returns the new item. + * + * This method has 3 possible signatures: + * + * add(Component $comp) // Adds a new component + * add(Property $prop) // Adds a new property + * add($name, $value, array $parameters = []) // Adds a new property + * add($name, array $children = []) // Adds a new component + * by name. + * + * @return Node + */ + public function add() + { + $arguments = func_get_args(); + + if ($arguments[0] instanceof Node) { + if (isset($arguments[1])) { + throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject Node'); + } + $arguments[0]->parent = $this; + $newNode = $arguments[0]; + } elseif (is_string($arguments[0])) { + $newNode = call_user_func_array([$this->root, 'create'], $arguments); + } else { + throw new \InvalidArgumentException('The first argument must either be a \\Sabre\\VObject\\Node or a string'); + } + + $name = $newNode->name; + if (isset($this->children[$name])) { + $this->children[$name][] = $newNode; + } else { + $this->children[$name] = [$newNode]; + } + + return $newNode; + } + + /** + * This method removes a component or property from this component. + * + * You can either specify the item by name (like DTSTART), in which case + * all properties/components with that name will be removed, or you can + * pass an instance of a property or component, in which case only that + * exact item will be removed. + * + * @param string|Property|Component $item + */ + public function remove($item) + { + if (is_string($item)) { + // If there's no dot in the name, it's an exact property name and + // we can just wipe out all those properties. + // + if (false === strpos($item, '.')) { + unset($this->children[strtoupper($item)]); + + return; + } + // If there was a dot, we need to ask select() to help us out and + // then we just call remove recursively. + foreach ($this->select($item) as $child) { + $this->remove($child); + } + } else { + foreach ($this->select($item->name) as $k => $child) { + if ($child === $item) { + unset($this->children[$item->name][$k]); + + return; + } + } + + throw new \InvalidArgumentException('The item you passed to remove() was not a child of this component'); + } + } + + /** + * Returns a flat list of all the properties and components in this + * component. + * + * @return array + */ + public function children() + { + $result = []; + foreach ($this->children as $childGroup) { + $result = array_merge($result, $childGroup); + } + + return $result; + } + + /** + * This method only returns a list of sub-components. Properties are + * ignored. + * + * @return array + */ + public function getComponents() + { + $result = []; + + foreach ($this->children as $childGroup) { + foreach ($childGroup as $child) { + if ($child instanceof self) { + $result[] = $child; + } + } + } + + return $result; + } + + /** + * Returns an array with elements that match the specified name. + * + * This function is also aware of MIME-Directory groups (as they appear in + * vcards). This means that if a property is grouped as "HOME.EMAIL", it + * will also be returned when searching for just "EMAIL". If you want to + * search for a property in a specific group, you can select on the entire + * string ("HOME.EMAIL"). If you want to search on a specific property that + * has not been assigned a group, specify ".EMAIL". + * + * @param string $name + * + * @return array + */ + public function select($name) + { + $group = null; + $name = strtoupper($name); + if (false !== strpos($name, '.')) { + list($group, $name) = explode('.', $name, 2); + } + if ('' === $name) { + $name = null; + } + + if (!is_null($name)) { + $result = isset($this->children[$name]) ? $this->children[$name] : []; + + if (is_null($group)) { + return $result; + } else { + // If we have a group filter as well, we need to narrow it down + // more. + return array_filter( + $result, + function ($child) use ($group) { + return $child instanceof Property && (null !== $child->group ? strtoupper($child->group) : '') === $group; + } + ); + } + } + + // If we got to this point, it means there was no 'name' specified for + // searching, implying that this is a group-only search. + $result = []; + foreach ($this->children as $childGroup) { + foreach ($childGroup as $child) { + if ($child instanceof Property && (null !== $child->group ? strtoupper($child->group) : '') === $group) { + $result[] = $child; + } + } + } + + return $result; + } + + /** + * Turns the object back into a serialized blob. + * + * @return string + */ + public function serialize() + { + $str = 'BEGIN:'.$this->name."\r\n"; + + /** + * Gives a component a 'score' for sorting purposes. + * + * This is solely used by the childrenSort method. + * + * A higher score means the item will be lower in the list. + * To avoid score collisions, each "score category" has a reasonable + * space to accommodate elements. The $key is added to the $score to + * preserve the original relative order of elements. + * + * @param int $key + * @param array $array + * + * @return int + */ + $sortScore = function ($key, $array) { + if ($array[$key] instanceof Component) { + // We want to encode VTIMEZONE first, this is a personal + // preference. + if ('VTIMEZONE' === $array[$key]->name) { + $score = 300000000; + + return $score + $key; + } else { + $score = 400000000; + + return $score + $key; + } + } else { + // Properties get encoded first + // VCARD version 4.0 wants the VERSION property to appear first + if ($array[$key] instanceof Property) { + if ('VERSION' === $array[$key]->name) { + $score = 100000000; + + return $score + $key; + } else { + // All other properties + $score = 200000000; + + return $score + $key; + } + } + } + }; + + $children = $this->children(); + $tmp = $children; + uksort( + $children, + function ($a, $b) use ($sortScore, $tmp) { + $sA = $sortScore($a, $tmp); + $sB = $sortScore($b, $tmp); + + return $sA - $sB; + } + ); + + foreach ($children as $child) { + $str .= $child->serialize(); + } + $str .= 'END:'.$this->name."\r\n"; + + return $str; + } + + /** + * This method returns an array, with the representation as it should be + * encoded in JSON. This is used to create jCard or jCal documents. + * + * @return array + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $components = []; + $properties = []; + + foreach ($this->children as $childGroup) { + foreach ($childGroup as $child) { + if ($child instanceof self) { + $components[] = $child->jsonSerialize(); + } else { + $properties[] = $child->jsonSerialize(); + } + } + } + + return [ + strtolower($this->name), + $properties, + $components, + ]; + } + + /** + * This method serializes the data into XML. This is used to create xCard or + * xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + public function xmlSerialize(Xml\Writer $writer): void + { + $components = []; + $properties = []; + + foreach ($this->children as $childGroup) { + foreach ($childGroup as $child) { + if ($child instanceof self) { + $components[] = $child; + } else { + $properties[] = $child; + } + } + } + + $writer->startElement(strtolower($this->name)); + + if (!empty($properties)) { + $writer->startElement('properties'); + + foreach ($properties as $property) { + $property->xmlSerialize($writer); + } + + $writer->endElement(); + } + + if (!empty($components)) { + $writer->startElement('components'); + + foreach ($components as $component) { + $component->xmlSerialize($writer); + } + + $writer->endElement(); + } + + $writer->endElement(); + } + + /** + * This method should return a list of default property values. + * + * @return array + */ + protected function getDefaults() + { + return []; + } + + /* Magic property accessors {{{ */ + + /** + * Using 'get' you will either get a property or component. + * + * If there were no child-elements found with the specified name, + * null is returned. + * + * To use this, this may look something like this: + * + * $event = $calendar->VEVENT; + * + * @param string $name + * + * @return Property|null + */ + public function __get($name) + { + if ('children' === $name) { + throw new \RuntimeException('Starting sabre/vobject 4.0 the children property is now protected. You should use the children() method instead'); + } + + $matches = $this->select($name); + if (0 === count($matches)) { + return; + } else { + $firstMatch = current($matches); + /* @var $firstMatch Property */ + $firstMatch->setIterator(new ElementList(array_values($matches))); + + return $firstMatch; + } + } + + /** + * This method checks if a sub-element with the specified name exists. + * + * @param string $name + * + * @return bool + */ + public function __isset($name) + { + $matches = $this->select($name); + + return count($matches) > 0; + } + + /** + * Using the setter method you can add properties or subcomponents. + * + * You can either pass a Component, Property + * object, or a string to automatically create a Property. + * + * If the item already exists, it will be removed. If you want to add + * a new item with the same name, always use the add() method. + * + * @param string $name + * @param mixed $value + */ + public function __set($name, $value) + { + $name = strtoupper($name); + $this->remove($name); + if ($value instanceof self || $value instanceof Property) { + $this->add($value); + } else { + $this->add($name, $value); + } + } + + /** + * Removes all properties and components within this component with the + * specified name. + * + * @param string $name + */ + public function __unset($name) + { + $this->remove($name); + } + + /* }}} */ + + /** + * This method is automatically called when the object is cloned. + * Specifically, this will ensure all child elements are also cloned. + */ + public function __clone() + { + foreach ($this->children as $childName => $childGroup) { + foreach ($childGroup as $key => $child) { + $clonedChild = clone $child; + $clonedChild->parent = $this; + $clonedChild->root = $this->root; + $this->children[$childName][$key] = $clonedChild; + } + } + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * It is also possible to specify defaults and severity levels for + * violating the rule. + * + * See the VEVENT implementation for getValidationRules for a more complex + * example. + * + * @var array + */ + public function getValidationRules() + { + return []; + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes. + * Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on). + * 2 - A warning. + * 3 - An error. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $rules = $this->getValidationRules(); + $defaults = $this->getDefaults(); + + $propertyCounters = []; + + $messages = []; + + foreach ($this->children() as $child) { + $name = strtoupper($child->name); + if (!isset($propertyCounters[$name])) { + $propertyCounters[$name] = 1; + } else { + ++$propertyCounters[$name]; + } + $messages = array_merge($messages, $child->validate($options)); + } + + foreach ($rules as $propName => $rule) { + switch ($rule) { + case '0': + if (isset($propertyCounters[$propName])) { + $messages[] = [ + 'level' => 3, + 'message' => $propName.' MUST NOT appear in a '.$this->name.' component', + 'node' => $this, + ]; + } + break; + case '1': + if (!isset($propertyCounters[$propName]) || 1 !== $propertyCounters[$propName]) { + $repaired = false; + if ($options & self::REPAIR && isset($defaults[$propName])) { + $this->add($propName, $defaults[$propName]); + $repaired = true; + } + $messages[] = [ + 'level' => $repaired ? 1 : 3, + 'message' => $propName.' MUST appear exactly once in a '.$this->name.' component', + 'node' => $this, + ]; + } + break; + case '+': + if (!isset($propertyCounters[$propName]) || $propertyCounters[$propName] < 1) { + $messages[] = [ + 'level' => 3, + 'message' => $propName.' MUST appear at least once in a '.$this->name.' component', + 'node' => $this, + ]; + } + break; + case '*': + break; + case '?': + if (isset($propertyCounters[$propName]) && $propertyCounters[$propName] > 1) { + $level = 3; + + // We try to repair the same property appearing multiple times with the exact same value + // by removing the duplicates and keeping only one property + if ($options & self::REPAIR) { + $properties = array_unique($this->select($propName), SORT_REGULAR); + + if (1 === count($properties)) { + $this->remove($propName); + $this->add($properties[0]); + + $level = 1; + } + } + + $messages[] = [ + 'level' => $level, + 'message' => $propName.' MUST NOT appear more than once in a '.$this->name.' component', + 'node' => $this, + ]; + } + break; + } + } + + return $messages; + } + + /** + * Call this method on a document if you're done using it. + * + * It's intended to remove all circular references, so PHP can easily clean + * it up. + */ + public function destroy() + { + parent::destroy(); + foreach ($this->children as $childGroup) { + foreach ($childGroup as $child) { + $child->destroy(); + } + } + $this->children = []; + } +} diff --git a/lib/vendor/sabre/vobject/lib/Component/Available.php b/lib/vendor/sabre/vobject/lib/Component/Available.php new file mode 100644 index 0000000..5510b9e --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Component/Available.php @@ -0,0 +1,123 @@ +DTSTART->getDateTime(); + if (isset($this->DTEND)) { + $effectiveEnd = $this->DTEND->getDateTime(); + } else { + $effectiveEnd = $effectiveStart->add(VObject\DateTimeParser::parseDuration($this->DURATION)); + } + + return [$effectiveStart, $effectiveEnd]; + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * @var array + */ + public function getValidationRules() + { + return [ + 'UID' => 1, + 'DTSTART' => 1, + 'DTSTAMP' => 1, + + 'DTEND' => '?', + 'DURATION' => '?', + + 'CREATED' => '?', + 'DESCRIPTION' => '?', + 'LAST-MODIFIED' => '?', + 'RECURRENCE-ID' => '?', + 'RRULE' => '?', + 'SUMMARY' => '?', + + 'CATEGORIES' => '*', + 'COMMENT' => '*', + 'CONTACT' => '*', + 'EXDATE' => '*', + 'RDATE' => '*', + + 'AVAILABLE' => '*', + ]; + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes. + * Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on). + * 2 - A warning. + * 3 - An error. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $result = parent::validate($options); + + if (isset($this->DTEND) && isset($this->DURATION)) { + $result[] = [ + 'level' => 3, + 'message' => 'DTEND and DURATION cannot both be present', + 'node' => $this, + ]; + } + + return $result; + } +} diff --git a/lib/vendor/sabre/vobject/lib/Component/VAlarm.php b/lib/vendor/sabre/vobject/lib/Component/VAlarm.php new file mode 100644 index 0000000..bd00eb6 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Component/VAlarm.php @@ -0,0 +1,138 @@ +TRIGGER; + if (!isset($trigger['VALUE']) || 'DURATION' === strtoupper($trigger['VALUE'])) { + $triggerDuration = VObject\DateTimeParser::parseDuration($this->TRIGGER); + $related = (isset($trigger['RELATED']) && 'END' == strtoupper($trigger['RELATED'])) ? 'END' : 'START'; + + $parentComponent = $this->parent; + if ('START' === $related) { + if ('VTODO' === $parentComponent->name) { + $propName = 'DUE'; + } else { + $propName = 'DTSTART'; + } + + $effectiveTrigger = $parentComponent->$propName->getDateTime(); + $effectiveTrigger = $effectiveTrigger->add($triggerDuration); + } else { + if ('VTODO' === $parentComponent->name) { + $endProp = 'DUE'; + } elseif ('VEVENT' === $parentComponent->name) { + $endProp = 'DTEND'; + } else { + throw new InvalidDataException('time-range filters on VALARM components are only supported when they are a child of VTODO or VEVENT'); + } + + if (isset($parentComponent->$endProp)) { + $effectiveTrigger = $parentComponent->$endProp->getDateTime(); + $effectiveTrigger = $effectiveTrigger->add($triggerDuration); + } elseif (isset($parentComponent->DURATION)) { + $effectiveTrigger = $parentComponent->DTSTART->getDateTime(); + $duration = VObject\DateTimeParser::parseDuration($parentComponent->DURATION); + $effectiveTrigger = $effectiveTrigger->add($duration); + $effectiveTrigger = $effectiveTrigger->add($triggerDuration); + } else { + $effectiveTrigger = $parentComponent->DTSTART->getDateTime(); + $effectiveTrigger = $effectiveTrigger->add($triggerDuration); + } + } + } else { + $effectiveTrigger = $trigger->getDateTime(); + } + + return $effectiveTrigger; + } + + /** + * Returns true or false depending on if the event falls in the specified + * time-range. This is used for filtering purposes. + * + * The rules used to determine if an event falls within the specified + * time-range is based on the CalDAV specification. + * + * @param DateTime $start + * @param DateTime $end + * + * @return bool + */ + public function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end) + { + $effectiveTrigger = $this->getEffectiveTriggerTime(); + + if (isset($this->DURATION)) { + $duration = VObject\DateTimeParser::parseDuration($this->DURATION); + $repeat = (string) $this->REPEAT; + if (!$repeat) { + $repeat = 1; + } + + $period = new \DatePeriod($effectiveTrigger, $duration, (int) $repeat); + + foreach ($period as $occurrence) { + if ($start <= $occurrence && $end > $occurrence) { + return true; + } + } + + return false; + } else { + return $start <= $effectiveTrigger && $end > $effectiveTrigger; + } + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * @var array + */ + public function getValidationRules() + { + return [ + 'ACTION' => 1, + 'TRIGGER' => 1, + + 'DURATION' => '?', + 'REPEAT' => '?', + + 'ATTACH' => '?', + ]; + } +} diff --git a/lib/vendor/sabre/vobject/lib/Component/VAvailability.php b/lib/vendor/sabre/vobject/lib/Component/VAvailability.php new file mode 100644 index 0000000..04ec38d --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Component/VAvailability.php @@ -0,0 +1,149 @@ +getEffectiveStartEnd(); + + return + (is_null($effectiveStart) || $start < $effectiveEnd) && + (is_null($effectiveEnd) || $end > $effectiveStart) + ; + } + + /** + * Returns the 'effective start' and 'effective end' of this VAVAILABILITY + * component. + * + * We use the DTSTART and DTEND or DURATION to determine this. + * + * The returned value is an array containing DateTimeImmutable instances. + * If either the start or end is 'unbounded' its value will be null + * instead. + * + * @return array + */ + public function getEffectiveStartEnd() + { + $effectiveStart = null; + $effectiveEnd = null; + + if (isset($this->DTSTART)) { + $effectiveStart = $this->DTSTART->getDateTime(); + } + if (isset($this->DTEND)) { + $effectiveEnd = $this->DTEND->getDateTime(); + } elseif ($effectiveStart && isset($this->DURATION)) { + $effectiveEnd = $effectiveStart->add(VObject\DateTimeParser::parseDuration($this->DURATION)); + } + + return [$effectiveStart, $effectiveEnd]; + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * @var array + */ + public function getValidationRules() + { + return [ + 'UID' => 1, + 'DTSTAMP' => 1, + + 'BUSYTYPE' => '?', + 'CLASS' => '?', + 'CREATED' => '?', + 'DESCRIPTION' => '?', + 'DTSTART' => '?', + 'LAST-MODIFIED' => '?', + 'ORGANIZER' => '?', + 'PRIORITY' => '?', + 'SEQUENCE' => '?', + 'SUMMARY' => '?', + 'URL' => '?', + 'DTEND' => '?', + 'DURATION' => '?', + + 'CATEGORIES' => '*', + 'COMMENT' => '*', + 'CONTACT' => '*', + ]; + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes. + * Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on). + * 2 - A warning. + * 3 - An error. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $result = parent::validate($options); + + if (isset($this->DTEND) && isset($this->DURATION)) { + $result[] = [ + 'level' => 3, + 'message' => 'DTEND and DURATION cannot both be present', + 'node' => $this, + ]; + } + + return $result; + } +} diff --git a/lib/vendor/sabre/vobject/lib/Component/VCalendar.php b/lib/vendor/sabre/vobject/lib/Component/VCalendar.php new file mode 100644 index 0000000..017aed7 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Component/VCalendar.php @@ -0,0 +1,528 @@ + self::class, + 'VALARM' => VAlarm::class, + 'VEVENT' => VEvent::class, + 'VFREEBUSY' => VFreeBusy::class, + 'VAVAILABILITY' => VAvailability::class, + 'AVAILABLE' => Available::class, + 'VJOURNAL' => VJournal::class, + 'VTIMEZONE' => VTimeZone::class, + 'VTODO' => VTodo::class, + ]; + + /** + * List of value-types, and which classes they map to. + * + * @var array + */ + public static $valueMap = [ + 'BINARY' => VObject\Property\Binary::class, + 'BOOLEAN' => VObject\Property\Boolean::class, + 'CAL-ADDRESS' => VObject\Property\ICalendar\CalAddress::class, + 'DATE' => VObject\Property\ICalendar\Date::class, + 'DATE-TIME' => VObject\Property\ICalendar\DateTime::class, + 'DURATION' => VObject\Property\ICalendar\Duration::class, + 'FLOAT' => VObject\Property\FloatValue::class, + 'INTEGER' => VObject\Property\IntegerValue::class, + 'PERIOD' => VObject\Property\ICalendar\Period::class, + 'RECUR' => VObject\Property\ICalendar\Recur::class, + 'TEXT' => VObject\Property\Text::class, + 'TIME' => VObject\Property\Time::class, + 'UNKNOWN' => VObject\Property\Unknown::class, // jCard / jCal-only. + 'URI' => VObject\Property\Uri::class, + 'UTC-OFFSET' => VObject\Property\UtcOffset::class, + ]; + + /** + * List of properties, and which classes they map to. + * + * @var array + */ + public static $propertyMap = [ + // Calendar properties + 'CALSCALE' => VObject\Property\FlatText::class, + 'METHOD' => VObject\Property\FlatText::class, + 'PRODID' => VObject\Property\FlatText::class, + 'VERSION' => VObject\Property\FlatText::class, + + // Component properties + 'ATTACH' => VObject\Property\Uri::class, + 'CATEGORIES' => VObject\Property\Text::class, + 'CLASS' => VObject\Property\FlatText::class, + 'COMMENT' => VObject\Property\FlatText::class, + 'DESCRIPTION' => VObject\Property\FlatText::class, + 'GEO' => VObject\Property\FloatValue::class, + 'LOCATION' => VObject\Property\FlatText::class, + 'PERCENT-COMPLETE' => VObject\Property\IntegerValue::class, + 'PRIORITY' => VObject\Property\IntegerValue::class, + 'RESOURCES' => VObject\Property\Text::class, + 'STATUS' => VObject\Property\FlatText::class, + 'SUMMARY' => VObject\Property\FlatText::class, + + // Date and Time Component Properties + 'COMPLETED' => VObject\Property\ICalendar\DateTime::class, + 'DTEND' => VObject\Property\ICalendar\DateTime::class, + 'DUE' => VObject\Property\ICalendar\DateTime::class, + 'DTSTART' => VObject\Property\ICalendar\DateTime::class, + 'DURATION' => VObject\Property\ICalendar\Duration::class, + 'FREEBUSY' => VObject\Property\ICalendar\Period::class, + 'TRANSP' => VObject\Property\FlatText::class, + + // Time Zone Component Properties + 'TZID' => VObject\Property\FlatText::class, + 'TZNAME' => VObject\Property\FlatText::class, + 'TZOFFSETFROM' => VObject\Property\UtcOffset::class, + 'TZOFFSETTO' => VObject\Property\UtcOffset::class, + 'TZURL' => VObject\Property\Uri::class, + + // Relationship Component Properties + 'ATTENDEE' => VObject\Property\ICalendar\CalAddress::class, + 'CONTACT' => VObject\Property\FlatText::class, + 'ORGANIZER' => VObject\Property\ICalendar\CalAddress::class, + 'RECURRENCE-ID' => VObject\Property\ICalendar\DateTime::class, + 'RELATED-TO' => VObject\Property\FlatText::class, + 'URL' => VObject\Property\Uri::class, + 'UID' => VObject\Property\FlatText::class, + + // Recurrence Component Properties + 'EXDATE' => VObject\Property\ICalendar\DateTime::class, + 'RDATE' => VObject\Property\ICalendar\DateTime::class, + 'RRULE' => VObject\Property\ICalendar\Recur::class, + 'EXRULE' => VObject\Property\ICalendar\Recur::class, // Deprecated since rfc5545 + + // Alarm Component Properties + 'ACTION' => VObject\Property\FlatText::class, + 'REPEAT' => VObject\Property\IntegerValue::class, + 'TRIGGER' => VObject\Property\ICalendar\Duration::class, + + // Change Management Component Properties + 'CREATED' => VObject\Property\ICalendar\DateTime::class, + 'DTSTAMP' => VObject\Property\ICalendar\DateTime::class, + 'LAST-MODIFIED' => VObject\Property\ICalendar\DateTime::class, + 'SEQUENCE' => VObject\Property\IntegerValue::class, + + // Request Status + 'REQUEST-STATUS' => VObject\Property\Text::class, + + // Additions from draft-daboo-valarm-extensions-04 + 'ALARM-AGENT' => VObject\Property\Text::class, + 'ACKNOWLEDGED' => VObject\Property\ICalendar\DateTime::class, + 'PROXIMITY' => VObject\Property\Text::class, + 'DEFAULT-ALARM' => VObject\Property\Boolean::class, + + // Additions from draft-daboo-calendar-availability-05 + 'BUSYTYPE' => VObject\Property\Text::class, + ]; + + /** + * Returns the current document type. + * + * @return int + */ + public function getDocumentType() + { + return self::ICALENDAR20; + } + + /** + * Returns a list of all 'base components'. For instance, if an Event has + * a recurrence rule, and one instance is overridden, the overridden event + * will have the same UID, but will be excluded from this list. + * + * VTIMEZONE components will always be excluded. + * + * @param string $componentName filter by component name + * + * @return VObject\Component[] + */ + public function getBaseComponents($componentName = null) + { + $isBaseComponent = function ($component) { + if (!$component instanceof VObject\Component) { + return false; + } + if ('VTIMEZONE' === $component->name) { + return false; + } + if (isset($component->{'RECURRENCE-ID'})) { + return false; + } + + return true; + }; + + if ($componentName) { + // Early exit + return array_filter( + $this->select($componentName), + $isBaseComponent + ); + } + + $components = []; + foreach ($this->children as $childGroup) { + foreach ($childGroup as $child) { + if (!$child instanceof Component) { + // If one child is not a component, they all are so we skip + // the entire group. + continue 2; + } + if ($isBaseComponent($child)) { + $components[] = $child; + } + } + } + + return $components; + } + + /** + * Returns the first component that is not a VTIMEZONE, and does not have + * an RECURRENCE-ID. + * + * If there is no such component, null will be returned. + * + * @param string $componentName filter by component name + * + * @return VObject\Component|null + */ + public function getBaseComponent($componentName = null) + { + $isBaseComponent = function ($component) { + if (!$component instanceof VObject\Component) { + return false; + } + if ('VTIMEZONE' === $component->name) { + return false; + } + if (isset($component->{'RECURRENCE-ID'})) { + return false; + } + + return true; + }; + + if ($componentName) { + foreach ($this->select($componentName) as $child) { + if ($isBaseComponent($child)) { + return $child; + } + } + + return null; + } + + // Searching all components + foreach ($this->children as $childGroup) { + foreach ($childGroup as $child) { + if ($isBaseComponent($child)) { + return $child; + } + } + } + + return null; + } + + /** + * Expand all events in this VCalendar object and return a new VCalendar + * with the expanded events. + * + * If this calendar object, has events with recurrence rules, this method + * can be used to expand the event into multiple sub-events. + * + * Each event will be stripped from its recurrence information, and only + * the instances of the event in the specified timerange will be left + * alone. + * + * In addition, this method will cause timezone information to be stripped, + * and normalized to UTC. + * + * @param DateTimeZone $timeZone reference timezone for floating dates and + * times + * + * @return VCalendar + */ + public function expand(DateTimeInterface $start, DateTimeInterface $end, ?DateTimeZone $timeZone = null) + { + $newChildren = []; + $recurringEvents = []; + + if (!$timeZone) { + $timeZone = new DateTimeZone('UTC'); + } + + $stripTimezones = function (Component $component) use ($timeZone, &$stripTimezones) { + foreach ($component->children() as $componentChild) { + if ($componentChild instanceof Property\ICalendar\DateTime && $componentChild->hasTime()) { + $dt = $componentChild->getDateTimes($timeZone); + // We only need to update the first timezone, because + // setDateTimes will match all other timezones to the + // first. + $dt[0] = $dt[0]->setTimeZone(new DateTimeZone('UTC')); + $componentChild->setDateTimes($dt); + } elseif ($componentChild instanceof Component) { + $stripTimezones($componentChild); + } + } + + return $component; + }; + + foreach ($this->children() as $child) { + if ($child instanceof Property && 'PRODID' !== $child->name) { + // We explicitly want to ignore PRODID, because we want to + // overwrite it with our own. + $newChildren[] = clone $child; + } elseif ($child instanceof Component && 'VTIMEZONE' !== $child->name) { + // We're also stripping all VTIMEZONE objects because we're + // converting everything to UTC. + if ('VEVENT' === $child->name && (isset($child->{'RECURRENCE-ID'}) || isset($child->RRULE) || isset($child->RDATE))) { + // Handle these a bit later. + $uid = (string) $child->UID; + if (!$uid) { + throw new InvalidDataException('Every VEVENT object must have a UID property'); + } + if (isset($recurringEvents[$uid])) { + $recurringEvents[$uid][] = clone $child; + } else { + $recurringEvents[$uid] = [clone $child]; + } + } elseif ('VEVENT' === $child->name && $child->isInTimeRange($start, $end)) { + $newChildren[] = $stripTimezones(clone $child); + } + } + } + + foreach ($recurringEvents as $events) { + try { + $it = new EventIterator($events, null, $timeZone); + } catch (NoInstancesException $e) { + // This event is recurring, but it doesn't have a single + // instance. We are skipping this event from the output + // entirely. + continue; + } + $it->fastForward($start); + + while ($it->valid() && $it->getDTStart() < $end) { + if ($it->getDTEnd() > $start) { + $newChildren[] = $stripTimezones($it->getEventObject()); + } + $it->next(); + } + } + + return new self($newChildren); + } + + /** + * This method should return a list of default property values. + * + * @return array + */ + protected function getDefaults() + { + return [ + 'VERSION' => '2.0', + 'PRODID' => '-//Sabre//Sabre VObject '.VObject\Version::VERSION.'//EN', + 'CALSCALE' => 'GREGORIAN', + ]; + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * @var array + */ + public function getValidationRules() + { + return [ + 'PRODID' => 1, + 'VERSION' => 1, + + 'CALSCALE' => '?', + 'METHOD' => '?', + ]; + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes. + * Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on). + * 2 - A warning. + * 3 - An error. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $warnings = parent::validate($options); + + if ($ver = $this->VERSION) { + if ('2.0' !== (string) $ver) { + $warnings[] = [ + 'level' => 3, + 'message' => 'Only iCalendar version 2.0 as defined in rfc5545 is supported.', + 'node' => $this, + ]; + } + } + + $uidList = []; + $componentsFound = 0; + $componentTypes = []; + + foreach ($this->children() as $child) { + if ($child instanceof Component) { + ++$componentsFound; + + if (!in_array($child->name, ['VEVENT', 'VTODO', 'VJOURNAL'])) { + continue; + } + $componentTypes[] = $child->name; + + $uid = (string) $child->UID; + $isMaster = isset($child->{'RECURRENCE-ID'}) ? 0 : 1; + if (isset($uidList[$uid])) { + ++$uidList[$uid]['count']; + if ($isMaster && $uidList[$uid]['hasMaster']) { + $warnings[] = [ + 'level' => 3, + 'message' => 'More than one master object was found for the object with UID '.$uid, + 'node' => $this, + ]; + } + $uidList[$uid]['hasMaster'] += $isMaster; + } else { + $uidList[$uid] = [ + 'count' => 1, + 'hasMaster' => $isMaster, + ]; + } + } + } + + if (0 === $componentsFound) { + $warnings[] = [ + 'level' => 3, + 'message' => 'An iCalendar object must have at least 1 component.', + 'node' => $this, + ]; + } + + if ($options & self::PROFILE_CALDAV) { + if (count($uidList) > 1) { + $warnings[] = [ + 'level' => 3, + 'message' => 'A calendar object on a CalDAV server may only have components with the same UID.', + 'node' => $this, + ]; + } + if (0 === count($componentTypes)) { + $warnings[] = [ + 'level' => 3, + 'message' => 'A calendar object on a CalDAV server must have at least 1 component (VTODO, VEVENT, VJOURNAL).', + 'node' => $this, + ]; + } + if (count(array_unique($componentTypes)) > 1) { + $warnings[] = [ + 'level' => 3, + 'message' => 'A calendar object on a CalDAV server may only have 1 type of component (VEVENT, VTODO or VJOURNAL).', + 'node' => $this, + ]; + } + + if (isset($this->METHOD)) { + $warnings[] = [ + 'level' => 3, + 'message' => 'A calendar object on a CalDAV server MUST NOT have a METHOD property.', + 'node' => $this, + ]; + } + } + + return $warnings; + } + + /** + * Returns all components with a specific UID value. + * + * @return array + */ + public function getByUID($uid) + { + return array_filter($this->getComponents(), function ($item) use ($uid) { + if (!$itemUid = $item->select('UID')) { + return false; + } + $itemUid = current($itemUid)->getValue(); + + return $uid === $itemUid; + }); + } +} diff --git a/lib/vendor/sabre/vobject/lib/Component/VCard.php b/lib/vendor/sabre/vobject/lib/Component/VCard.php new file mode 100644 index 0000000..82fab82 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Component/VCard.php @@ -0,0 +1,541 @@ + VCard::class, + ]; + + /** + * List of value-types, and which classes they map to. + * + * @var array + */ + public static $valueMap = [ + 'BINARY' => VObject\Property\Binary::class, + 'BOOLEAN' => VObject\Property\Boolean::class, + 'CONTENT-ID' => VObject\Property\FlatText::class, // vCard 2.1 only + 'DATE' => VObject\Property\VCard\Date::class, + 'DATE-TIME' => VObject\Property\VCard\DateTime::class, + 'DATE-AND-OR-TIME' => VObject\Property\VCard\DateAndOrTime::class, // vCard only + 'FLOAT' => VObject\Property\FloatValue::class, + 'INTEGER' => VObject\Property\IntegerValue::class, + 'LANGUAGE-TAG' => VObject\Property\VCard\LanguageTag::class, + 'PHONE-NUMBER' => VObject\Property\VCard\PhoneNumber::class, // vCard 3.0 only + 'TIMESTAMP' => VObject\Property\VCard\TimeStamp::class, + 'TEXT' => VObject\Property\Text::class, + 'TIME' => VObject\Property\Time::class, + 'UNKNOWN' => VObject\Property\Unknown::class, // jCard / jCal-only. + 'URI' => VObject\Property\Uri::class, + 'URL' => VObject\Property\Uri::class, // vCard 2.1 only + 'UTC-OFFSET' => VObject\Property\UtcOffset::class, + ]; + + /** + * List of properties, and which classes they map to. + * + * @var array + */ + public static $propertyMap = [ + // vCard 2.1 properties and up + 'N' => VObject\Property\Text::class, + 'FN' => VObject\Property\FlatText::class, + 'PHOTO' => VObject\Property\Binary::class, + 'BDAY' => VObject\Property\VCard\DateAndOrTime::class, + 'ADR' => VObject\Property\Text::class, + 'LABEL' => VObject\Property\FlatText::class, // Removed in vCard 4.0 + 'TEL' => VObject\Property\FlatText::class, + 'EMAIL' => VObject\Property\FlatText::class, + 'MAILER' => VObject\Property\FlatText::class, // Removed in vCard 4.0 + 'GEO' => VObject\Property\FlatText::class, + 'TITLE' => VObject\Property\FlatText::class, + 'ROLE' => VObject\Property\FlatText::class, + 'LOGO' => VObject\Property\Binary::class, + // 'AGENT' => 'Sabre\\VObject\\Property\\', // Todo: is an embedded vCard. Probably rare, so + // not supported at the moment + 'ORG' => VObject\Property\Text::class, + 'NOTE' => VObject\Property\FlatText::class, + 'REV' => VObject\Property\VCard\TimeStamp::class, + 'SOUND' => VObject\Property\FlatText::class, + 'URL' => VObject\Property\Uri::class, + 'UID' => VObject\Property\FlatText::class, + 'VERSION' => VObject\Property\FlatText::class, + 'KEY' => VObject\Property\FlatText::class, + 'TZ' => VObject\Property\Text::class, + + // vCard 3.0 properties + 'CATEGORIES' => VObject\Property\Text::class, + 'SORT-STRING' => VObject\Property\FlatText::class, + 'PRODID' => VObject\Property\FlatText::class, + 'NICKNAME' => VObject\Property\Text::class, + 'CLASS' => VObject\Property\FlatText::class, // Removed in vCard 4.0 + + // rfc2739 properties + 'FBURL' => VObject\Property\Uri::class, + 'CAPURI' => VObject\Property\Uri::class, + 'CALURI' => VObject\Property\Uri::class, + 'CALADRURI' => VObject\Property\Uri::class, + + // rfc4770 properties + 'IMPP' => VObject\Property\Uri::class, + + // vCard 4.0 properties + 'SOURCE' => VObject\Property\Uri::class, + 'XML' => VObject\Property\FlatText::class, + 'ANNIVERSARY' => VObject\Property\VCard\DateAndOrTime::class, + 'CLIENTPIDMAP' => VObject\Property\Text::class, + 'LANG' => VObject\Property\VCard\LanguageTag::class, + 'GENDER' => VObject\Property\Text::class, + 'KIND' => VObject\Property\FlatText::class, + 'MEMBER' => VObject\Property\Uri::class, + 'RELATED' => VObject\Property\Uri::class, + + // rfc6474 properties + 'BIRTHPLACE' => VObject\Property\FlatText::class, + 'DEATHPLACE' => VObject\Property\FlatText::class, + 'DEATHDATE' => VObject\Property\VCard\DateAndOrTime::class, + + // rfc6715 properties + 'EXPERTISE' => VObject\Property\FlatText::class, + 'HOBBY' => VObject\Property\FlatText::class, + 'INTEREST' => VObject\Property\FlatText::class, + 'ORG-DIRECTORY' => VObject\Property\FlatText::class, + ]; + + /** + * Returns the current document type. + * + * @return int + */ + public function getDocumentType() + { + if (!$this->version) { + $version = (string) $this->VERSION; + + switch ($version) { + case '2.1': + $this->version = self::VCARD21; + break; + case '3.0': + $this->version = self::VCARD30; + break; + case '4.0': + $this->version = self::VCARD40; + break; + default: + // We don't want to cache the version if it's unknown, + // because we might get a version property in a bit. + return self::UNKNOWN; + } + } + + return $this->version; + } + + /** + * Converts the document to a different vcard version. + * + * Use one of the VCARD constants for the target. This method will return + * a copy of the vcard in the new version. + * + * At the moment the only supported conversion is from 3.0 to 4.0. + * + * If input and output version are identical, a clone is returned. + * + * @param int $target + * + * @return VCard + */ + public function convert($target) + { + $converter = new VObject\VCardConverter(); + + return $converter->convert($this, $target); + } + + /** + * VCards with version 2.1, 3.0 and 4.0 are found. + * + * If the VCARD doesn't know its version, 2.1 is assumed. + */ + const DEFAULT_VERSION = self::VCARD21; + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on) + * 2 - An inconsequential issue + * 3 - A severe issue. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $warnings = []; + + $versionMap = [ + self::VCARD21 => '2.1', + self::VCARD30 => '3.0', + self::VCARD40 => '4.0', + ]; + + $version = $this->select('VERSION'); + if (1 === count($version)) { + $version = (string) $this->VERSION; + if ('2.1' !== $version && '3.0' !== $version && '4.0' !== $version) { + $warnings[] = [ + 'level' => 3, + 'message' => 'Only vcard version 4.0 (RFC6350), version 3.0 (RFC2426) or version 2.1 (icm-vcard-2.1) are supported.', + 'node' => $this, + ]; + if ($options & self::REPAIR) { + $this->VERSION = $versionMap[self::DEFAULT_VERSION]; + } + } + if ('2.1' === $version && ($options & self::PROFILE_CARDDAV)) { + $warnings[] = [ + 'level' => 3, + 'message' => 'CardDAV servers are not allowed to accept vCard 2.1.', + 'node' => $this, + ]; + } + } + $uid = $this->select('UID'); + if (0 === count($uid)) { + if ($options & self::PROFILE_CARDDAV) { + // Required for CardDAV + $warningLevel = 3; + $message = 'vCards on CardDAV servers MUST have a UID property.'; + } else { + // Not required for regular vcards + $warningLevel = 2; + $message = 'Adding a UID to a vCard property is recommended.'; + } + if ($options & self::REPAIR) { + $this->UID = VObject\UUIDUtil::getUUID(); + $warningLevel = 1; + } + $warnings[] = [ + 'level' => $warningLevel, + 'message' => $message, + 'node' => $this, + ]; + } + + $fn = $this->select('FN'); + if (1 !== count($fn)) { + $repaired = false; + if (($options & self::REPAIR) && 0 === count($fn)) { + // We're going to try to see if we can use the contents of the + // N property. + if (isset($this->N)) { + $value = explode(';', (string) $this->N); + if (isset($value[1]) && $value[1]) { + $this->FN = $value[1].' '.$value[0]; + } else { + $this->FN = $value[0]; + } + $repaired = true; + + // Otherwise, the ORG property may work + } elseif (isset($this->ORG)) { + $this->FN = (string) $this->ORG; + $repaired = true; + + // Otherwise, the NICKNAME property may work + } elseif (isset($this->NICKNAME)) { + $this->FN = (string) $this->NICKNAME; + $repaired = true; + + // Otherwise, the EMAIL property may work + } elseif (isset($this->EMAIL)) { + $this->FN = (string) $this->EMAIL; + $repaired = true; + } + } + $warnings[] = [ + 'level' => $repaired ? 1 : 3, + 'message' => 'The FN property must appear in the VCARD component exactly 1 time', + 'node' => $this, + ]; + } + + return array_merge( + parent::validate($options), + $warnings + ); + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * @var array + */ + public function getValidationRules() + { + return [ + 'ADR' => '*', + 'ANNIVERSARY' => '?', + 'BDAY' => '?', + 'CALADRURI' => '*', + 'CALURI' => '*', + 'CATEGORIES' => '*', + 'CLIENTPIDMAP' => '*', + 'EMAIL' => '*', + 'FBURL' => '*', + 'IMPP' => '*', + 'GENDER' => '?', + 'GEO' => '*', + 'KEY' => '*', + 'KIND' => '?', + 'LANG' => '*', + 'LOGO' => '*', + 'MEMBER' => '*', + 'N' => '?', + 'NICKNAME' => '*', + 'NOTE' => '*', + 'ORG' => '*', + 'PHOTO' => '*', + 'PRODID' => '?', + 'RELATED' => '*', + 'REV' => '?', + 'ROLE' => '*', + 'SOUND' => '*', + 'SOURCE' => '*', + 'TEL' => '*', + 'TITLE' => '*', + 'TZ' => '*', + 'URL' => '*', + 'VERSION' => '1', + 'XML' => '*', + + // FN is commented out, because it's already handled by the + // validate function, which may also try to repair it. + // 'FN' => '+', + 'UID' => '?', + ]; + } + + /** + * Returns a preferred field. + * + * VCards can indicate whether a field such as ADR, TEL or EMAIL is + * preferred by specifying TYPE=PREF (vcard 2.1, 3) or PREF=x (vcard 4, x + * being a number between 1 and 100). + * + * If neither of those parameters are specified, the first is returned, if + * a field with that name does not exist, null is returned. + * + * @param string $fieldName + * + * @return VObject\Property|null + */ + public function preferred($propertyName) + { + $preferred = null; + $lastPref = 101; + foreach ($this->select($propertyName) as $field) { + $pref = 101; + if (isset($field['TYPE']) && $field['TYPE']->has('PREF')) { + $pref = 1; + } elseif (isset($field['PREF'])) { + $pref = $field['PREF']->getValue(); + } + + if ($pref < $lastPref || is_null($preferred)) { + $preferred = $field; + $lastPref = $pref; + } + } + + return $preferred; + } + + /** + * Returns a property with a specific TYPE value (ADR, TEL, or EMAIL). + * + * This function will return null if the property does not exist. If there are + * multiple properties with the same TYPE value, only one will be returned. + * + * @param string $propertyName + * @param string $type + * + * @return VObject\Property|null + */ + public function getByType($propertyName, $type) + { + foreach ($this->select($propertyName) as $field) { + if (isset($field['TYPE']) && $field['TYPE']->has($type)) { + return $field; + } + } + } + + /** + * This method should return a list of default property values. + * + * @return array + */ + protected function getDefaults() + { + return [ + 'VERSION' => '4.0', + 'PRODID' => '-//Sabre//Sabre VObject '.VObject\Version::VERSION.'//EN', + 'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(), + ]; + } + + /** + * This method returns an array, with the representation as it should be + * encoded in json. This is used to create jCard or jCal documents. + * + * @return array + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + // A vcard does not have sub-components, so we're overriding this + // method to remove that array element. + $properties = []; + + foreach ($this->children() as $child) { + $properties[] = $child->jsonSerialize(); + } + + return [ + strtolower($this->name), + $properties, + ]; + } + + /** + * This method serializes the data into XML. This is used to create xCard or + * xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + public function xmlSerialize(Xml\Writer $writer): void + { + $propertiesByGroup = []; + + foreach ($this->children() as $property) { + $group = $property->group; + + if (!isset($propertiesByGroup[$group])) { + $propertiesByGroup[$group] = []; + } + + $propertiesByGroup[$group][] = $property; + } + + $writer->startElement(strtolower($this->name)); + + foreach ($propertiesByGroup as $group => $properties) { + if (!empty($group)) { + $writer->startElement('group'); + $writer->writeAttribute('name', strtolower($group)); + } + + foreach ($properties as $property) { + switch ($property->name) { + case 'VERSION': + break; + + case 'XML': + $value = $property->getParts(); + $fragment = new Xml\Element\XmlFragment($value[0]); + $writer->write($fragment); + break; + + default: + $property->xmlSerialize($writer); + break; + } + } + + if (!empty($group)) { + $writer->endElement(); + } + } + + $writer->endElement(); + } + + /** + * Returns the default class for a property name. + * + * @param string $propertyName + * + * @return string + */ + public function getClassNameForPropertyName($propertyName) + { + $className = parent::getClassNameForPropertyName($propertyName); + + // In vCard 4, BINARY no longer exists, and we need URI instead. + if (VObject\Property\Binary::class == $className && self::VCARD40 === $this->getDocumentType()) { + return VObject\Property\Uri::class; + } + + return $className; + } +} diff --git a/lib/vendor/sabre/vobject/lib/Component/VEvent.php b/lib/vendor/sabre/vobject/lib/Component/VEvent.php new file mode 100644 index 0000000..a385a09 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Component/VEvent.php @@ -0,0 +1,140 @@ +RRULE || $this->RDATE) { + try { + $it = new EventIterator($this, null, $start->getTimezone()); + } catch (NoInstancesException $e) { + // If we've caught this exception, there are no instances + // for the event that fall into the specified time-range. + return false; + } + + $it->fastForward($start); + + // We fast-forwarded to a spot where the end-time of the + // recurrence instance exceeded the start of the requested + // time-range. + // + // If the starttime of the recurrence did not exceed the + // end of the time range as well, we have a match. + return $it->getDTStart() < $end && $it->getDTEnd() > $start; + } + + $effectiveStart = $this->DTSTART->getDateTime($start->getTimezone()); + if (isset($this->DTEND)) { + // The DTEND property is considered non inclusive. So for a 3 day + // event in july, dtstart and dtend would have to be July 1st and + // July 4th respectively. + // + // See: + // http://tools.ietf.org/html/rfc5545#page-54 + $effectiveEnd = $this->DTEND->getDateTime($end->getTimezone()); + } elseif (isset($this->DURATION)) { + $effectiveEnd = $effectiveStart->add(VObject\DateTimeParser::parseDuration($this->DURATION)); + } elseif (!$this->DTSTART->hasTime()) { + $effectiveEnd = $effectiveStart->modify('+1 day'); + } else { + $effectiveEnd = $effectiveStart; + } + + return + ($start < $effectiveEnd) && ($end > $effectiveStart) + ; + } + + /** + * This method should return a list of default property values. + * + * @return array + */ + protected function getDefaults() + { + return [ + 'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(), + 'DTSTAMP' => gmdate('Ymd\\THis\\Z'), + ]; + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * @var array + */ + public function getValidationRules() + { + $hasMethod = isset($this->parent->METHOD); + + return [ + 'UID' => 1, + 'DTSTAMP' => 1, + 'DTSTART' => $hasMethod ? '?' : '1', + 'CLASS' => '?', + 'CREATED' => '?', + 'DESCRIPTION' => '?', + 'GEO' => '?', + 'LAST-MODIFIED' => '?', + 'LOCATION' => '?', + 'ORGANIZER' => '?', + 'PRIORITY' => '?', + 'SEQUENCE' => '?', + 'STATUS' => '?', + 'SUMMARY' => '?', + 'TRANSP' => '?', + 'URL' => '?', + 'RECURRENCE-ID' => '?', + 'RRULE' => '?', + 'DTEND' => '?', + 'DURATION' => '?', + + 'ATTACH' => '*', + 'ATTENDEE' => '*', + 'CATEGORIES' => '*', + 'COMMENT' => '*', + 'CONTACT' => '*', + 'EXDATE' => '*', + 'REQUEST-STATUS' => '*', + 'RELATED-TO' => '*', + 'RESOURCES' => '*', + 'RDATE' => '*', + ]; + } +} diff --git a/lib/vendor/sabre/vobject/lib/Component/VFreeBusy.php b/lib/vendor/sabre/vobject/lib/Component/VFreeBusy.php new file mode 100644 index 0000000..fef418b --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Component/VFreeBusy.php @@ -0,0 +1,93 @@ +select('FREEBUSY') as $freebusy) { + // We are only interested in FBTYPE=BUSY (the default), + // FBTYPE=BUSY-TENTATIVE or FBTYPE=BUSY-UNAVAILABLE. + if (isset($freebusy['FBTYPE']) && 'BUSY' !== strtoupper(substr((string) $freebusy['FBTYPE'], 0, 4))) { + continue; + } + + // The freebusy component can hold more than 1 value, separated by + // commas. + $periods = explode(',', (string) $freebusy); + + foreach ($periods as $period) { + // Every period is formatted as [start]/[end]. The start is an + // absolute UTC time, the end may be an absolute UTC time, or + // duration (relative) value. + list($busyStart, $busyEnd) = explode('/', $period); + + $busyStart = VObject\DateTimeParser::parse($busyStart); + $busyEnd = VObject\DateTimeParser::parse($busyEnd); + if ($busyEnd instanceof \DateInterval) { + $busyEnd = $busyStart->add($busyEnd); + } + + if ($start < $busyEnd && $end > $busyStart) { + return false; + } + } + } + + return true; + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * @var array + */ + public function getValidationRules() + { + return [ + 'UID' => 1, + 'DTSTAMP' => 1, + + 'CONTACT' => '?', + 'DTSTART' => '?', + 'DTEND' => '?', + 'ORGANIZER' => '?', + 'URL' => '?', + + 'ATTENDEE' => '*', + 'COMMENT' => '*', + 'FREEBUSY' => '*', + 'REQUEST-STATUS' => '*', + ]; + } +} diff --git a/lib/vendor/sabre/vobject/lib/Component/VJournal.php b/lib/vendor/sabre/vobject/lib/Component/VJournal.php new file mode 100644 index 0000000..9b7f1b8 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Component/VJournal.php @@ -0,0 +1,101 @@ +DTSTART) ? $this->DTSTART->getDateTime() : null; + if ($dtstart) { + $effectiveEnd = $dtstart; + if (!$this->DTSTART->hasTime()) { + $effectiveEnd = $effectiveEnd->modify('+1 day'); + } + + return $start <= $effectiveEnd && $end > $dtstart; + } + + return false; + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * @var array + */ + public function getValidationRules() + { + return [ + 'UID' => 1, + 'DTSTAMP' => 1, + + 'CLASS' => '?', + 'CREATED' => '?', + 'DTSTART' => '?', + 'LAST-MODIFIED' => '?', + 'ORGANIZER' => '?', + 'RECURRENCE-ID' => '?', + 'SEQUENCE' => '?', + 'STATUS' => '?', + 'SUMMARY' => '?', + 'URL' => '?', + + 'RRULE' => '?', + + 'ATTACH' => '*', + 'ATTENDEE' => '*', + 'CATEGORIES' => '*', + 'COMMENT' => '*', + 'CONTACT' => '*', + 'DESCRIPTION' => '*', + 'EXDATE' => '*', + 'RELATED-TO' => '*', + 'RDATE' => '*', + ]; + } + + /** + * This method should return a list of default property values. + * + * @return array + */ + protected function getDefaults() + { + return [ + 'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(), + 'DTSTAMP' => gmdate('Ymd\\THis\\Z'), + ]; + } +} diff --git a/lib/vendor/sabre/vobject/lib/Component/VTimeZone.php b/lib/vendor/sabre/vobject/lib/Component/VTimeZone.php new file mode 100644 index 0000000..21c0623 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Component/VTimeZone.php @@ -0,0 +1,63 @@ +TZID, $this->root); + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * @var array + */ + public function getValidationRules() + { + return [ + 'TZID' => 1, + + 'LAST-MODIFIED' => '?', + 'TZURL' => '?', + + // At least 1 STANDARD or DAYLIGHT must appear. + // + // The validator is not specific yet to pick this up, so these + // rules are too loose. + 'STANDARD' => '*', + 'DAYLIGHT' => '*', + ]; + } +} diff --git a/lib/vendor/sabre/vobject/lib/Component/VTodo.php b/lib/vendor/sabre/vobject/lib/Component/VTodo.php new file mode 100644 index 0000000..6f022ba --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Component/VTodo.php @@ -0,0 +1,181 @@ +DTSTART) ? $this->DTSTART->getDateTime() : null; + $duration = isset($this->DURATION) ? VObject\DateTimeParser::parseDuration($this->DURATION) : null; + $due = isset($this->DUE) ? $this->DUE->getDateTime() : null; + $completed = isset($this->COMPLETED) ? $this->COMPLETED->getDateTime() : null; + $created = isset($this->CREATED) ? $this->CREATED->getDateTime() : null; + + if ($dtstart) { + if ($duration) { + $effectiveEnd = $dtstart->add($duration); + + return $start <= $effectiveEnd && $end > $dtstart; + } elseif ($due) { + return + ($start < $due || $start <= $dtstart) && + ($end > $dtstart || $end >= $due); + } else { + return $start <= $dtstart && $end > $dtstart; + } + } + if ($due) { + return $start < $due && $end >= $due; + } + if ($completed && $created) { + return + ($start <= $created || $start <= $completed) && + ($end >= $created || $end >= $completed); + } + if ($completed) { + return $start <= $completed && $end >= $completed; + } + if ($created) { + return $end > $created; + } + + return true; + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * @var array + */ + public function getValidationRules() + { + return [ + 'UID' => 1, + 'DTSTAMP' => 1, + + 'CLASS' => '?', + 'COMPLETED' => '?', + 'CREATED' => '?', + 'DESCRIPTION' => '?', + 'DTSTART' => '?', + 'GEO' => '?', + 'LAST-MODIFIED' => '?', + 'LOCATION' => '?', + 'ORGANIZER' => '?', + 'PERCENT' => '?', + 'PRIORITY' => '?', + 'RECURRENCE-ID' => '?', + 'SEQUENCE' => '?', + 'STATUS' => '?', + 'SUMMARY' => '?', + 'URL' => '?', + + 'RRULE' => '?', + 'DUE' => '?', + 'DURATION' => '?', + + 'ATTACH' => '*', + 'ATTENDEE' => '*', + 'CATEGORIES' => '*', + 'COMMENT' => '*', + 'CONTACT' => '*', + 'EXDATE' => '*', + 'REQUEST-STATUS' => '*', + 'RELATED-TO' => '*', + 'RESOURCES' => '*', + 'RDATE' => '*', + ]; + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on) + * 2 - An inconsequential issue + * 3 - A severe issue. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $result = parent::validate($options); + if (isset($this->DUE) && isset($this->DTSTART)) { + $due = $this->DUE; + $dtStart = $this->DTSTART; + + if ($due->getValueType() !== $dtStart->getValueType()) { + $result[] = [ + 'level' => 3, + 'message' => 'The value type (DATE or DATE-TIME) must be identical for DUE and DTSTART', + 'node' => $due, + ]; + } elseif ($due->getDateTime() < $dtStart->getDateTime()) { + $result[] = [ + 'level' => 3, + 'message' => 'DUE must occur after DTSTART', + 'node' => $due, + ]; + } + } + + return $result; + } + + /** + * This method should return a list of default property values. + * + * @return array + */ + protected function getDefaults() + { + return [ + 'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(), + 'DTSTAMP' => date('Ymd\\THis\\Z'), + ]; + } +} diff --git a/lib/vendor/sabre/vobject/lib/DateTimeParser.php b/lib/vendor/sabre/vobject/lib/DateTimeParser.php new file mode 100644 index 0000000..69072ef --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/DateTimeParser.php @@ -0,0 +1,560 @@ +\+|-)?P((?\d+)W)?((?\d+)D)?(T((?\d+)H)?((?\d+)M)?((?\d+)S)?)?$/', $duration, $matches); + if (!$result) { + throw new InvalidDataException('The supplied iCalendar duration value is incorrect: '.$duration); + } + + if (!$asString) { + $invert = false; + + if (isset($matches['plusminus']) && '-' === $matches['plusminus']) { + $invert = true; + } + + $parts = [ + 'week', + 'day', + 'hour', + 'minute', + 'second', + ]; + + foreach ($parts as $part) { + $matches[$part] = isset($matches[$part]) && $matches[$part] ? (int) $matches[$part] : 0; + } + + // We need to re-construct the $duration string, because weeks and + // days are not supported by DateInterval in the same string. + $duration = 'P'; + $days = $matches['day']; + + if ($matches['week']) { + $days += $matches['week'] * 7; + } + + if ($days) { + $duration .= $days.'D'; + } + + if ($matches['minute'] || $matches['second'] || $matches['hour']) { + $duration .= 'T'; + + if ($matches['hour']) { + $duration .= $matches['hour'].'H'; + } + + if ($matches['minute']) { + $duration .= $matches['minute'].'M'; + } + + if ($matches['second']) { + $duration .= $matches['second'].'S'; + } + } + + if ('P' === $duration) { + $duration = 'PT0S'; + } + + $iv = new DateInterval($duration); + + if ($invert) { + $iv->invert = true; + } + + return $iv; + } + + $parts = [ + 'week', + 'day', + 'hour', + 'minute', + 'second', + ]; + + $newDur = ''; + + foreach ($parts as $part) { + if (isset($matches[$part]) && $matches[$part]) { + $newDur .= ' '.$matches[$part].' '.$part.'s'; + } + } + + $newDur = ('-' === $matches['plusminus'] ? '-' : '+').trim($newDur); + + if ('+' === $newDur) { + $newDur = '+0 seconds'; + } + + return $newDur; + } + + /** + * Parses either a Date or DateTime, or Duration value. + * + * @param string $date + * @param DateTimeZone|string $referenceTz + * + * @return DateTimeImmutable|DateInterval + */ + public static function parse($date, $referenceTz = null) + { + if ('P' === $date[0] || ('-' === $date[0] && 'P' === $date[1])) { + return self::parseDuration($date); + } elseif (8 === strlen($date)) { + return self::parseDate($date, $referenceTz); + } else { + return self::parseDateTime($date, $referenceTz); + } + } + + /** + * This method parses a vCard date and or time value. + * + * This can be used for the DATE, DATE-TIME, TIMESTAMP and + * DATE-AND-OR-TIME value. + * + * This method returns an array, not a DateTime value. + * + * The elements in the array are in the following order: + * year, month, date, hour, minute, second, timezone + * + * Almost any part of the string may be omitted. It's for example legal to + * just specify seconds, leave out the year, etc. + * + * Timezone is either returned as 'Z' or as '+0800' + * + * For any non-specified values null is returned. + * + * List of date formats that are supported: + * YYYY + * YYYY-MM + * YYYYMMDD + * --MMDD + * ---DD + * + * YYYY-MM-DD + * --MM-DD + * ---DD + * + * List of supported time formats: + * + * HH + * HHMM + * HHMMSS + * -MMSS + * --SS + * + * HH + * HH:MM + * HH:MM:SS + * -MM:SS + * --SS + * + * A full basic-format date-time string looks like : + * 20130603T133901 + * + * A full extended-format date-time string looks like : + * 2013-06-03T13:39:01 + * + * Times may be postfixed by a timezone offset. This can be either 'Z' for + * UTC, or a string like -0500 or +1100. + * + * @param string $date + * + * @return array + */ + public static function parseVCardDateTime($date) + { + $regex = '/^ + (?: # date part + (?: + (?: (? [0-9]{4}) (?: -)?| --) + (? [0-9]{2})? + |---) + (? [0-9]{2})? + )? + (?:T # time part + (? [0-9]{2} | -) + (? [0-9]{2} | -)? + (? [0-9]{2})? + + (?: \.[0-9]{3})? # milliseconds + (?P # timezone offset + + Z | (?: \+|-)(?: [0-9]{4}) + + )? + + )? + $/x'; + + if (!preg_match($regex, $date, $matches)) { + // Attempting to parse the extended format. + $regex = '/^ + (?: # date part + (?: (? [0-9]{4}) - | -- ) + (? [0-9]{2}) - + (? [0-9]{2}) + )? + (?:T # time part + + (?: (? [0-9]{2}) : | -) + (?: (? [0-9]{2}) : | -)? + (? [0-9]{2})? + + (?: \.[0-9]{3})? # milliseconds + (?P # timezone offset + + Z | (?: \+|-)(?: [0-9]{2}:[0-9]{2}) + + )? + + )? + $/x'; + + if (!preg_match($regex, $date, $matches)) { + throw new InvalidDataException('Invalid vCard date-time string: '.$date); + } + } + $parts = [ + 'year', + 'month', + 'date', + 'hour', + 'minute', + 'second', + 'timezone', + ]; + + $result = []; + foreach ($parts as $part) { + if (empty($matches[$part])) { + $result[$part] = null; + } elseif ('-' === $matches[$part] || '--' === $matches[$part]) { + $result[$part] = null; + } else { + $result[$part] = $matches[$part]; + } + } + + return $result; + } + + /** + * This method parses a vCard TIME value. + * + * This method returns an array, not a DateTime value. + * + * The elements in the array are in the following order: + * hour, minute, second, timezone + * + * Almost any part of the string may be omitted. It's for example legal to + * just specify seconds, leave out the hour etc. + * + * Timezone is either returned as 'Z' or as '+08:00' + * + * For any non-specified values null is returned. + * + * List of supported time formats: + * + * HH + * HHMM + * HHMMSS + * -MMSS + * --SS + * + * HH + * HH:MM + * HH:MM:SS + * -MM:SS + * --SS + * + * A full basic-format time string looks like : + * 133901 + * + * A full extended-format time string looks like : + * 13:39:01 + * + * Times may be postfixed by a timezone offset. This can be either 'Z' for + * UTC, or a string like -0500 or +11:00. + * + * @param string $date + * + * @return array + */ + public static function parseVCardTime($date) + { + $regex = '/^ + (? [0-9]{2} | -) + (? [0-9]{2} | -)? + (? [0-9]{2})? + + (?: \.[0-9]{3})? # milliseconds + (?P # timezone offset + + Z | (?: \+|-)(?: [0-9]{4}) + + )? + $/x'; + + if (!preg_match($regex, $date, $matches)) { + // Attempting to parse the extended format. + $regex = '/^ + (?: (? [0-9]{2}) : | -) + (?: (? [0-9]{2}) : | -)? + (? [0-9]{2})? + + (?: \.[0-9]{3})? # milliseconds + (?P # timezone offset + + Z | (?: \+|-)(?: [0-9]{2}:[0-9]{2}) + + )? + $/x'; + + if (!preg_match($regex, $date, $matches)) { + throw new InvalidDataException('Invalid vCard time string: '.$date); + } + } + $parts = [ + 'hour', + 'minute', + 'second', + 'timezone', + ]; + + $result = []; + foreach ($parts as $part) { + if (empty($matches[$part])) { + $result[$part] = null; + } elseif ('-' === $matches[$part]) { + $result[$part] = null; + } else { + $result[$part] = $matches[$part]; + } + } + + return $result; + } + + /** + * This method parses a vCard date and or time value. + * + * This can be used for the DATE, DATE-TIME and + * DATE-AND-OR-TIME value. + * + * This method returns an array, not a DateTime value. + * The elements in the array are in the following order: + * year, month, date, hour, minute, second, timezone + * Almost any part of the string may be omitted. It's for example legal to + * just specify seconds, leave out the year, etc. + * + * Timezone is either returned as 'Z' or as '+0800' + * + * For any non-specified values null is returned. + * + * List of date formats that are supported: + * 20150128 + * 2015-01 + * --01 + * --0128 + * ---28 + * + * List of supported time formats: + * 13 + * 1353 + * 135301 + * -53 + * -5301 + * --01 (unreachable, see the tests) + * --01Z + * --01+1234 + * + * List of supported date-time formats: + * 20150128T13 + * --0128T13 + * ---28T13 + * ---28T1353 + * ---28T135301 + * ---28T13Z + * ---28T13+1234 + * + * See the regular expressions for all the possible patterns. + * + * Times may be postfixed by a timezone offset. This can be either 'Z' for + * UTC, or a string like -0500 or +1100. + * + * @param string $date + * + * @return array + */ + public static function parseVCardDateAndOrTime($date) + { + // \d{8}|\d{4}-\d\d|--\d\d(\d\d)?|---\d\d + $valueDate = '/^(?J)(?:'. + '(?\d{4})(?\d\d)(?\d\d)'. + '|(?\d{4})-(?\d\d)'. + '|--(?\d\d)(?\d\d)?'. + '|---(?\d\d)'. + ')$/'; + + // (\d\d(\d\d(\d\d)?)?|-\d\d(\d\d)?|--\d\d)(Z|[+\-]\d\d(\d\d)?)? + $valueTime = '/^(?J)(?:'. + '((?\d\d)((?\d\d)(?\d\d)?)?'. + '|-(?\d\d)(?\d\d)?'. + '|--(?\d\d))'. + '(?(Z|[+\-]\d\d(\d\d)?))?'. + ')$/'; + + // (\d{8}|--\d{4}|---\d\d)T\d\d(\d\d(\d\d)?)?(Z|[+\-]\d\d(\d\d?)? + $valueDateTime = '/^(?:'. + '((?\d{4})(?\d\d)(?\d\d)'. + '|--(?\d\d)(?\d\d)'. + '|---(?\d\d))'. + 'T'. + '(?\d\d)((?\d\d)(?\d\d)?)?'. + '(?(Z|[+\-]\d\d(\d\d?)))?'. + ')$/'; + + // date-and-or-time is date | date-time | time + // in this strict order. + + if (0 === preg_match($valueDate, $date, $matches) + && 0 === preg_match($valueDateTime, $date, $matches) + && 0 === preg_match($valueTime, $date, $matches)) { + throw new InvalidDataException('Invalid vCard date-time string: '.$date); + } + + $parts = [ + 'year' => null, + 'month' => null, + 'date' => null, + 'hour' => null, + 'minute' => null, + 'second' => null, + 'timezone' => null, + ]; + + // The $valueDateTime expression has a bug with (?J) so we simulate it. + $parts['date0'] = &$parts['date']; + $parts['date1'] = &$parts['date']; + $parts['date2'] = &$parts['date']; + $parts['month0'] = &$parts['month']; + $parts['month1'] = &$parts['month']; + $parts['year0'] = &$parts['year']; + + foreach ($parts as $part => &$value) { + if (!empty($matches[$part])) { + $value = $matches[$part]; + } + } + + unset($parts['date0']); + unset($parts['date1']); + unset($parts['date2']); + unset($parts['month0']); + unset($parts['month1']); + unset($parts['year0']); + + return $parts; + } +} diff --git a/lib/vendor/sabre/vobject/lib/Document.php b/lib/vendor/sabre/vobject/lib/Document.php new file mode 100644 index 0000000..36f20dd --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Document.php @@ -0,0 +1,269 @@ +value syntax, in which case + * properties will automatically be created, or you can just pass a list of + * Component and Property object. + * + * By default, a set of sensible values will be added to the component. For + * an iCalendar object, this may be something like CALSCALE:GREGORIAN. To + * ensure that this does not happen, set $defaults to false. + * + * @param string $name + * @param array $children + * @param bool $defaults + * + * @return Component + */ + public function createComponent($name, ?array $children = null, $defaults = true) + { + $name = strtoupper($name); + $class = Component::class; + + if (isset(static::$componentMap[$name])) { + $class = static::$componentMap[$name]; + } + if (is_null($children)) { + $children = []; + } + + return new $class($this, $name, $children, $defaults); + } + + /** + * Factory method for creating new properties. + * + * This method automatically searches for the correct property class, based + * on its name. + * + * You can specify the parameters either in key=>value syntax, in which case + * parameters will automatically be created, or you can just pass a list of + * Parameter objects. + * + * @param string $name + * @param mixed $value + * @param array $parameters + * @param string $valueType Force a specific valuetype, such as URI or TEXT + */ + public function createProperty($name, $value = null, ?array $parameters = null, $valueType = null, ?int $lineIndex = null, ?string $lineString = null): Property + { + // If there's a . in the name, it means it's prefixed by a groupname. + if (false !== ($i = strpos($name, '.'))) { + $group = substr($name, 0, $i); + $name = strtoupper(substr($name, $i + 1)); + } else { + $name = strtoupper($name); + $group = null; + } + + $class = null; + + // If a VALUE parameter is supplied, we have to use that + // According to https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.20 + // If the property's value is the default value type, then this + // parameter need not be specified. However, if the property's + // default value type is overridden by some other allowable value + // type, then this parameter MUST be specified. + if (!$valueType) { + $valueType = $parameters['VALUE'] ?? null; + } + + if ($valueType) { + // The valueType argument comes first to figure out the correct + // class. + $class = $this->getClassNameForPropertyValue($valueType); + } + + // If the value parameter is not set or set to something we do not recognize + // we do not attempt to interpret or parse the datass value as specified in + // https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.20 + // So when we so far did not get a class-name, we use the default for the property + if (is_null($class)) { + $class = $this->getClassNameForPropertyName($name); + } + + if (is_null($parameters)) { + $parameters = []; + } + + return new $class($this, $name, $value, $parameters, $group, $lineIndex, $lineString); + } + + /** + * This method returns a full class-name for a value parameter. + * + * For instance, DTSTART may have VALUE=DATE. In that case we will look in + * our valueMap table and return the appropriate class name. + * + * This method returns null if we don't have a specialized class. + * + * @param string $valueParam + * + * @return string|null + */ + public function getClassNameForPropertyValue($valueParam) + { + $valueParam = strtoupper($valueParam); + if (isset(static::$valueMap[$valueParam])) { + return static::$valueMap[$valueParam]; + } + } + + /** + * Returns the default class for a property name. + * + * @param string $propertyName + * + * @return string + */ + public function getClassNameForPropertyName($propertyName) + { + if (isset(static::$propertyMap[$propertyName])) { + return static::$propertyMap[$propertyName]; + } else { + return Property\Unknown::class; + } + } +} diff --git a/lib/vendor/sabre/vobject/lib/ElementList.php b/lib/vendor/sabre/vobject/lib/ElementList.php new file mode 100644 index 0000000..e419d48 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/ElementList.php @@ -0,0 +1,52 @@ +vevent where there's multiple VEVENT objects. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class ElementList extends ArrayIterator +{ + /* {{{ ArrayAccess Interface */ + + /** + * Sets an item through ArrayAccess. + * + * @param int $offset + * @param mixed $value + * + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + throw new LogicException('You can not add new objects to an ElementList'); + } + + /** + * Sets an item through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + * + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) + { + throw new LogicException('You can not remove objects from an ElementList'); + } + + /* }}} */ +} diff --git a/lib/vendor/sabre/vobject/lib/EofException.php b/lib/vendor/sabre/vobject/lib/EofException.php new file mode 100644 index 0000000..837af7e --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/EofException.php @@ -0,0 +1,15 @@ +start = $start; + $this->end = $end; + $this->data = []; + + $this->data[] = [ + 'start' => $this->start, + 'end' => $this->end, + 'type' => 'FREE', + ]; + } + + /** + * Adds free or busytime to the data. + * + * @param int $start + * @param int $end + * @param string $type FREE, BUSY, BUSY-UNAVAILABLE or BUSY-TENTATIVE + */ + public function add($start, $end, $type) + { + if ($start > $this->end || $end < $this->start) { + // This new data is outside our timerange. + return; + } + + if ($start < $this->start) { + // The item starts before our requested time range + $start = $this->start; + } + if ($end > $this->end) { + // The item ends after our requested time range + $end = $this->end; + } + + // Finding out where we need to insert the new item. + $currentIndex = 0; + while ($start > $this->data[$currentIndex]['end']) { + ++$currentIndex; + } + + // The standard insertion point will be one _after_ the first + // overlapping item. + $insertStartIndex = $currentIndex + 1; + + $newItem = [ + 'start' => $start, + 'end' => $end, + 'type' => $type, + ]; + + $precedingItem = $this->data[$insertStartIndex - 1]; + if ($this->data[$insertStartIndex - 1]['start'] === $start) { + // The old item starts at the exact same point as the new item. + --$insertStartIndex; + } + + // Now we know where to insert the item, we need to know where it + // starts overlapping with items on the tail end. We need to start + // looking one item before the insertStartIndex, because it's possible + // that the new item 'sits inside' the previous old item. + if ($insertStartIndex > 0) { + $currentIndex = $insertStartIndex - 1; + } else { + $currentIndex = 0; + } + + while ($end > $this->data[$currentIndex]['end']) { + ++$currentIndex; + } + + // What we are about to insert into the array + $newItems = [ + $newItem, + ]; + + // This is the amount of items that are completely overwritten by the + // new item. + $itemsToDelete = $currentIndex - $insertStartIndex; + if ($this->data[$currentIndex]['end'] <= $end) { + ++$itemsToDelete; + } + + // If itemsToDelete was -1, it means that the newly inserted item is + // actually sitting inside an existing one. This means we need to split + // the item at the current position in two and insert the new item in + // between. + if (-1 === $itemsToDelete) { + $itemsToDelete = 0; + if ($newItem['end'] < $precedingItem['end']) { + $newItems[] = [ + 'start' => $newItem['end'] + 1, + 'end' => $precedingItem['end'], + 'type' => $precedingItem['type'], + ]; + } + } + + array_splice( + $this->data, + $insertStartIndex, + $itemsToDelete, + $newItems + ); + + $doMerge = false; + $mergeOffset = $insertStartIndex; + $mergeItem = $newItem; + $mergeDelete = 1; + + if (isset($this->data[$insertStartIndex - 1])) { + // Updating the start time of the previous item. + $this->data[$insertStartIndex - 1]['end'] = $start; + + // If the previous and the current are of the same type, we can + // merge them into one item. + if ($this->data[$insertStartIndex - 1]['type'] === $this->data[$insertStartIndex]['type']) { + $doMerge = true; + --$mergeOffset; + ++$mergeDelete; + $mergeItem['start'] = $this->data[$insertStartIndex - 1]['start']; + } + } + if (isset($this->data[$insertStartIndex + 1])) { + // Updating the start time of the next item. + $this->data[$insertStartIndex + 1]['start'] = $end; + + // If the next and the current are of the same type, we can + // merge them into one item. + if ($this->data[$insertStartIndex + 1]['type'] === $this->data[$insertStartIndex]['type']) { + $doMerge = true; + ++$mergeDelete; + $mergeItem['end'] = $this->data[$insertStartIndex + 1]['end']; + } + } + if ($doMerge) { + array_splice( + $this->data, + $mergeOffset, + $mergeDelete, + [$mergeItem] + ); + } + } + + public function getData() + { + return $this->data; + } +} diff --git a/lib/vendor/sabre/vobject/lib/FreeBusyGenerator.php b/lib/vendor/sabre/vobject/lib/FreeBusyGenerator.php new file mode 100644 index 0000000..56ae166 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/FreeBusyGenerator.php @@ -0,0 +1,549 @@ +setTimeRange($start, $end); + + if ($objects) { + $this->setObjects($objects); + } + if (is_null($timeZone)) { + $timeZone = new DateTimeZone('UTC'); + } + $this->setTimeZone($timeZone); + } + + /** + * Sets the VCALENDAR object. + * + * If this is set, it will not be generated for you. You are responsible + * for setting things like the METHOD, CALSCALE, VERSION, etc.. + * + * The VFREEBUSY object will be automatically added though. + */ + public function setBaseObject(Document $vcalendar) + { + $this->baseObject = $vcalendar; + } + + /** + * Sets a VAVAILABILITY document. + */ + public function setVAvailability(Document $vcalendar) + { + $this->vavailability = $vcalendar; + } + + /** + * Sets the input objects. + * + * You must either specify a vcalendar object as a string, or as the parse + * Component. + * It's also possible to specify multiple objects as an array. + * + * @param mixed $objects + */ + public function setObjects($objects) + { + if (!is_array($objects)) { + $objects = [$objects]; + } + + $this->objects = []; + foreach ($objects as $object) { + if (is_string($object) || is_resource($object)) { + $this->objects[] = Reader::read($object); + } elseif ($object instanceof Component) { + $this->objects[] = $object; + } else { + throw new \InvalidArgumentException('You can only pass strings or \\Sabre\\VObject\\Component arguments to setObjects'); + } + } + } + + /** + * Sets the time range. + * + * Any freebusy object falling outside of this time range will be ignored. + * + * @param DateTimeInterface $start + * @param DateTimeInterface $end + */ + public function setTimeRange(?DateTimeInterface $start = null, ?DateTimeInterface $end = null) + { + if (!$start) { + $start = new DateTimeImmutable(Settings::$minDate); + } + if (!$end) { + $end = new DateTimeImmutable(Settings::$maxDate); + } + $this->start = $start; + $this->end = $end; + } + + /** + * Sets the reference timezone for floating times. + */ + public function setTimeZone(DateTimeZone $timeZone) + { + $this->timeZone = $timeZone; + } + + /** + * Parses the input data and returns a correct VFREEBUSY object, wrapped in + * a VCALENDAR. + * + * @return Component + */ + public function getResult() + { + $fbData = new FreeBusyData( + $this->start->getTimeStamp(), + $this->end->getTimeStamp() + ); + if ($this->vavailability) { + $this->calculateAvailability($fbData, $this->vavailability); + } + + $this->calculateBusy($fbData, $this->objects); + + return $this->generateFreeBusyCalendar($fbData); + } + + /** + * This method takes a VAVAILABILITY component and figures out all the + * available times. + */ + protected function calculateAvailability(FreeBusyData $fbData, VCalendar $vavailability) + { + $vavailComps = iterator_to_array($vavailability->VAVAILABILITY); + usort( + $vavailComps, + function ($a, $b) { + // We need to order the components by priority. Priority 1 + // comes first, up until priority 9. Priority 0 comes after + // priority 9. No priority implies priority 0. + // + // Yes, I'm serious. + $priorityA = isset($a->PRIORITY) ? (int) $a->PRIORITY->getValue() : 0; + $priorityB = isset($b->PRIORITY) ? (int) $b->PRIORITY->getValue() : 0; + + if (0 === $priorityA) { + $priorityA = 10; + } + if (0 === $priorityB) { + $priorityB = 10; + } + + return $priorityA - $priorityB; + } + ); + + // Now we go over all the VAVAILABILITY components and figure if + // there's any we don't need to consider. + // + // This is can be because of one of two reasons: either the + // VAVAILABILITY component falls outside the time we are interested in, + // or a different VAVAILABILITY component with a higher priority has + // already completely covered the time-range. + $old = $vavailComps; + $new = []; + + foreach ($old as $vavail) { + list($compStart, $compEnd) = $vavail->getEffectiveStartEnd(); + + // We don't care about datetimes that are earlier or later than the + // start and end of the freebusy report, so this gets normalized + // first. + if (is_null($compStart) || $compStart < $this->start) { + $compStart = $this->start; + } + if (is_null($compEnd) || $compEnd > $this->end) { + $compEnd = $this->end; + } + + // If the item fell out of the timerange, we can just skip it. + if ($compStart > $this->end || $compEnd < $this->start) { + continue; + } + + // Going through our existing list of components to see if there's + // a higher priority component that already fully covers this one. + foreach ($new as $higherVavail) { + list($higherStart, $higherEnd) = $higherVavail->getEffectiveStartEnd(); + if ( + (is_null($higherStart) || $higherStart < $compStart) && + (is_null($higherEnd) || $higherEnd > $compEnd) + ) { + // Component is fully covered by a higher priority + // component. We can skip this component. + continue 2; + } + } + + // We're keeping it! + $new[] = $vavail; + } + + // Lastly, we need to traverse the remaining components and fill in the + // freebusydata slots. + // + // We traverse the components in reverse, because we want the higher + // priority components to override the lower ones. + foreach (array_reverse($new) as $vavail) { + $busyType = isset($vavail->BUSYTYPE) ? strtoupper($vavail->BUSYTYPE) : 'BUSY-UNAVAILABLE'; + list($vavailStart, $vavailEnd) = $vavail->getEffectiveStartEnd(); + + // Making the component size no larger than the requested free-busy + // report range. + if (!$vavailStart || $vavailStart < $this->start) { + $vavailStart = $this->start; + } + if (!$vavailEnd || $vavailEnd > $this->end) { + $vavailEnd = $this->end; + } + + // Marking the entire time range of the VAVAILABILITY component as + // busy. + $fbData->add( + $vavailStart->getTimeStamp(), + $vavailEnd->getTimeStamp(), + $busyType + ); + + // Looping over the AVAILABLE components. + if (isset($vavail->AVAILABLE)) { + foreach ($vavail->AVAILABLE as $available) { + list($availStart, $availEnd) = $available->getEffectiveStartEnd(); + $fbData->add( + $availStart->getTimeStamp(), + $availEnd->getTimeStamp(), + 'FREE' + ); + + if ($available->RRULE) { + // Our favourite thing: recurrence!! + + $rruleIterator = new Recur\RRuleIterator( + $available->RRULE->getValue(), + $availStart + ); + $rruleIterator->fastForward($vavailStart); + + $startEndDiff = $availStart->diff($availEnd); + + while ($rruleIterator->valid()) { + $recurStart = $rruleIterator->current(); + $recurEnd = $recurStart->add($startEndDiff); + + if ($recurStart > $vavailEnd) { + // We're beyond the legal timerange. + break; + } + + if ($recurEnd > $vavailEnd) { + // Truncating the end if it exceeds the + // VAVAILABILITY end. + $recurEnd = $vavailEnd; + } + + $fbData->add( + $recurStart->getTimeStamp(), + $recurEnd->getTimeStamp(), + 'FREE' + ); + + $rruleIterator->next(); + } + } + } + } + } + } + + /** + * This method takes an array of iCalendar objects and applies its busy + * times on fbData. + * + * @param VCalendar[] $objects + */ + protected function calculateBusy(FreeBusyData $fbData, array $objects) + { + foreach ($objects as $key => $object) { + foreach ($object->getBaseComponents() as $component) { + switch ($component->name) { + case 'VEVENT': + $FBTYPE = 'BUSY'; + if (isset($component->TRANSP) && ('TRANSPARENT' === strtoupper($component->TRANSP))) { + break; + } + if (isset($component->STATUS)) { + $status = strtoupper($component->STATUS); + if ('CANCELLED' === $status) { + break; + } + if ('TENTATIVE' === $status) { + $FBTYPE = 'BUSY-TENTATIVE'; + } + } + + $times = []; + + if ($component->RRULE) { + try { + $iterator = new EventIterator($object, (string) $component->UID, $this->timeZone); + } catch (NoInstancesException $e) { + // This event is recurring, but it doesn't have a single + // instance. We are skipping this event from the output + // entirely. + unset($this->objects[$key]); + break; + } + + if ($this->start) { + $iterator->fastForward($this->start); + } + + $maxRecurrences = Settings::$maxRecurrences; + + while ($iterator->valid() && --$maxRecurrences) { + $startTime = $iterator->getDTStart(); + if ($this->end && $startTime > $this->end) { + break; + } + $times[] = [ + $iterator->getDTStart(), + $iterator->getDTEnd(), + ]; + + $iterator->next(); + } + } else { + $startTime = $component->DTSTART->getDateTime($this->timeZone); + if ($this->end && $startTime > $this->end) { + break; + } + $endTime = null; + if (isset($component->DTEND)) { + $endTime = $component->DTEND->getDateTime($this->timeZone); + } elseif (isset($component->DURATION)) { + $duration = DateTimeParser::parseDuration((string) $component->DURATION); + $endTime = clone $startTime; + $endTime = $endTime->add($duration); + } elseif (!$component->DTSTART->hasTime()) { + $endTime = clone $startTime; + $endTime = $endTime->modify('+1 day'); + } else { + // The event had no duration (0 seconds) + break; + } + + $times[] = [$startTime, $endTime]; + } + + foreach ($times as $time) { + if ($this->end && $time[0] > $this->end) { + break; + } + if ($this->start && $time[1] < $this->start) { + break; + } + + $fbData->add( + $time[0]->getTimeStamp(), + $time[1]->getTimeStamp(), + $FBTYPE + ); + } + break; + + case 'VFREEBUSY': + foreach ($component->FREEBUSY as $freebusy) { + $fbType = isset($freebusy['FBTYPE']) ? strtoupper($freebusy['FBTYPE']) : 'BUSY'; + + // Skipping intervals marked as 'free' + if ('FREE' === $fbType) { + continue; + } + + $values = explode(',', $freebusy); + foreach ($values as $value) { + list($startTime, $endTime) = explode('/', $value); + $startTime = DateTimeParser::parseDateTime($startTime); + + if ('P' === substr($endTime, 0, 1) || '-P' === substr($endTime, 0, 2)) { + $duration = DateTimeParser::parseDuration($endTime); + $endTime = clone $startTime; + $endTime = $endTime->add($duration); + } else { + $endTime = DateTimeParser::parseDateTime($endTime); + } + + if ($this->start && $this->start > $endTime) { + continue; + } + if ($this->end && $this->end < $startTime) { + continue; + } + $fbData->add( + $startTime->getTimeStamp(), + $endTime->getTimeStamp(), + $fbType + ); + } + } + break; + } + } + } + } + + /** + * This method takes a FreeBusyData object and generates the VCALENDAR + * object associated with it. + * + * @return VCalendar + */ + protected function generateFreeBusyCalendar(FreeBusyData $fbData) + { + if ($this->baseObject) { + $calendar = $this->baseObject; + } else { + $calendar = new VCalendar(); + } + + $vfreebusy = $calendar->createComponent('VFREEBUSY'); + $calendar->add($vfreebusy); + + if ($this->start) { + $dtstart = $calendar->createProperty('DTSTART'); + $dtstart->setDateTime($this->start); + $vfreebusy->add($dtstart); + } + if ($this->end) { + $dtend = $calendar->createProperty('DTEND'); + $dtend->setDateTime($this->end); + $vfreebusy->add($dtend); + } + + $tz = new \DateTimeZone('UTC'); + $dtstamp = $calendar->createProperty('DTSTAMP'); + $dtstamp->setDateTime(new DateTimeImmutable('now', $tz)); + $vfreebusy->add($dtstamp); + + foreach ($fbData->getData() as $busyTime) { + $busyType = strtoupper($busyTime['type']); + + // Ignoring all the FREE parts, because those are already assumed. + if ('FREE' === $busyType) { + continue; + } + + $busyTime[0] = new \DateTimeImmutable('@'.$busyTime['start'], $tz); + $busyTime[1] = new \DateTimeImmutable('@'.$busyTime['end'], $tz); + + $prop = $calendar->createProperty( + 'FREEBUSY', + $busyTime[0]->format('Ymd\\THis\\Z').'/'.$busyTime[1]->format('Ymd\\THis\\Z') + ); + + // Only setting FBTYPE if it's not BUSY, because BUSY is the + // default anyway. + if ('BUSY' !== $busyType) { + $prop['FBTYPE'] = $busyType; + } + $vfreebusy->add($prop); + } + + return $calendar; + } +} diff --git a/lib/vendor/sabre/vobject/lib/ITip/Broker.php b/lib/vendor/sabre/vobject/lib/ITip/Broker.php new file mode 100644 index 0000000..e100c14 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/ITip/Broker.php @@ -0,0 +1,1003 @@ +component) { + return false; + } + + switch ($itipMessage->method) { + case 'REQUEST': + return $this->processMessageRequest($itipMessage, $existingObject); + + case 'CANCEL': + return $this->processMessageCancel($itipMessage, $existingObject); + + case 'REPLY': + return $this->processMessageReply($itipMessage, $existingObject); + + default: + // Unsupported iTip message + return; + } + + return $existingObject; + } + + /** + * This function parses a VCALENDAR object and figure out if any messages + * need to be sent. + * + * A VCALENDAR object will be created from the perspective of either an + * attendee, or an organizer. You must pass a string identifying the + * current user, so we can figure out who in the list of attendees or the + * organizer we are sending this message on behalf of. + * + * It's possible to specify the current user as an array, in case the user + * has more than one identifying href (such as multiple emails). + * + * It $oldCalendar is specified, it is assumed that the operation is + * updating an existing event, which means that we need to look at the + * differences between events, and potentially send old attendees + * cancellations, and current attendees updates. + * + * If $calendar is null, but $oldCalendar is specified, we treat the + * operation as if the user has deleted an event. If the user was an + * organizer, this means that we need to send cancellation notices to + * people. If the user was an attendee, we need to make sure that the + * organizer gets the 'declined' message. + * + * @param VCalendar|string $calendar + * @param string|array $userHref + * @param VCalendar|string|null $oldCalendar + * + * @return array + */ + public function parseEvent($calendar, $userHref, $oldCalendar = null) + { + if ($oldCalendar) { + if (is_string($oldCalendar)) { + $oldCalendar = Reader::read($oldCalendar); + } + if (!isset($oldCalendar->VEVENT)) { + // We only support events at the moment + return []; + } + + $oldEventInfo = $this->parseEventInfo($oldCalendar); + } else { + $oldEventInfo = [ + 'organizer' => null, + 'significantChangeHash' => '', + 'attendees' => [], + ]; + } + + $userHref = (array) $userHref; + + if (!is_null($calendar)) { + if (is_string($calendar)) { + $calendar = Reader::read($calendar); + } + if (!isset($calendar->VEVENT)) { + // We only support events at the moment + return []; + } + $eventInfo = $this->parseEventInfo($calendar); + if (!$eventInfo['attendees'] && !$oldEventInfo['attendees']) { + // If there were no attendees on either side of the equation, + // we don't need to do anything. + return []; + } + if (!$eventInfo['organizer'] && !$oldEventInfo['organizer']) { + // There was no organizer before or after the change. + return []; + } + + $baseCalendar = $calendar; + + // If the new object didn't have an organizer, the organizer + // changed the object from a scheduling object to a non-scheduling + // object. We just copy the info from the old object. + if (!$eventInfo['organizer'] && $oldEventInfo['organizer']) { + $eventInfo['organizer'] = $oldEventInfo['organizer']; + $eventInfo['organizerName'] = $oldEventInfo['organizerName']; + } + } else { + // The calendar object got deleted, we need to process this as a + // cancellation / decline. + if (!$oldCalendar) { + // No old and no new calendar, there's no thing to do. + return []; + } + + $eventInfo = $oldEventInfo; + + if (in_array($eventInfo['organizer'], $userHref)) { + // This is an organizer deleting the event. + $eventInfo['attendees'] = []; + // Increasing the sequence, but only if the organizer deleted + // the event. + ++$eventInfo['sequence']; + } else { + // This is an attendee deleting the event. + foreach ($eventInfo['attendees'] as $key => $attendee) { + if (in_array($attendee['href'], $userHref)) { + $eventInfo['attendees'][$key]['instances'] = ['master' => ['id' => 'master', 'partstat' => 'DECLINED'], + ]; + } + } + } + $baseCalendar = $oldCalendar; + } + + // Check if the user is the organizer + if (in_array($eventInfo['organizer'], $userHref)) { + return $this->parseEventForOrganizer($baseCalendar, $eventInfo, $oldEventInfo); + } + + // Check if the user is an attendee + foreach ($eventInfo['attendees'] as $attendee) { + if (in_array($attendee['href'], $userHref)) { + // If this is a event update, we always generate a reply + if ($oldCalendar) { + return $this->parseEventForAttendee($baseCalendar, $eventInfo, $oldEventInfo, $attendee['href']); + } + + // If this is a new event, we only generate a reply if the participation status is set + foreach ($attendee['instances'] as $instance) { + if (isset($instance['partstat']) && 'NEEDS-ACTION' !== $instance['partstat']) { + // Attendee has responded (ACCEPTED/DECLINED/TENTATIVE) - generate REPLY + return $this->parseEventForAttendee($baseCalendar, $eventInfo, $oldEventInfo, $attendee['href']); + } + } + + // User is attendee but no response to process + break; + } + } + + return []; + } + + /** + * Processes incoming REQUEST messages. + * + * This is message from an organizer, and is either a new event + * invite, or an update to an existing one. + * + * @param VCalendar $existingObject + * + * @return VCalendar|null + */ + protected function processMessageRequest(Message $itipMessage, ?VCalendar $existingObject = null) + { + if (!$existingObject) { + // This is a new invite, and we're just going to copy over + // all the components from the invite. + $existingObject = new VCalendar(); + foreach ($itipMessage->message->getComponents() as $component) { + $existingObject->add(clone $component); + } + } else { + // We need to update an existing object with all the new + // information. We can just remove all existing components + // and create new ones. + foreach ($existingObject->getComponents() as $component) { + $existingObject->remove($component); + } + foreach ($itipMessage->message->getComponents() as $component) { + $existingObject->add(clone $component); + } + } + + return $existingObject; + } + + /** + * Processes incoming CANCEL messages. + * + * This is a message from an organizer, and means that either an + * attendee got removed from an event, or an event got cancelled + * altogether. + * + * @param VCalendar $existingObject + * + * @return VCalendar|null + */ + protected function processMessageCancel(Message $itipMessage, ?VCalendar $existingObject = null) + { + if (!$existingObject) { + // The event didn't exist in the first place, so we're just + // ignoring this message. + } else { + foreach ($existingObject->VEVENT as $vevent) { + $vevent->STATUS = 'CANCELLED'; + $vevent->SEQUENCE = $itipMessage->sequence; + } + } + + return $existingObject; + } + + /** + * Processes incoming REPLY messages. + * + * The message is a reply. This is for example an attendee telling + * an organizer he accepted the invite, or declined it. + * + * @param VCalendar $existingObject + * + * @return VCalendar|null + */ + protected function processMessageReply(Message $itipMessage, ?VCalendar $existingObject = null) + { + // A reply can only be processed based on an existing object. + // If the object is not available, the reply is ignored. + if (!$existingObject) { + return; + } + $instances = []; + $requestStatus = '2.0'; + + // Finding all the instances the attendee replied to. + foreach ($itipMessage->message->VEVENT as $vevent) { + // Use the Unix timestamp returned by getTimestamp as a unique identifier for the recurrence. + // The Unix timestamp will be the same for an event, even if the reply from the attendee + // used a different format/timezone to express the event date-time. + $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimestamp() : 'master'; + $attendee = $vevent->ATTENDEE; + $instances[$recurId] = $attendee['PARTSTAT']->getValue(); + if (isset($vevent->{'REQUEST-STATUS'})) { + $requestStatus = $vevent->{'REQUEST-STATUS'}->getValue(); + list($requestStatus) = explode(';', $requestStatus); + } + } + + // Now we need to loop through the original organizer event, to find + // all the instances where we have a reply for. + $masterObject = null; + foreach ($existingObject->VEVENT as $vevent) { + // Use the Unix timestamp returned by getTimestamp as a unique identifier for the recurrence. + $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimestamp() : 'master'; + if ('master' === $recurId) { + $masterObject = $vevent; + } + if (isset($instances[$recurId])) { + $attendeeFound = false; + if (isset($vevent->ATTENDEE)) { + foreach ($vevent->ATTENDEE as $attendee) { + if ($attendee->getValue() === $itipMessage->sender) { + $attendeeFound = true; + $attendee['PARTSTAT'] = $instances[$recurId]; + $attendee['SCHEDULE-STATUS'] = $requestStatus; + // Un-setting the RSVP status, because we now know + // that the attendee already replied. + unset($attendee['RSVP']); + break; + } + } + } + if (!$attendeeFound) { + // Adding a new attendee. The iTip documentation calls this + // a party crasher. + $attendee = $vevent->add('ATTENDEE', $itipMessage->sender, [ + 'PARTSTAT' => $instances[$recurId], + ]); + if ($itipMessage->senderName) { + $attendee['CN'] = $itipMessage->senderName; + } + } + unset($instances[$recurId]); + } + } + + if (!$masterObject) { + // No master object, we can't add new instances. + return; + } + // If we got replies to instances that did not exist in the + // original list, it means that new exceptions must be created. + foreach ($instances as $recurId => $partstat) { + $recurrenceIterator = new EventIterator($existingObject, $itipMessage->uid); + $found = false; + $iterations = 1000; + do { + $newObject = $recurrenceIterator->getEventObject(); + $recurrenceIterator->next(); + + // Compare the Unix timestamp returned by getTimestamp with the previously calculated timestamp. + // If they are the same, then this is a matching recurrence, even though its date-time may have + // been expressed in a different format/timezone. + if (isset($newObject->{'RECURRENCE-ID'}) && $newObject->{'RECURRENCE-ID'}->getDateTime()->getTimestamp() === $recurId) { + $found = true; + } + --$iterations; + } while ($recurrenceIterator->valid() && !$found && $iterations); + + // Invalid recurrence id. Skipping this object. + if (!$found) { + continue; + } + + unset( + $newObject->RRULE, + $newObject->EXDATE, + $newObject->RDATE + ); + $attendeeFound = false; + if (isset($newObject->ATTENDEE)) { + foreach ($newObject->ATTENDEE as $attendee) { + if ($attendee->getValue() === $itipMessage->sender) { + $attendeeFound = true; + $attendee['PARTSTAT'] = $partstat; + break; + } + } + } + if (!$attendeeFound) { + // Adding a new attendee + $attendee = $newObject->add('ATTENDEE', $itipMessage->sender, [ + 'PARTSTAT' => $partstat, + ]); + if ($itipMessage->senderName) { + $attendee['CN'] = $itipMessage->senderName; + } + } + $existingObject->add($newObject); + } + + return $existingObject; + } + + /** + * This method is used in cases where an event got updated, and we + * potentially need to send emails to attendees to let them know of updates + * in the events. + * + * We will detect which attendees got added, which got removed and create + * specific messages for these situations. + * + * @return array + */ + protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo, array $oldEventInfo) + { + // Merging attendee lists. + $attendees = []; + foreach ($oldEventInfo['attendees'] as $attendee) { + $attendees[$attendee['href']] = [ + 'href' => $attendee['href'], + 'oldInstances' => $attendee['instances'], + 'newInstances' => [], + 'name' => $attendee['name'], + 'forceSend' => null, + ]; + } + foreach ($eventInfo['attendees'] as $attendee) { + if (isset($attendees[$attendee['href']])) { + $attendees[$attendee['href']]['name'] = $attendee['name']; + $attendees[$attendee['href']]['newInstances'] = $attendee['instances']; + $attendees[$attendee['href']]['forceSend'] = $attendee['forceSend']; + } else { + $attendees[$attendee['href']] = [ + 'href' => $attendee['href'], + 'oldInstances' => [], + 'newInstances' => $attendee['instances'], + 'name' => $attendee['name'], + 'forceSend' => $attendee['forceSend'], + ]; + } + } + + $messages = []; + foreach ($attendees as $attendee) { + // An organizer can also be an attendee. We should not generate any + // messages for those. + if ($attendee['href'] === $eventInfo['organizer']) { + continue; + } + + $message = new Message(); + $message->uid = $eventInfo['uid']; + $message->component = 'VEVENT'; + $message->sequence = $eventInfo['sequence']; + $message->sender = $eventInfo['organizer']; + $message->senderName = $eventInfo['organizerName']; + $message->recipient = $attendee['href']; + $message->recipientName = $attendee['name']; + + // Creating the new iCalendar body. + $icalMsg = new VCalendar(); + + foreach ($calendar->select('VTIMEZONE') as $timezone) { + $icalMsg->add(clone $timezone); + } + + if (!$attendee['newInstances'] || 'CANCELLED' === $eventInfo['status']) { + // If there are no instances the attendee is a part of, it means + // the attendee was removed and we need to send them a CANCEL message. + // Also If the meeting STATUS property was changed to CANCELLED + // we need to send the attendee a CANCEL message. + $message->method = 'CANCEL'; + + $icalMsg->METHOD = $message->method; + + $event = $icalMsg->add('VEVENT', [ + 'UID' => $message->uid, + 'SEQUENCE' => $message->sequence, + 'DTSTAMP' => gmdate('Ymd\\THis\\Z'), + ]); + if (isset($calendar->VEVENT->SUMMARY)) { + $event->add('SUMMARY', $calendar->VEVENT->SUMMARY->getValue()); + } + $event->add(clone $calendar->VEVENT->DTSTART); + if (isset($calendar->VEVENT->DTEND)) { + $event->add(clone $calendar->VEVENT->DTEND); + } elseif (isset($calendar->VEVENT->DURATION)) { + $event->add(clone $calendar->VEVENT->DURATION); + } + $org = $event->add('ORGANIZER', $eventInfo['organizer']); + if ($eventInfo['organizerName']) { + $org['CN'] = $eventInfo['organizerName']; + } + $event->add('ATTENDEE', $attendee['href'], [ + 'CN' => $attendee['name'], + ]); + $message->significantChange = true; + } else { + // The attendee gets the updated event body + $message->method = 'REQUEST'; + + $icalMsg->METHOD = $message->method; + + // We need to find out that this change is significant. If it's + // not, systems may opt to not send messages. + // + // We do this based on the 'significantChangeHash' which is + // some value that changes if there's a certain set of + // properties changed in the event, or simply if there's a + // difference in instances that the attendee is invited to. + + $oldAttendeeInstances = array_keys($attendee['oldInstances']); + $newAttendeeInstances = array_keys($attendee['newInstances']); + + $message->significantChange = + 'REQUEST' === $attendee['forceSend'] || + count($oldAttendeeInstances) != count($newAttendeeInstances) || + count(array_diff($oldAttendeeInstances, $newAttendeeInstances)) > 0 || + $oldEventInfo['significantChangeHash'] !== $eventInfo['significantChangeHash']; + + foreach ($attendee['newInstances'] as $instanceId => $instanceInfo) { + $currentEvent = clone $eventInfo['instances'][$instanceId]; + if ('master' === $instanceId) { + // We need to find a list of events that the attendee + // is not a part of to add to the list of exceptions. + $exceptions = []; + foreach ($eventInfo['instances'] as $instanceId => $vevent) { + if (!isset($attendee['newInstances'][$instanceId])) { + $exceptions[] = $instanceId; + } + } + + // If there were exceptions, we need to add it to an + // existing EXDATE property, if it exists. + if ($exceptions) { + if (isset($currentEvent->EXDATE)) { + $currentEvent->EXDATE->setParts(array_merge( + $currentEvent->EXDATE->getParts(), + $exceptions + )); + } else { + $currentEvent->EXDATE = $exceptions; + if ($currentEvent->DTSTART['TZID']) { + $currentEvent->EXDATE['TZID'] = clone $currentEvent->DTSTART['TZID']; + } + } + } + + // Cleaning up any scheduling information that + // shouldn't be sent along. + unset($currentEvent->ORGANIZER['SCHEDULE-FORCE-SEND']); + unset($currentEvent->ORGANIZER['SCHEDULE-STATUS']); + + foreach ($currentEvent->ATTENDEE as $currentEventAttendee) { + unset($currentEventAttendee['SCHEDULE-FORCE-SEND']); + unset($currentEventAttendee['SCHEDULE-STATUS']); + + // We're adding PARTSTAT=NEEDS-ACTION to ensure that + // iOS shows an "Inbox Item" + if (!isset($currentEventAttendee['PARTSTAT'])) { + $currentEventAttendee['PARTSTAT'] = 'NEEDS-ACTION'; + } + } + } + + $currentEvent->DTSTAMP = gmdate('Ymd\\THis\\Z'); + $icalMsg->add($currentEvent); + } + } + + $message->message = $icalMsg; + $messages[] = $message; + } + + return $messages; + } + + /** + * Parse an event update for an attendee. + * + * This function figures out if we need to send a reply to an organizer. + * + * @param string $attendee + * + * @return Message[] + */ + protected function parseEventForAttendee(VCalendar $calendar, array $eventInfo, array $oldEventInfo, $attendee) + { + if ($this->scheduleAgentServerRules && 'CLIENT' === $eventInfo['organizerScheduleAgent']) { + return []; + } + + // Don't bother generating messages for events that have already been + // cancelled. + if ('CANCELLED' === $eventInfo['status']) { + return []; + } + + $oldInstances = !empty($oldEventInfo['attendees'][$attendee]['instances']) ? + $oldEventInfo['attendees'][$attendee]['instances'] : + []; + + $instances = []; + foreach ($oldInstances as $instance) { + $instances[$instance['id']] = [ + 'id' => $instance['id'], + 'oldstatus' => $instance['partstat'], + 'newstatus' => null, + ]; + } + foreach ($eventInfo['attendees'][$attendee]['instances'] as $instance) { + if (isset($instances[$instance['id']])) { + $instances[$instance['id']]['newstatus'] = $instance['partstat']; + } else { + $instances[$instance['id']] = [ + 'id' => $instance['id'], + 'oldstatus' => null, + 'newstatus' => $instance['partstat'], + ]; + } + } + + // We need to also look for differences in EXDATE. If there are new + // items in EXDATE, it means that an attendee deleted instances of an + // event, which means we need to send DECLINED specifically for those + // instances. + // We only need to do that though, if the master event is not declined. + if (isset($instances['master']) && 'DECLINED' !== $instances['master']['newstatus']) { + foreach ($eventInfo['exdate'] as $exDate) { + if (!in_array($exDate, $oldEventInfo['exdate'])) { + if (isset($instances[$exDate])) { + $instances[$exDate]['newstatus'] = 'DECLINED'; + } else { + $instances[$exDate] = [ + 'id' => $exDate, + 'oldstatus' => null, + 'newstatus' => 'DECLINED', + ]; + } + } + } + } + + // Gathering a few extra properties for each instance. + foreach ($instances as $recurId => $instanceInfo) { + if (isset($eventInfo['instances'][$recurId])) { + $instances[$recurId]['dtstart'] = clone $eventInfo['instances'][$recurId]->DTSTART; + } else { + $instances[$recurId]['dtstart'] = $recurId; + } + } + + $message = new Message(); + $message->uid = $eventInfo['uid']; + $message->method = 'REPLY'; + $message->component = 'VEVENT'; + $message->sequence = $eventInfo['sequence']; + $message->sender = $attendee; + $message->senderName = $eventInfo['attendees'][$attendee]['name']; + $message->recipient = $eventInfo['organizer']; + $message->recipientName = $eventInfo['organizerName']; + + $icalMsg = new VCalendar(); + $icalMsg->METHOD = 'REPLY'; + + foreach ($calendar->select('VTIMEZONE') as $timezone) { + $icalMsg->add(clone $timezone); + } + + $hasReply = false; + + foreach ($instances as $instance) { + if ($instance['oldstatus'] == $instance['newstatus'] && 'REPLY' !== $eventInfo['organizerForceSend']) { + // Skip + continue; + } + + $event = $icalMsg->add('VEVENT', [ + 'UID' => $message->uid, + 'SEQUENCE' => $message->sequence, + ]); + $summary = isset($calendar->VEVENT->SUMMARY) ? $calendar->VEVENT->SUMMARY->getValue() : ''; + // Adding properties from the correct source instance + if (isset($eventInfo['instances'][$instance['id']])) { + $instanceObj = $eventInfo['instances'][$instance['id']]; + $event->add(clone $instanceObj->DTSTART); + if (isset($instanceObj->DTEND)) { + $event->add(clone $instanceObj->DTEND); + } elseif (isset($instanceObj->DURATION)) { + $event->add(clone $instanceObj->DURATION); + } + if (isset($instanceObj->SUMMARY)) { + $event->add('SUMMARY', $instanceObj->SUMMARY->getValue()); + } elseif ($summary) { + $event->add('SUMMARY', $summary); + } + } else { + // This branch of the code is reached, when a reply is + // generated for an instance of a recurring event, through the + // fact that the instance has disappeared by showing up in + // EXDATE + $dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']); + // Treat is as a DATE field + if (strlen($instance['id']) <= 8) { + $event->add('DTSTART', $dt, ['VALUE' => 'DATE']); + } else { + $event->add('DTSTART', $dt); + } + if ($summary) { + $event->add('SUMMARY', $summary); + } + } + if ('master' !== $instance['id']) { + $dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']); + // Treat is as a DATE field + if (strlen($instance['id']) <= 8) { + $event->add('RECURRENCE-ID', $dt, ['VALUE' => 'DATE']); + } else { + $event->add('RECURRENCE-ID', $dt); + } + } + $organizer = $event->add('ORGANIZER', $message->recipient); + if ($message->recipientName) { + $organizer['CN'] = $message->recipientName; + } + $attendee = $event->add('ATTENDEE', $message->sender, [ + 'PARTSTAT' => $instance['newstatus'], + ]); + if ($message->senderName) { + $attendee['CN'] = $message->senderName; + } + $hasReply = true; + } + + if ($hasReply) { + $message->message = $icalMsg; + + return [$message]; + } else { + return []; + } + } + + /** + * Returns attendee information and information about instances of an + * event. + * + * Returns an array with the following keys: + * + * 1. uid + * 2. organizer + * 3. organizerName + * 4. organizerScheduleAgent + * 5. organizerForceSend + * 6. instances + * 7. attendees + * 8. sequence + * 9. exdate + * 10. timezone - strictly the timezone on which the recurrence rule is + * based on. + * 11. significantChangeHash + * 12. status + * + * @param VCalendar $calendar + * + * @return array + */ + protected function parseEventInfo(?VCalendar $calendar = null) + { + $uid = null; + $organizer = null; + $organizerName = null; + $organizerForceSend = null; + $sequence = null; + $timezone = null; + $status = null; + $organizerScheduleAgent = 'SERVER'; + + $significantChangeHash = ''; + + // Now we need to collect a list of attendees, and which instances they + // are a part of. + $attendees = []; + + $instances = []; + $exdate = []; + + $significantChangeEventProperties = []; + + foreach ($calendar->VEVENT as $vevent) { + $eventSignificantChangeHash = ''; + $rrule = []; + + if (is_null($uid)) { + $uid = $vevent->UID->getValue(); + } else { + if ($uid !== $vevent->UID->getValue()) { + throw new ITipException('If a calendar contained more than one event, they must have the same UID.'); + } + } + + if (!isset($vevent->DTSTART)) { + throw new ITipException('An event MUST have a DTSTART property.'); + } + + if (isset($vevent->ORGANIZER)) { + if (is_null($organizer)) { + $organizer = $vevent->ORGANIZER->getNormalizedValue(); + $organizerName = isset($vevent->ORGANIZER['CN']) ? $vevent->ORGANIZER['CN'] : null; + } else { + if (strtoupper($organizer) !== strtoupper($vevent->ORGANIZER->getNormalizedValue())) { + throw new SameOrganizerForAllComponentsException('Every instance of the event must have the same organizer.'); + } + } + $organizerForceSend = + isset($vevent->ORGANIZER['SCHEDULE-FORCE-SEND']) ? + strtoupper($vevent->ORGANIZER['SCHEDULE-FORCE-SEND']) : + null; + $organizerScheduleAgent = + isset($vevent->ORGANIZER['SCHEDULE-AGENT']) ? + strtoupper((string) $vevent->ORGANIZER['SCHEDULE-AGENT']) : + 'SERVER'; + } + if (is_null($sequence) && isset($vevent->SEQUENCE)) { + $sequence = $vevent->SEQUENCE->getValue(); + } + if (isset($vevent->EXDATE)) { + foreach ($vevent->select('EXDATE') as $val) { + $exdate = array_merge($exdate, $val->getParts()); + } + sort($exdate); + } + if (isset($vevent->RRULE)) { + foreach ($vevent->select('RRULE') as $rr) { + foreach ($rr->getParts() as $key => $val) { + // ignore default values (https://github.com/sabre-io/vobject/issues/126) + if ('INTERVAL' === $key && 1 == $val) { + continue; + } + if (is_array($val)) { + $val = implode(',', $val); + } + $rrule[] = "$key=$val"; + } + } + sort($rrule); + } + if (isset($vevent->STATUS)) { + $status = strtoupper($vevent->STATUS->getValue()); + } + + $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master'; + if (is_null($timezone)) { + if ('master' === $recurId) { + $timezone = $vevent->DTSTART->getDateTime()->getTimeZone(); + } else { + $timezone = $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimeZone(); + } + } + + $instances[$recurId] = $vevent; + + if (isset($vevent->ATTENDEE)) { + foreach ($vevent->ATTENDEE as $attendee) { + if ($this->scheduleAgentServerRules && + isset($attendee['SCHEDULE-AGENT']) && + 'CLIENT' === strtoupper($attendee['SCHEDULE-AGENT']->getValue()) + ) { + continue; + } + $partStat = + isset($attendee['PARTSTAT']) ? + strtoupper($attendee['PARTSTAT']) : + 'NEEDS-ACTION'; + + $forceSend = + isset($attendee['SCHEDULE-FORCE-SEND']) ? + strtoupper($attendee['SCHEDULE-FORCE-SEND']) : + null; + + if (isset($attendees[$attendee->getNormalizedValue()])) { + $attendees[$attendee->getNormalizedValue()]['instances'][$recurId] = [ + 'id' => $recurId, + 'partstat' => $partStat, + 'forceSend' => $forceSend, + ]; + } else { + $attendees[$attendee->getNormalizedValue()] = [ + 'href' => $attendee->getNormalizedValue(), + 'instances' => [ + $recurId => [ + 'id' => $recurId, + 'partstat' => $partStat, + ], + ], + 'name' => isset($attendee['CN']) ? (string) $attendee['CN'] : null, + 'forceSend' => $forceSend, + ]; + } + } + } + + foreach ($this->significantChangeProperties as $prop) { + if (isset($vevent->$prop)) { + $propertyValues = $vevent->select($prop); + + $eventSignificantChangeHash .= $prop.':'; + + if ('EXDATE' === $prop) { + $eventSignificantChangeHash .= implode(',', $exdate).';'; + } elseif ('RRULE' === $prop) { + $eventSignificantChangeHash .= implode(',', $rrule).';'; + } else { + foreach ($propertyValues as $val) { + $eventSignificantChangeHash .= $val->getValue().';'; + } + } + } + } + $significantChangeEventProperties[] = $eventSignificantChangeHash; + } + + asort($significantChangeEventProperties); + + foreach ($significantChangeEventProperties as $eventSignificantChangeHash) { + $significantChangeHash .= $eventSignificantChangeHash; + } + $significantChangeHash = md5($significantChangeHash); + + return compact( + 'uid', + 'organizer', + 'organizerName', + 'organizerScheduleAgent', + 'organizerForceSend', + 'instances', + 'attendees', + 'sequence', + 'exdate', + 'timezone', + 'significantChangeHash', + 'status' + ); + } +} diff --git a/lib/vendor/sabre/vobject/lib/ITip/ITipException.php b/lib/vendor/sabre/vobject/lib/ITip/ITipException.php new file mode 100644 index 0000000..9495636 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/ITip/ITipException.php @@ -0,0 +1,16 @@ +scheduleStatus) { + return false; + } else { + list($scheduleStatus) = explode(';', $this->scheduleStatus); + + return $scheduleStatus; + } + } +} diff --git a/lib/vendor/sabre/vobject/lib/ITip/SameOrganizerForAllComponentsException.php b/lib/vendor/sabre/vobject/lib/ITip/SameOrganizerForAllComponentsException.php new file mode 100644 index 0000000..4c48625 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/ITip/SameOrganizerForAllComponentsException.php @@ -0,0 +1,18 @@ +parent = null; + $this->root = null; + } + + /* {{{ IteratorAggregator interface */ + + /** + * Returns the iterator for this object. + * + * @return ElementList + */ + #[\ReturnTypeWillChange] + public function getIterator() + { + if (!is_null($this->iterator)) { + return $this->iterator; + } + + return new ElementList([$this]); + } + + /** + * Sets the overridden iterator. + * + * Note that this is not actually part of the iterator interface + */ + public function setIterator(ElementList $iterator) + { + $this->iterator = $iterator; + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on) + * 2 - An inconsequential issue + * 3 - A severe issue. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + return []; + } + + /* }}} */ + + /* {{{ Countable interface */ + + /** + * Returns the number of elements. + * + * @return int + */ + #[\ReturnTypeWillChange] + public function count() + { + $it = $this->getIterator(); + + return $it->count(); + } + + /* }}} */ + + /* {{{ ArrayAccess Interface */ + + /** + * Checks if an item exists through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + * + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists($offset) + { + $iterator = $this->getIterator(); + + return $iterator->offsetExists($offset); + } + + /** + * Gets an item through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + * + * @return mixed + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + $iterator = $this->getIterator(); + + return $iterator->offsetGet($offset); + } + + /** + * Sets an item through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + * @param mixed $value + * + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + $iterator = $this->getIterator(); + $iterator->offsetSet($offset, $value); + + // @codeCoverageIgnoreStart + // + // This method always throws an exception, so we ignore the closing + // brace + } + + // @codeCoverageIgnoreEnd + + /** + * Sets an item through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + * + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) + { + $iterator = $this->getIterator(); + $iterator->offsetUnset($offset); + + // @codeCoverageIgnoreStart + // + // This method always throws an exception, so we ignore the closing + // brace + } + + // @codeCoverageIgnoreEnd + + /* }}} */ +} diff --git a/lib/vendor/sabre/vobject/lib/PHPUnitAssertions.php b/lib/vendor/sabre/vobject/lib/PHPUnitAssertions.php new file mode 100644 index 0000000..45c0a21 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/PHPUnitAssertions.php @@ -0,0 +1,75 @@ +fail('Input must be a string, stream or VObject component'); + } + unset($input->PRODID); + if ($input instanceof Component\VCalendar && 'GREGORIAN' === (string) $input->CALSCALE) { + unset($input->CALSCALE); + } + + return $input; + }; + + $expected = $getObj($expected)->serialize(); + $actual = $getObj($actual)->serialize(); + + // Finding wildcards in expected. + preg_match_all('|^([A-Z]+):\\*\\*ANY\\*\\*\r$|m', $expected, $matches, PREG_SET_ORDER); + + foreach ($matches as $match) { + $actual = preg_replace( + '|^'.preg_quote($match[1], '|').':(.*)\r$|m', + $match[1].':**ANY**'."\r", + $actual + ); + } + + $this->assertEquals( + $expected, + $actual, + $message + ); + } +} diff --git a/lib/vendor/sabre/vobject/lib/Parameter.php b/lib/vendor/sabre/vobject/lib/Parameter.php new file mode 100644 index 0000000..0f0b586 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Parameter.php @@ -0,0 +1,368 @@ +root = $root; + if (is_null($name)) { + $this->noName = true; + $this->name = static::guessParameterNameByValue($value); + } else { + $this->name = strtoupper($name); + } + + // If guessParameterNameByValue() returns an empty string + // above, we're actually dealing with a parameter that has no value. + // In that case we have to move the value to the name. + if ('' === $this->name) { + $this->noName = false; + $this->name = strtoupper($value); + } else { + $this->setValue($value); + } + } + + /** + * Try to guess property name by value, can be used for vCard 2.1 nameless parameters. + * + * Figuring out what the name should have been. Note that a ton of + * these are rather silly in 2014 and would probably rarely be + * used, but we like to be complete. + * + * @param string $value + * + * @return string + */ + public static function guessParameterNameByValue($value) + { + switch (strtoupper($value)) { + // Encodings + case '7-BIT': + case 'QUOTED-PRINTABLE': + case 'BASE64': + $name = 'ENCODING'; + break; + + // Common types + case 'WORK': + case 'HOME': + case 'PREF': + // Delivery Label Type + case 'DOM': + case 'INTL': + case 'POSTAL': + case 'PARCEL': + // Telephone types + case 'VOICE': + case 'FAX': + case 'MSG': + case 'CELL': + case 'PAGER': + case 'BBS': + case 'MODEM': + case 'CAR': + case 'ISDN': + case 'VIDEO': + // EMAIL types (lol) + case 'AOL': + case 'APPLELINK': + case 'ATTMAIL': + case 'CIS': + case 'EWORLD': + case 'INTERNET': + case 'IBMMAIL': + case 'MCIMAIL': + case 'POWERSHARE': + case 'PRODIGY': + case 'TLX': + case 'X400': + // Photo / Logo format types + case 'GIF': + case 'CGM': + case 'WMF': + case 'BMP': + case 'DIB': + case 'PICT': + case 'TIFF': + case 'PDF': + case 'PS': + case 'JPEG': + case 'MPEG': + case 'MPEG2': + case 'AVI': + case 'QTIME': + // Sound Digital Audio Type + case 'WAVE': + case 'PCM': + case 'AIFF': + // Key types + case 'X509': + case 'PGP': + $name = 'TYPE'; + break; + + // Value types + case 'INLINE': + case 'URL': + case 'CONTENT-ID': + case 'CID': + $name = 'VALUE'; + break; + + default: + $name = ''; + } + + return $name; + } + + /** + * Updates the current value. + * + * This may be either a single, or multiple strings in an array. + * + * @param string|array $value + */ + public function setValue($value) + { + $this->value = $value; + } + + /** + * Returns the current value. + * + * This method will always return a string, or null. If there were multiple + * values, it will automatically concatenate them (separated by comma). + * + * @return string|null + */ + public function getValue() + { + if (is_array($this->value)) { + return implode(',', $this->value); + } else { + return $this->value; + } + } + + /** + * Sets multiple values for this parameter. + */ + public function setParts(array $value) + { + $this->value = $value; + } + + /** + * Returns all values for this parameter. + * + * If there were no values, an empty array will be returned. + * + * @return array + */ + public function getParts() + { + if (is_array($this->value)) { + return $this->value; + } elseif (is_null($this->value)) { + return []; + } else { + return [$this->value]; + } + } + + /** + * Adds a value to this parameter. + * + * If the argument is specified as an array, all items will be added to the + * parameter value list. + * + * @param string|array $part + */ + public function addValue($part) + { + if (is_null($this->value)) { + $this->value = $part; + } else { + $this->value = array_merge((array) $this->value, (array) $part); + } + } + + /** + * Checks if this parameter contains the specified value. + * + * This is a case-insensitive match. It makes sense to call this for for + * instance the TYPE parameter, to see if it contains a keyword such as + * 'WORK' or 'FAX'. + * + * @param string $value + * + * @return bool + */ + public function has($value) + { + return in_array( + strtolower($value), + array_map('strtolower', (array) $this->value) + ); + } + + /** + * Turns the object back into a serialized blob. + * + * @return string + */ + public function serialize() + { + $value = $this->getParts(); + + if (0 === count($value)) { + return $this->name.'='; + } + + if (Document::VCARD21 === $this->root->getDocumentType() && $this->noName) { + return implode(';', $value); + } + + return $this->name.'='.array_reduce( + $value, + function ($out, $item) { + if (!is_null($out)) { + $out .= ','; + } + + // If there's no special characters in the string, we'll use the simple + // format. + // + // The list of special characters is defined as: + // + // Any character except CONTROL, DQUOTE, ";", ":", "," + // + // by the iCalendar spec: + // https://tools.ietf.org/html/rfc5545#section-3.1 + // + // And we add ^ to that because of: + // https://tools.ietf.org/html/rfc6868 + // + // But we've found that iCal (7.0, shipped with OSX 10.9) + // severely trips on + characters not being quoted, so we + // added + as well. + if (!preg_match('#(?: [\n":;\^,\+] )#x', $item)) { + return $out.$item; + } else { + // Enclosing in double-quotes, and using RFC6868 for encoding any + // special characters + $out .= '"'.strtr( + $item, + [ + '^' => '^^', + "\n" => '^n', + '"' => '^\'', + ] + ).'"'; + + return $out; + } + } + ); + } + + /** + * This method returns an array, with the representation as it should be + * encoded in JSON. This is used to create jCard or jCal documents. + * + * @return array + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return $this->value; + } + + /** + * This method serializes the data into XML. This is used to create xCard or + * xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + public function xmlSerialize(Xml\Writer $writer): void + { + foreach (explode(',', $this->value) as $value) { + $writer->writeElement('text', $value); + } + } + + /** + * Called when this object is being cast to a string. + * + * @return string + */ + public function __toString() + { + return (string) $this->getValue(); + } + + /** + * Returns the iterator for this object. + * + * @return ElementList + */ + #[\ReturnTypeWillChange] + public function getIterator() + { + if (!is_null($this->iterator)) { + return $this->iterator; + } + + return $this->iterator = new ArrayIterator((array) $this->value); + } +} diff --git a/lib/vendor/sabre/vobject/lib/ParseException.php b/lib/vendor/sabre/vobject/lib/ParseException.php new file mode 100644 index 0000000..a8f497b --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/ParseException.php @@ -0,0 +1,14 @@ +setInput($input); + } + if (is_null($this->input)) { + throw new EofException('End of input stream, or no input supplied'); + } + + if (0 !== $options) { + $this->options = $options; + } + + switch ($this->input[0]) { + case 'vcalendar': + $this->root = new VCalendar([], false); + break; + case 'vcard': + $this->root = new VCard([], false); + break; + default: + throw new ParseException('The root component must either be a vcalendar, or a vcard'); + } + foreach ($this->input[1] as $prop) { + $this->root->add($this->parseProperty($prop)); + } + if (isset($this->input[2])) { + foreach ($this->input[2] as $comp) { + $this->root->add($this->parseComponent($comp)); + } + } + + // Resetting the input so we can throw an feof exception the next time. + $this->input = null; + + return $this->root; + } + + /** + * Parses a component. + * + * @return \Sabre\VObject\Component + */ + public function parseComponent(array $jComp) + { + // We can remove $self from PHP 5.4 onward. + $self = $this; + + $properties = array_map( + function ($jProp) use ($self) { + return $self->parseProperty($jProp); + }, + $jComp[1] + ); + + if (isset($jComp[2])) { + $components = array_map( + function ($jComp) use ($self) { + return $self->parseComponent($jComp); + }, + $jComp[2] + ); + } else { + $components = []; + } + + return $this->root->createComponent( + $jComp[0], + array_merge($properties, $components), + $defaults = false + ); + } + + /** + * Parses properties. + * + * @return \Sabre\VObject\Property + */ + public function parseProperty(array $jProp) + { + list( + $propertyName, + $parameters, + $valueType + ) = $jProp; + + $propertyName = strtoupper($propertyName); + + // This is the default class we would be using if we didn't know the + // value type. We're using this value later in this function. + $defaultPropertyClass = $this->root->getClassNameForPropertyName($propertyName); + + $parameters = (array) $parameters; + + $value = array_slice($jProp, 3); + + $valueType = strtoupper($valueType); + + if (isset($parameters['group'])) { + $propertyName = $parameters['group'].'.'.$propertyName; + unset($parameters['group']); + } + + $prop = $this->root->createProperty($propertyName, null, $parameters, $valueType); + $prop->setJsonValue($value); + + // We have to do something awkward here. FlatText as well as Text + // represents TEXT values. We have to normalize these here. In the + // future we can get rid of FlatText once we're allowed to break BC + // again. + if (FlatText::class === $defaultPropertyClass) { + $defaultPropertyClass = Text::class; + } + + // If the value type we received (e.g.: TEXT) was not the default value + // type for the given property (e.g.: BDAY), we need to add a VALUE= + // parameter. + if ($defaultPropertyClass !== get_class($prop)) { + $prop['VALUE'] = $valueType; + } + + return $prop; + } + + /** + * Sets the input data. + * + * @param resource|string|array $input + */ + public function setInput($input) + { + if (is_resource($input)) { + $input = stream_get_contents($input); + } + if (is_string($input)) { + $input = json_decode($input); + } + $this->input = $input; + } +} diff --git a/lib/vendor/sabre/vobject/lib/Parser/MimeDir.php b/lib/vendor/sabre/vobject/lib/Parser/MimeDir.php new file mode 100644 index 0000000..5520530 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Parser/MimeDir.php @@ -0,0 +1,710 @@ +root = null; + + if (!is_null($input)) { + $this->setInput($input); + } + + if (!\is_resource($this->input)) { + // Null was passed as input, but there was no existing input buffer + // There is nothing to parse. + throw new ParseException('No input provided to parse'); + } + + if (0 !== $options) { + $this->options = $options; + } + + $this->parseDocument(); + + return $this->root; + } + + /** + * By default all input will be assumed to be UTF-8. + * + * However, both iCalendar and vCard might be encoded using different + * character sets. The character set is usually set in the mime-type. + * + * If this is the case, use setEncoding to specify that a different + * encoding will be used. If this is set, the parser will automatically + * convert all incoming data to UTF-8. + * + * @param string $charset + */ + public function setCharset($charset) + { + if (!in_array($charset, self::$SUPPORTED_CHARSETS)) { + throw new \InvalidArgumentException('Unsupported encoding. (Supported encodings: '.implode(', ', self::$SUPPORTED_CHARSETS).')'); + } + $this->charset = $charset; + } + + /** + * Sets the input buffer. Must be a string or stream. + * + * @param resource|string $input + */ + public function setInput($input) + { + // Resetting the parser + $this->lineIndex = 0; + $this->startLine = 0; + + if (is_string($input)) { + // Converting to a stream. + $stream = fopen('php://temp', 'r+'); + fwrite($stream, $input); + rewind($stream); + $this->input = $stream; + } elseif (is_resource($input)) { + $this->input = $input; + } else { + throw new \InvalidArgumentException('This parser can only read from strings or streams.'); + } + } + + /** + * Parses an entire document. + */ + protected function parseDocument() + { + $line = $this->readLine(); + + // BOM is ZERO WIDTH NO-BREAK SPACE (U+FEFF). + // It's 0xEF 0xBB 0xBF in UTF-8 hex. + if (3 <= strlen($line) + && 0xef === ord($line[0]) + && 0xbb === ord($line[1]) + && 0xbf === ord($line[2])) { + $line = substr($line, 3); + } + + switch (strtoupper($line)) { + case 'BEGIN:VCALENDAR': + $class = VCalendar::$componentMap['VCALENDAR']; + break; + case 'BEGIN:VCARD': + $class = VCard::$componentMap['VCARD']; + break; + default: + throw new ParseException('This parser only supports VCARD and VCALENDAR files'); + } + + $this->root = new $class([], false); + + while (true) { + // Reading until we hit END: + try { + $line = $this->readLine(); + } catch (EofException $oEx) { + $line = 'END:'.$this->root->name; + } + if ('END:' === strtoupper(substr($line, 0, 4))) { + break; + } + $result = $this->parseLine($line); + if ($result) { + $this->root->add($result); + } + } + + $name = strtoupper(substr($line, 4)); + if ($name !== $this->root->name) { + throw new ParseException('Invalid MimeDir file. expected: "END:'.$this->root->name.'" got: "END:'.$name.'"'); + } + } + + /** + * Parses a line, and if it hits a component, it will also attempt to parse + * the entire component. + * + * @param string $line Unfolded line + * + * @return Node + */ + protected function parseLine($line) + { + // Start of a new component + if ('BEGIN:' === strtoupper(substr($line, 0, 6))) { + if (substr($line, 6) === $this->root->name) { + throw new ParseException('Invalid MimeDir file. Unexpected component: "'.$line.'" in document type '.$this->root->name); + } + $component = $this->root->createComponent(substr($line, 6), [], false); + + while (true) { + // Reading until we hit END: + $line = $this->readLine(); + if ('END:' === strtoupper(substr($line, 0, 4))) { + break; + } + $result = $this->parseLine($line); + if ($result) { + $component->add($result); + } + } + + $name = strtoupper(substr($line, 4)); + if ($name !== $component->name) { + throw new ParseException('Invalid MimeDir file. expected: "END:'.$component->name.'" got: "END:'.$name.'"'); + } + + return $component; + } else { + // Property reader + $property = $this->readProperty($line); + if (!$property) { + // Ignored line + return false; + } + + return $property; + } + } + + /** + * We need to look ahead 1 line every time to see if we need to 'unfold' + * the next line. + * + * If that was not the case, we store it here. + * + * @var string|null + */ + protected $lineBuffer; + + /** + * The real current line number. + */ + protected $lineIndex = 0; + + /** + * In the case of unfolded lines, this property holds the line number for + * the start of the line. + * + * @var int + */ + protected $startLine = 0; + + /** + * Contains a 'raw' representation of the current line. + * + * @var string + */ + protected $rawLine; + + /** + * Reads a single line from the buffer. + * + * This method strips any newlines and also takes care of unfolding. + * + * @throws \Sabre\VObject\EofException + * + * @return string + */ + protected function readLine() + { + if (!\is_null($this->lineBuffer)) { + $rawLine = $this->lineBuffer; + $this->lineBuffer = null; + } else { + do { + $eof = \feof($this->input); + + $rawLine = \fgets($this->input); + + if ($eof || (\feof($this->input) && false === $rawLine)) { + throw new EofException('End of document reached prematurely'); + } + if (false === $rawLine) { + throw new ParseException('Error reading from input stream'); + } + $rawLine = \rtrim($rawLine, "\r\n"); + } while ('' === $rawLine); // Skipping empty lines + ++$this->lineIndex; + } + $line = $rawLine; + + $this->startLine = $this->lineIndex; + + // Looking ahead for folded lines. + while (true) { + $nextLine = \rtrim(\fgets($this->input), "\r\n"); + ++$this->lineIndex; + if (!$nextLine) { + break; + } + if ("\t" === $nextLine[0] || ' ' === $nextLine[0]) { + $curLine = \substr($nextLine, 1); + $line .= $curLine; + $rawLine .= "\n ".$curLine; + } else { + $this->lineBuffer = $nextLine; + break; + } + } + $this->rawLine = $rawLine; + + return $line; + } + + /** + * Reads a property or component from a line. + */ + protected function readProperty($line) + { + if ($this->options & self::OPTION_FORGIVING) { + $propNameToken = 'A-Z0-9\-\._\\/'; + } else { + $propNameToken = 'A-Z0-9\-\.'; + } + + $paramNameToken = 'A-Z0-9\-'; + $safeChar = '^";:,'; + $qSafeChar = '^"'; + + $regex = "/ + ^(?P [$propNameToken]+ ) (?=[;:]) # property name + | + (?<=:)(?P .+)$ # property value + | + ;(?P [$paramNameToken]+) (?=[=;:]) # parameter name + | + (=|,)(?P # parameter value + (?: [$safeChar]*) | + \"(?: [$qSafeChar]+)\" + ) (?=[;:,]) + /xi"; + + //echo $regex, "\n"; exit(); + preg_match_all($regex, $line, $matches, PREG_SET_ORDER); + + $property = [ + 'name' => null, + 'parameters' => [], + 'value' => null, + ]; + + /* + * Keep track on the last token we parsed in order to do + * better error checking + */ + $lastToken = null; + + $lastParam = null; + + /* + * Looping through all the tokens. + * + * Note that we are looping through them in reverse order, because if a + * sub-pattern matched, the subsequent named patterns will not show up + * in the result. + */ + foreach ($matches as $match) { + if (isset($match['paramValue'])) { + if ($match['paramValue'] && '"' === $match['paramValue'][0]) { + $value = substr($match['paramValue'], 1, -1); + } else { + $value = $match['paramValue']; + } + + $value = $this->unescapeParam($value); + + if (is_null($lastParam)) { + if ($this->options & self::OPTION_IGNORE_INVALID_LINES) { + // When the property can't be matched and the configuration + // option is set to ignore invalid lines, we ignore this line + // This can happen when servers provide faulty data as iCloud + // frequently does with X-APPLE-STRUCTURED-LOCATION + $lastToken = self::TOKEN_PARAMVALUE; + continue; + } + throw new ParseException('Invalid Mimedir file. Line starting at '.$this->startLine.' did not follow iCalendar/vCard conventions'); + } + + if ('=' == $match[0][0] && self::TOKEN_PARAMNAME != $lastToken) { + throw new ParseException('Invalid Mimedir file. Line starting at '.$this->startLine.': Missing parameter name for parameter value "'.$match['paramValue'].'"'); + } + + if (is_null($property['parameters'][$lastParam])) { + $property['parameters'][$lastParam] = $value; + } elseif (is_array($property['parameters'][$lastParam])) { + $property['parameters'][$lastParam][] = $value; + } elseif ($property['parameters'][$lastParam] === $value) { + // When the current value of the parameter is the same as the + // new one, then we can leave the current parameter as it is. + } else { + $property['parameters'][$lastParam] = [ + $property['parameters'][$lastParam], + $value, + ]; + } + $lastToken = self::TOKEN_PARAMVALUE; + continue; + } + if (isset($match['paramName'])) { + $lastParam = strtoupper($match['paramName']); + if (!isset($property['parameters'][$lastParam])) { + $property['parameters'][$lastParam] = null; + } + $lastToken = self::TOKEN_PARAMNAME; + continue; + } + if (isset($match['propValue'])) { + $property['value'] = $match['propValue']; + $lastToken = self::TOKEN_PROPVALUE; + continue; + } + if (isset($match['name']) && 0 < strlen($match['name'])) { + $property['name'] = strtoupper($match['name']); + $lastToken = self::TOKEN_PROPNAME; + continue; + } + + // @codeCoverageIgnoreStart + throw new \LogicException('This code should not be reachable'); + // @codeCoverageIgnoreEnd + } + + if (is_null($property['value'])) { + $property['value'] = ''; + } + if (!isset($property['name']) || 0 == strlen($property['name'])) { + if ($this->options & self::OPTION_IGNORE_INVALID_LINES) { + return false; + } + throw new ParseException('Invalid Mimedir file. Line starting at '.$this->startLine.' did not follow iCalendar/vCard conventions'); + } + + // vCard 2.1 states that parameters may appear without a name, and only + // a value. We can deduce the value based on its name. + // + // Our parser will get those as parameters without a value instead, so + // we're filtering these parameters out first. + $namedParameters = []; + $namelessParameters = []; + + foreach ($property['parameters'] as $name => $value) { + if (!is_null($value)) { + $namedParameters[$name] = $value; + } else { + $namelessParameters[] = $name; + } + } + + $propObj = $this->root->createProperty($property['name'], null, $namedParameters, null, $this->startLine, $line); + + foreach ($namelessParameters as $namelessParameter) { + $propObj->add(null, $namelessParameter); + } + + if (isset($propObj['ENCODING']) && 'QUOTED-PRINTABLE' === strtoupper($propObj['ENCODING'])) { + $propObj->setQuotedPrintableValue($this->extractQuotedPrintableValue()); + } else { + $charset = $this->charset; + if (Document::VCARD21 === $this->root->getDocumentType() && isset($propObj['CHARSET'])) { + // vCard 2.1 allows the character set to be specified per property. + $charset = (string) $propObj['CHARSET']; + } + switch (strtolower($charset)) { + case 'utf-8': + break; + case 'windows-1252': + case 'iso-8859-1': + $property['value'] = mb_convert_encoding($property['value'], 'UTF-8', $charset); + break; + default: + throw new ParseException('Unsupported CHARSET: '.$propObj['CHARSET']); + } + $propObj->setRawMimeDirValue($property['value']); + } + + return $propObj; + } + + /** + * Unescapes a property value. + * + * vCard 2.1 says: + * * Semi-colons must be escaped in some property values, specifically + * ADR, ORG and N. + * * Semi-colons must be escaped in parameter values, because semi-colons + * are also use to separate values. + * * No mention of escaping backslashes with another backslash. + * * newlines are not escaped either, instead QUOTED-PRINTABLE is used to + * span values over more than 1 line. + * + * vCard 3.0 says: + * * (rfc2425) Backslashes, newlines (\n or \N) and comma's must be + * escaped, all time time. + * * Comma's are used for delimiters in multiple values + * * (rfc2426) Adds to to this that the semi-colon MUST also be escaped, + * as in some properties semi-colon is used for separators. + * * Properties using semi-colons: N, ADR, GEO, ORG + * * Both ADR and N's individual parts may be broken up further with a + * comma. + * * Properties using commas: NICKNAME, CATEGORIES + * + * vCard 4.0 (rfc6350) says: + * * Commas must be escaped. + * * Semi-colons may be escaped, an unescaped semi-colon _may_ be a + * delimiter, depending on the property. + * * Backslashes must be escaped + * * Newlines must be escaped as either \N or \n. + * * Some compound properties may contain multiple parts themselves, so a + * comma within a semi-colon delimited property may also be unescaped + * to denote multiple parts _within_ the compound property. + * * Text-properties using semi-colons: N, ADR, ORG, CLIENTPIDMAP. + * * Text-properties using commas: NICKNAME, RELATED, CATEGORIES, PID. + * + * Even though the spec says that commas must always be escaped, the + * example for GEO in Section 6.5.2 seems to violate this. + * + * iCalendar 2.0 (rfc5545) says: + * * Commas or semi-colons may be used as delimiters, depending on the + * property. + * * Commas, semi-colons, backslashes, newline (\N or \n) are always + * escaped, unless they are delimiters. + * * Colons shall not be escaped. + * * Commas can be considered the 'default delimiter' and is described as + * the delimiter in cases where the order of the multiple values is + * insignificant. + * * Semi-colons are described as the delimiter for 'structured values'. + * They are specifically used in Semi-colons are used as a delimiter in + * REQUEST-STATUS, RRULE, GEO and EXRULE. EXRULE is deprecated however. + * + * Now for the parameters + * + * If delimiter is not set (empty string) this method will just return a string. + * If it's a comma or a semi-colon the string will be split on those + * characters, and always return an array. + * + * @param string $input + * @param string $delimiter + * + * @return string|string[] + */ + public static function unescapeValue($input, $delimiter = ';') + { + $regex = '# (?: (\\\\ (?: \\\\ | N | n | ; | , ) )'; + if ($delimiter) { + $regex .= ' | ('.$delimiter.')'; + } + $regex .= ') #x'; + + $matches = preg_split($regex, $input, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + + $resultArray = []; + $result = ''; + + foreach ($matches as $match) { + switch ($match) { + case '\\\\': + $result .= '\\'; + break; + case '\N': + case '\n': + $result .= "\n"; + break; + case '\;': + $result .= ';'; + break; + case '\,': + $result .= ','; + break; + case $delimiter: + $resultArray[] = $result; + $result = ''; + break; + default: + $result .= $match; + break; + } + } + + $resultArray[] = $result; + + return $delimiter ? $resultArray : $result; + } + + /** + * Unescapes a parameter value. + * + * vCard 2.1: + * * Does not mention a mechanism for this. In addition, double quotes + * are never used to wrap values. + * * This means that parameters can simply not contain colons or + * semi-colons. + * + * vCard 3.0 (rfc2425, rfc2426): + * * Parameters _may_ be surrounded by double quotes. + * * If this is not the case, semi-colon, colon and comma may simply not + * occur (the comma used for multiple parameter values though). + * * If it is surrounded by double-quotes, it may simply not contain + * double-quotes. + * * This means that a parameter can in no case encode double-quotes, or + * newlines. + * + * vCard 4.0 (rfc6350) + * * Behavior seems to be identical to vCard 3.0 + * + * iCalendar 2.0 (rfc5545) + * * Behavior seems to be identical to vCard 3.0 + * + * Parameter escaping mechanism (rfc6868) : + * * This rfc describes a new way to escape parameter values. + * * New-line is encoded as ^n + * * ^ is encoded as ^^. + * * " is encoded as ^' + * + * @param string $input + */ + private function unescapeParam($input) + { + return + preg_replace_callback( + '#(\^(\^|n|\'))#', + function ($matches) { + switch ($matches[2]) { + case 'n': + return "\n"; + case '^': + return '^'; + case '\'': + return '"'; + + // @codeCoverageIgnoreStart + } + // @codeCoverageIgnoreEnd + }, + $input + ); + } + + /** + * Gets the full quoted printable value. + * + * We need a special method for this, because newlines have both a meaning + * in vCards, and in QuotedPrintable. + * + * This method does not do any decoding. + * + * @return string + */ + private function extractQuotedPrintableValue() + { + // We need to parse the raw line again to get the start of the value. + // + // We are basically looking for the first colon (:), but we need to + // skip over the parameters first, as they may contain one. + $regex = '/^ + (?: [^:])+ # Anything but a colon + (?: "[^"]")* # A parameter in double quotes + : # start of the value we really care about + (.*)$ + /xs'; + + preg_match($regex, $this->rawLine, $matches); + + $value = $matches[1]; + // Removing the first whitespace character from every line. Kind of + // like unfolding, but we keep the newline. + $value = str_replace("\n ", "\n", $value); + + // Microsoft products don't always correctly fold lines, they may be + // missing a whitespace. So if 'forgiving' is turned on, we will take + // those as well. + if ($this->options & self::OPTION_FORGIVING) { + while ('=' === substr($value, -1) && $this->lineBuffer) { + // Reading the line + $this->readLine(); + // Grabbing the raw form + $value .= "\n".$this->rawLine; + } + } + + return $value; + } +} diff --git a/lib/vendor/sabre/vobject/lib/Parser/Parser.php b/lib/vendor/sabre/vobject/lib/Parser/Parser.php new file mode 100644 index 0000000..b7b6114 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Parser/Parser.php @@ -0,0 +1,75 @@ +setInput($input); + } + $this->options = $options; + } + + /** + * This method starts the parsing process. + * + * If the input was not supplied during construction, it's possible to pass + * it here instead. + * + * If either input or options are not supplied, the defaults will be used. + * + * @param mixed $input + * @param int $options + * + * @return array + */ + abstract public function parse($input = null, $options = 0); + + /** + * Sets the input data. + * + * @param mixed $input + */ + abstract public function setInput($input); +} diff --git a/lib/vendor/sabre/vobject/lib/Parser/XML.php b/lib/vendor/sabre/vobject/lib/Parser/XML.php new file mode 100644 index 0000000..7877317 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Parser/XML.php @@ -0,0 +1,377 @@ +setInput($input); + } + + if (0 !== $options) { + $this->options = $options; + } + + if (is_null($this->input)) { + throw new EofException('End of input stream, or no input supplied'); + } + + switch ($this->input['name']) { + case '{'.self::XCAL_NAMESPACE.'}icalendar': + $this->root = new VCalendar([], false); + $this->pointer = &$this->input['value'][0]; + $this->parseVCalendarComponents($this->root); + break; + + case '{'.self::XCARD_NAMESPACE.'}vcards': + foreach ($this->input['value'] as &$vCard) { + $this->root = new VCard(['version' => '4.0'], false); + $this->pointer = &$vCard; + $this->parseVCardComponents($this->root); + + // We just parse the first element. + break; + } + break; + + default: + throw new ParseException('Unsupported XML standard'); + } + + return $this->root; + } + + /** + * Parse a xCalendar component. + */ + protected function parseVCalendarComponents(Component $parentComponent) + { + foreach ($this->pointer['value'] ?: [] as $children) { + switch (static::getTagName($children['name'])) { + case 'properties': + $this->pointer = &$children['value']; + $this->parseProperties($parentComponent); + break; + + case 'components': + $this->pointer = &$children; + $this->parseComponent($parentComponent); + break; + } + } + } + + /** + * Parse a xCard component. + */ + protected function parseVCardComponents(Component $parentComponent) + { + $this->pointer = &$this->pointer['value']; + $this->parseProperties($parentComponent); + } + + /** + * Parse xCalendar and xCard properties. + * + * @param string $propertyNamePrefix + */ + protected function parseProperties(Component $parentComponent, $propertyNamePrefix = '') + { + foreach ($this->pointer ?: [] as $xmlProperty) { + list($namespace, $tagName) = SabreXml\Service::parseClarkNotation($xmlProperty['name']); + + $propertyName = $tagName; + $propertyValue = []; + $propertyParameters = []; + $propertyType = 'text'; + + // A property which is not part of the standard. + if (self::XCAL_NAMESPACE !== $namespace + && self::XCARD_NAMESPACE !== $namespace) { + $propertyName = 'xml'; + $value = '<'.$tagName.' xmlns="'.$namespace.'"'; + + foreach ($xmlProperty['attributes'] as $attributeName => $attributeValue) { + $value .= ' '.$attributeName.'="'.str_replace('"', '\"', $attributeValue).'"'; + } + + $value .= '>'.$xmlProperty['value'].''; + + $propertyValue = [$value]; + + $this->createProperty( + $parentComponent, + $propertyName, + $propertyParameters, + $propertyType, + $propertyValue + ); + + continue; + } + + // xCard group. + if ('group' === $propertyName) { + if (!isset($xmlProperty['attributes']['name'])) { + continue; + } + + $this->pointer = &$xmlProperty['value']; + $this->parseProperties( + $parentComponent, + strtoupper($xmlProperty['attributes']['name']).'.' + ); + + continue; + } + + // Collect parameters. + foreach ($xmlProperty['value'] as $i => $xmlPropertyChild) { + if (!is_array($xmlPropertyChild) + || 'parameters' !== static::getTagName($xmlPropertyChild['name'])) { + continue; + } + + $xmlParameters = $xmlPropertyChild['value']; + + foreach ($xmlParameters as $xmlParameter) { + $propertyParameterValues = []; + + foreach ($xmlParameter['value'] as $xmlParameterValues) { + $propertyParameterValues[] = $xmlParameterValues['value']; + } + + $propertyParameters[static::getTagName($xmlParameter['name'])] + = implode(',', $propertyParameterValues); + } + + array_splice($xmlProperty['value'], $i, 1); + } + + $propertyNameExtended = ($this->root instanceof VCalendar + ? 'xcal' + : 'xcard').':'.$propertyName; + + switch ($propertyNameExtended) { + case 'xcal:geo': + $propertyType = 'float'; + $propertyValue['latitude'] = 0; + $propertyValue['longitude'] = 0; + + foreach ($xmlProperty['value'] as $xmlRequestChild) { + $propertyValue[static::getTagName($xmlRequestChild['name'])] + = $xmlRequestChild['value']; + } + break; + + case 'xcal:request-status': + $propertyType = 'text'; + + foreach ($xmlProperty['value'] as $xmlRequestChild) { + $propertyValue[static::getTagName($xmlRequestChild['name'])] + = $xmlRequestChild['value']; + } + break; + + case 'xcal:freebusy': + $propertyType = 'freebusy'; + // We don't break because we only want to set + // another property type. + + // no break + case 'xcal:categories': + case 'xcal:resources': + case 'xcal:exdate': + foreach ($xmlProperty['value'] as $specialChild) { + $propertyValue[static::getTagName($specialChild['name'])] + = $specialChild['value']; + } + break; + + case 'xcal:rdate': + $propertyType = 'date-time'; + + foreach ($xmlProperty['value'] as $specialChild) { + $tagName = static::getTagName($specialChild['name']); + + if ('period' === $tagName) { + $propertyParameters['value'] = 'PERIOD'; + $propertyValue[] = implode('/', $specialChild['value']); + } else { + $propertyValue[] = $specialChild['value']; + } + } + break; + + default: + $propertyType = static::getTagName($xmlProperty['value'][0]['name']); + + foreach ($xmlProperty['value'] as $value) { + $propertyValue[] = $value['value']; + } + + if ('date' === $propertyType) { + $propertyParameters['value'] = 'DATE'; + } + break; + } + + $this->createProperty( + $parentComponent, + $propertyNamePrefix.$propertyName, + $propertyParameters, + $propertyType, + $propertyValue + ); + } + } + + /** + * Parse a component. + */ + protected function parseComponent(Component $parentComponent) + { + $components = $this->pointer['value'] ?: []; + + foreach ($components as $component) { + $componentName = static::getTagName($component['name']); + $currentComponent = $this->root->createComponent( + $componentName, + null, + false + ); + + $this->pointer = &$component; + $this->parseVCalendarComponents($currentComponent); + + $parentComponent->add($currentComponent); + } + } + + /** + * Create a property. + * + * @param string $name + * @param array $parameters + * @param string $type + * @param mixed $value + */ + protected function createProperty(Component $parentComponent, $name, $parameters, $type, $value) + { + $property = $this->root->createProperty( + $name, + null, + $parameters, + $type + ); + $parentComponent->add($property); + $property->setXmlValue($value); + } + + /** + * Sets the input data. + * + * @param resource|string $input + */ + public function setInput($input) + { + if (is_resource($input)) { + $input = stream_get_contents($input); + } + + if (is_string($input)) { + $reader = new SabreXml\Reader(); + $reader->elementMap['{'.self::XCAL_NAMESPACE.'}period'] + = XML\Element\KeyValue::class; + $reader->elementMap['{'.self::XCAL_NAMESPACE.'}recur'] + = XML\Element\KeyValue::class; + $reader->xml($input); + $input = $reader->parse(); + } + + $this->input = $input; + } + + /** + * Get tag name from a Clark notation. + * + * @param string $clarkedTagName + * + * @return string + */ + protected static function getTagName($clarkedTagName) + { + list(, $tagName) = SabreXml\Service::parseClarkNotation($clarkedTagName); + + return $tagName; + } +} diff --git a/lib/vendor/sabre/vobject/lib/Parser/XML/Element/KeyValue.php b/lib/vendor/sabre/vobject/lib/Parser/XML/Element/KeyValue.php new file mode 100644 index 0000000..e017725 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Parser/XML/Element/KeyValue.php @@ -0,0 +1,63 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param XML\Reader $reader + */ + public static function xmlDeserialize(SabreXml\Reader $reader): array + { + // If there's no children, we don't do anything. + if ($reader->isEmptyElement) { + $reader->next(); + + return []; + } + + $values = []; + $reader->read(); + + do { + if (SabreXml\Reader::ELEMENT === $reader->nodeType) { + $name = $reader->localName; + $values[$name] = $reader->parseCurrentElement()['value']; + } else { + $reader->read(); + } + } while (SabreXml\Reader::END_ELEMENT !== $reader->nodeType); + + $reader->read(); + + return $values; + } +} diff --git a/lib/vendor/sabre/vobject/lib/Property.php b/lib/vendor/sabre/vobject/lib/Property.php new file mode 100644 index 0000000..7cf5914 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Property.php @@ -0,0 +1,646 @@ +value syntax. + * + * @param Component $root The root document + * @param string $name + * @param string|array|null $value + * @param array $parameters List of parameters + * @param string $group The vcard property group + */ + public function __construct(Component $root, $name, $value = null, array $parameters = [], $group = null, ?int $lineIndex = null, ?string $lineString = null) + { + $this->name = $name; + $this->group = $group; + + $this->root = $root; + + foreach ($parameters as $k => $v) { + $this->add($k, $v); + } + + if (!is_null($value)) { + $this->setValue($value); + } + + if (!is_null($lineIndex)) { + $this->lineIndex = $lineIndex; + } + + if (!is_null($lineString)) { + $this->lineString = $lineString; + } + } + + /** + * Updates the current value. + * + * This may be either a single, or multiple strings in an array. + * + * @param string|array $value + */ + public function setValue($value) + { + $this->value = $value; + } + + /** + * Returns the current value. + * + * This method will always return a singular value. If this was a + * multi-value object, some decision will be made first on how to represent + * it as a string. + * + * To get the correct multi-value version, use getParts. + * + * @return string + */ + public function getValue() + { + if (is_array($this->value)) { + if (0 == count($this->value)) { + return; + } elseif (1 === count($this->value)) { + return $this->value[0]; + } else { + return $this->getRawMimeDirValue(); + } + } else { + return $this->value; + } + } + + /** + * Sets a multi-valued property. + */ + public function setParts(array $parts) + { + $this->value = $parts; + } + + /** + * Returns a multi-valued property. + * + * This method always returns an array, if there was only a single value, + * it will still be wrapped in an array. + * + * @return array + */ + public function getParts() + { + if (is_null($this->value)) { + return []; + } elseif (is_array($this->value)) { + return $this->value; + } else { + return [$this->value]; + } + } + + /** + * Adds a new parameter. + * + * If a parameter with same name already existed, the values will be + * combined. + * If nameless parameter is added, we try to guess its name. + * + * @param string $name + * @param string|array|null $value + */ + public function add($name, $value = null) + { + $noName = false; + if (null === $name) { + $name = Parameter::guessParameterNameByValue($value); + $noName = true; + } + + if (isset($this->parameters[strtoupper($name)])) { + $this->parameters[strtoupper($name)]->addValue($value); + } else { + $param = new Parameter($this->root, $name, $value); + $param->noName = $noName; + $this->parameters[$param->name] = $param; + } + } + + /** + * Returns an iterable list of children. + * + * @return array + */ + public function parameters() + { + return $this->parameters; + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + abstract public function getValueType(); + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + * + * @param string $val + */ + abstract public function setRawMimeDirValue($val); + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + abstract public function getRawMimeDirValue(); + + /** + * Turns the object back into a serialized blob. + * + * @return string + */ + public function serialize() + { + $str = $this->name; + if ($this->group) { + $str = $this->group.'.'.$this->name; + } + + foreach ($this->parameters() as $param) { + $str .= ';'.$param->serialize(); + } + + $str .= ':'.$this->getRawMimeDirValue(); + + $str = \preg_replace( + '/( + (?:^.)? # 1 additional byte in first line because of missing single space (see next line) + .{1,74} # max 75 bytes per line (1 byte is used for a single space added after every CRLF) + (?![\x80-\xbf]) # prevent splitting multibyte characters + )/x', + "$1\r\n ", + $str + ); + + // remove single space after last CRLF + return \substr($str, 0, -1); + } + + /** + * Returns the value, in the format it should be encoded for JSON. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + return $this->getParts(); + } + + /** + * Sets the JSON value, as it would appear in a jCard or jCal object. + * + * The value must always be an array. + */ + public function setJsonValue(array $value) + { + if (1 === count($value)) { + $this->setValue(reset($value)); + } else { + $this->setValue($value); + } + } + + /** + * This method returns an array, with the representation as it should be + * encoded in JSON. This is used to create jCard or jCal documents. + * + * @return array + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $parameters = []; + + foreach ($this->parameters as $parameter) { + if ('VALUE' === $parameter->name) { + continue; + } + $parameters[strtolower($parameter->name)] = $parameter->jsonSerialize(); + } + // In jCard, we need to encode the property-group as a separate 'group' + // parameter. + if ($this->group) { + $parameters['group'] = $this->group; + } + + return array_merge( + [ + strtolower($this->name), + (object) $parameters, + strtolower($this->getValueType()), + ], + $this->getJsonValue() + ); + } + + /** + * Hydrate data from a XML subtree, as it would appear in a xCard or xCal + * object. + */ + public function setXmlValue(array $value) + { + $this->setJsonValue($value); + } + + /** + * This method serializes the data into XML. This is used to create xCard or + * xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + public function xmlSerialize(Xml\Writer $writer): void + { + $parameters = []; + + foreach ($this->parameters as $parameter) { + if ('VALUE' === $parameter->name) { + continue; + } + + $parameters[] = $parameter; + } + + $writer->startElement(strtolower($this->name)); + + if (!empty($parameters)) { + $writer->startElement('parameters'); + + foreach ($parameters as $parameter) { + $writer->startElement(strtolower($parameter->name)); + $writer->write($parameter); + $writer->endElement(); + } + + $writer->endElement(); + } + + $this->xmlSerializeValue($writer); + $writer->endElement(); + } + + /** + * This method serializes only the value of a property. This is used to + * create xCard or xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + protected function xmlSerializeValue(Xml\Writer $writer) + { + $valueType = strtolower($this->getValueType()); + + foreach ($this->getJsonValue() as $values) { + foreach ((array) $values as $value) { + $writer->writeElement($valueType, $value); + } + } + } + + /** + * Called when this object is being cast to a string. + * + * If the property only had a single value, you will get just that. In the + * case the property had multiple values, the contents will be escaped and + * combined with ,. + * + * @return string + */ + public function __toString() + { + return (string) $this->getValue(); + } + + /* ArrayAccess interface {{{ */ + + /** + * Checks if an array element exists. + * + * @param mixed $name + * + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists($name) + { + if (is_int($name)) { + return parent::offsetExists($name); + } + + $name = strtoupper($name); + + foreach ($this->parameters as $parameter) { + if ($parameter->name == $name) { + return true; + } + } + + return false; + } + + /** + * Returns a parameter. + * + * If the parameter does not exist, null is returned. + * + * @param string $name + * + * @return Node + */ + #[\ReturnTypeWillChange] + public function offsetGet($name) + { + if (is_int($name)) { + return parent::offsetGet($name); + } + $name = strtoupper($name); + + if (!isset($this->parameters[$name])) { + return; + } + + return $this->parameters[$name]; + } + + /** + * Creates a new parameter. + * + * @param string $name + * @param mixed $value + * + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetSet($name, $value) + { + if (is_int($name)) { + parent::offsetSet($name, $value); + // @codeCoverageIgnoreStart + // This will never be reached, because an exception is always + // thrown. + return; + // @codeCoverageIgnoreEnd + } + + $param = new Parameter($this->root, $name, $value); + $this->parameters[$param->name] = $param; + } + + /** + * Removes one or more parameters with the specified name. + * + * @param string $name + * + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetUnset($name) + { + if (is_int($name)) { + parent::offsetUnset($name); + // @codeCoverageIgnoreStart + // This will never be reached, because an exception is always + // thrown. + return; + // @codeCoverageIgnoreEnd + } + + unset($this->parameters[strtoupper($name)]); + } + + /* }}} */ + + /** + * This method is automatically called when the object is cloned. + * Specifically, this will ensure all child elements are also cloned. + */ + public function __clone() + { + foreach ($this->parameters as $key => $child) { + $this->parameters[$key] = clone $child; + $this->parameters[$key]->parent = $this; + } + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * - Node::REPAIR - If something is broken, and automatic repair may + * be attempted. + * + * An array is returned with warnings. + * + * Every item in the array has the following properties: + * * level - (number between 1 and 3 with severity information) + * * message - (human readable message) + * * node - (reference to the offending node) + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $warnings = []; + + // Checking if our value is UTF-8 + if (!StringUtil::isUTF8($this->getRawMimeDirValue())) { + $oldValue = $this->getRawMimeDirValue(); + $level = 3; + if ($options & self::REPAIR) { + $newValue = StringUtil::convertToUTF8($oldValue); + if (true || StringUtil::isUTF8($newValue)) { + $this->setRawMimeDirValue($newValue); + $level = 1; + } + } + + if (preg_match('%([\x00-\x08\x0B-\x0C\x0E-\x1F\x7F])%', $oldValue, $matches)) { + $message = 'Property contained a control character (0x'.bin2hex($matches[1]).')'; + } else { + $message = 'Property is not valid UTF-8! '.$oldValue; + } + + $warnings[] = [ + 'level' => $level, + 'message' => $message, + 'node' => $this, + ]; + } + + // Checking if the propertyname does not contain any invalid bytes. + if (!preg_match('/^([A-Z0-9-]+)$/', $this->name)) { + $warnings[] = [ + 'level' => $options & self::REPAIR ? 1 : 3, + 'message' => 'The propertyname: '.$this->name.' contains invalid characters. Only A-Z, 0-9 and - are allowed', + 'node' => $this, + ]; + if ($options & self::REPAIR) { + // Uppercasing and converting underscores to dashes. + $this->name = strtoupper( + str_replace('_', '-', $this->name) + ); + // Removing every other invalid character + $this->name = preg_replace('/([^A-Z0-9-])/u', '', $this->name); + } + } + + if ($encoding = $this->offsetGet('ENCODING')) { + if (Document::VCARD40 === $this->root->getDocumentType()) { + $warnings[] = [ + 'level' => 3, + 'message' => 'ENCODING parameter is not valid in vCard 4.', + 'node' => $this, + ]; + } else { + $encoding = (string) $encoding; + + $allowedEncoding = []; + + switch ($this->root->getDocumentType()) { + case Document::ICALENDAR20: + $allowedEncoding = ['8BIT', 'BASE64']; + break; + case Document::VCARD21: + $allowedEncoding = ['QUOTED-PRINTABLE', 'BASE64', '8BIT']; + break; + case Document::VCARD30: + $allowedEncoding = ['B']; + //Repair vCard30 that use BASE64 encoding + if ($options & self::REPAIR) { + if ('BASE64' === strtoupper($encoding)) { + $encoding = 'B'; + $this['ENCODING'] = $encoding; + $warnings[] = [ + 'level' => 1, + 'message' => 'ENCODING=BASE64 has been transformed to ENCODING=B.', + 'node' => $this, + ]; + } + } + break; + } + if ($allowedEncoding && !in_array(strtoupper($encoding), $allowedEncoding)) { + $warnings[] = [ + 'level' => 3, + 'message' => 'ENCODING='.strtoupper($encoding).' is not valid for this document type.', + 'node' => $this, + ]; + } + } + } + + // Validating inner parameters + foreach ($this->parameters as $param) { + $warnings = array_merge($warnings, $param->validate($options)); + } + + return $warnings; + } + + /** + * Call this method on a document if you're done using it. + * + * It's intended to remove all circular references, so PHP can easily clean + * it up. + */ + public function destroy() + { + parent::destroy(); + foreach ($this->parameters as $param) { + $param->destroy(); + } + $this->parameters = []; + } +} diff --git a/lib/vendor/sabre/vobject/lib/Property/Binary.php b/lib/vendor/sabre/vobject/lib/Property/Binary.php new file mode 100644 index 0000000..1262dd0 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Property/Binary.php @@ -0,0 +1,109 @@ +value = $value[0]; + } else { + throw new \InvalidArgumentException('The argument must either be a string or an array with only one child'); + } + } else { + $this->value = $value; + } + } + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + * + * @param string $val + */ + public function setRawMimeDirValue($val) + { + $this->value = base64_decode($val); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return base64_encode($this->value); + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'BINARY'; + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + return [base64_encode($this->getValue())]; + } + + /** + * Sets the json value, as it would appear in a jCard or jCal object. + * + * The value must always be an array. + */ + public function setJsonValue(array $value) + { + $value = array_map('base64_decode', $value); + parent::setJsonValue($value); + } +} diff --git a/lib/vendor/sabre/vobject/lib/Property/Boolean.php b/lib/vendor/sabre/vobject/lib/Property/Boolean.php new file mode 100644 index 0000000..4bd6ffd --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Property/Boolean.php @@ -0,0 +1,72 @@ +setValue($val); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return $this->value ? 'TRUE' : 'FALSE'; + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'BOOLEAN'; + } + + /** + * Hydrate data from a XML subtree, as it would appear in a xCard or xCal + * object. + */ + public function setXmlValue(array $value) + { + $value = array_map( + function ($value) { + return 'true' === $value; + }, + $value + ); + parent::setXmlValue($value); + } +} diff --git a/lib/vendor/sabre/vobject/lib/Property/FlatText.php b/lib/vendor/sabre/vobject/lib/Property/FlatText.php new file mode 100644 index 0000000..d15cfe0 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Property/FlatText.php @@ -0,0 +1,46 @@ +setValue($val); + } +} diff --git a/lib/vendor/sabre/vobject/lib/Property/FloatValue.php b/lib/vendor/sabre/vobject/lib/Property/FloatValue.php new file mode 100644 index 0000000..e780ae6 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Property/FloatValue.php @@ -0,0 +1,124 @@ +delimiter, $val); + foreach ($val as &$item) { + $item = (float) $item; + } + $this->setParts($val); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return implode( + $this->delimiter, + $this->getParts() + ); + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'FLOAT'; + } + + /** + * Returns the value, in the format it should be encoded for JSON. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + $val = array_map('floatval', $this->getParts()); + + // Special-casing the GEO property. + // + // See: + // http://tools.ietf.org/html/draft-ietf-jcardcal-jcal-04#section-3.4.1.2 + if ('GEO' === $this->name) { + return [$val]; + } + + return $val; + } + + /** + * Hydrate data from a XML subtree, as it would appear in a xCard or xCal + * object. + */ + public function setXmlValue(array $value) + { + $value = array_map('floatval', $value); + parent::setXmlValue($value); + } + + /** + * This method serializes only the value of a property. This is used to + * create xCard or xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + protected function xmlSerializeValue(Xml\Writer $writer) + { + // Special-casing the GEO property. + // + // See: + // http://tools.ietf.org/html/rfc6321#section-3.4.1.2 + if ('GEO' === $this->name) { + $value = array_map('floatval', $this->getParts()); + + $writer->writeElement('latitude', $value[0]); + $writer->writeElement('longitude', $value[1]); + } else { + parent::xmlSerializeValue($writer); + } + } +} diff --git a/lib/vendor/sabre/vobject/lib/Property/ICalendar/CalAddress.php b/lib/vendor/sabre/vobject/lib/Property/ICalendar/CalAddress.php new file mode 100644 index 0000000..c90967d --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Property/ICalendar/CalAddress.php @@ -0,0 +1,63 @@ +getValue(); + if (!strpos($input, ':')) { + return $input; + } + list($schema, $everythingElse) = explode(':', $input, 2); + $schema = strtolower($schema); + if ('mailto' === $schema) { + $everythingElse = strtolower($everythingElse); + } + + return $schema.':'.$everythingElse; + } +} diff --git a/lib/vendor/sabre/vobject/lib/Property/ICalendar/Date.php b/lib/vendor/sabre/vobject/lib/Property/ICalendar/Date.php new file mode 100644 index 0000000..d8e86d1 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Property/ICalendar/Date.php @@ -0,0 +1,18 @@ +setDateTimes($parts); + } else { + parent::setParts($parts); + } + } + + /** + * Updates the current value. + * + * This may be either a single, or multiple strings in an array. + * + * Instead of strings, you may also use DateTime here. + * + * @param string|array|DateTimeInterface $value + */ + public function setValue($value) + { + if (is_array($value) && isset($value[0]) && $value[0] instanceof DateTimeInterface) { + $this->setDateTimes($value); + } elseif ($value instanceof DateTimeInterface) { + $this->setDateTimes([$value]); + } else { + parent::setValue($value); + } + } + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + * + * @param string $val + */ + public function setRawMimeDirValue($val) + { + $this->setValue(explode($this->delimiter, $val)); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return implode($this->delimiter, $this->getParts()); + } + + /** + * Returns true if this is a DATE-TIME value, false if it's a DATE. + * + * @return bool + */ + public function hasTime() + { + return 'DATE' !== strtoupper((string) $this['VALUE']); + } + + /** + * Returns true if this is a floating DATE or DATE-TIME. + * + * Note that DATE is always floating. + */ + public function isFloating() + { + return + !$this->hasTime() || + ( + !isset($this['TZID']) && + false === strpos($this->getValue(), 'Z') + ); + } + + /** + * Returns a date-time value. + * + * Note that if this property contained more than 1 date-time, only the + * first will be returned. To get an array with multiple values, call + * getDateTimes. + * + * If no timezone information is known, because it's either an all-day + * property or floating time, we will use the DateTimeZone argument to + * figure out the exact date. + * + * @param DateTimeZone $timeZone + * + * @return \DateTimeImmutable + */ + public function getDateTime(?DateTimeZone $timeZone = null) + { + $dt = $this->getDateTimes($timeZone); + if (!$dt) { + return; + } + + return $dt[0]; + } + + /** + * Returns multiple date-time values. + * + * If no timezone information is known, because it's either an all-day + * property or floating time, we will use the DateTimeZone argument to + * figure out the exact date. + * + * @param DateTimeZone $timeZone + * + * @return \DateTimeImmutable[] + * @return \DateTime[] + */ + public function getDateTimes(?DateTimeZone $timeZone = null) + { + // Does the property have a TZID? + $tzid = $this['TZID']; + + if ($tzid) { + $timeZone = TimeZoneUtil::getTimeZone((string) $tzid, $this->root); + } + + $dts = []; + foreach ($this->getParts() as $part) { + $dts[] = DateTimeParser::parse($part, $timeZone); + } + + return $dts; + } + + /** + * Sets the property as a DateTime object. + * + * @param bool isFloating If set to true, timezones will be ignored + */ + public function setDateTime(DateTimeInterface $dt, $isFloating = false) + { + $this->setDateTimes([$dt], $isFloating); + } + + /** + * Sets the property as multiple date-time objects. + * + * The first value will be used as a reference for the timezones, and all + * the other values will be adjusted for that timezone + * + * @param DateTimeInterface[] $dt + * @param bool isFloating If set to true, timezones will be ignored + */ + public function setDateTimes(array $dt, $isFloating = false) + { + $values = []; + + if ($this->hasTime()) { + $tz = null; + $isUtc = false; + + foreach ($dt as $d) { + if ($isFloating) { + $values[] = $d->format('Ymd\\THis'); + continue; + } + if (is_null($tz)) { + $tz = $d->getTimeZone(); + $isUtc = in_array($tz->getName(), ['UTC', 'GMT', 'Z', '+00:00']); + if (!$isUtc) { + $this->offsetSet('TZID', $tz->getName()); + } + } else { + $d = $d->setTimeZone($tz); + } + + if ($isUtc) { + $values[] = $d->format('Ymd\\THis\\Z'); + } else { + $values[] = $d->format('Ymd\\THis'); + } + } + if ($isUtc || $isFloating) { + $this->offsetUnset('TZID'); + } + } else { + foreach ($dt as $d) { + $values[] = $d->format('Ymd'); + } + $this->offsetUnset('TZID'); + } + + $this->value = $values; + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return $this->hasTime() ? 'DATE-TIME' : 'DATE'; + } + + /** + * Returns the value, in the format it should be encoded for JSON. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + $dts = $this->getDateTimes(); + $hasTime = $this->hasTime(); + $isFloating = $this->isFloating(); + + $tz = $dts[0]->getTimeZone(); + $isUtc = $isFloating ? false : in_array($tz->getName(), ['UTC', 'GMT', 'Z']); + + return array_map( + function (DateTimeInterface $dt) use ($hasTime, $isUtc) { + if ($hasTime) { + return $dt->format('Y-m-d\\TH:i:s').($isUtc ? 'Z' : ''); + } else { + return $dt->format('Y-m-d'); + } + }, + $dts + ); + } + + /** + * Sets the json value, as it would appear in a jCard or jCal object. + * + * The value must always be an array. + */ + public function setJsonValue(array $value) + { + // dates and times in jCal have one difference to dates and times in + // iCalendar. In jCal date-parts are separated by dashes, and + // time-parts are separated by colons. It makes sense to just remove + // those. + $this->setValue( + array_map( + function ($item) { + return strtr($item, [':' => '', '-' => '']); + }, + $value + ) + ); + } + + /** + * We need to intercept offsetSet, because it may be used to alter the + * VALUE from DATE-TIME to DATE or vice-versa. + * + * @param string $name + * @param mixed $value + * + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetSet($name, $value) + { + parent::offsetSet($name, $value); + if ('VALUE' !== strtoupper($name)) { + return; + } + + // This will ensure that dates are correctly encoded. + $this->setDateTimes($this->getDateTimes()); + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on) + * 2 - An inconsequential issue + * 3 - A severe issue. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $messages = parent::validate($options); + $valueType = $this->getValueType(); + $values = $this->getParts(); + foreach ($values as $value) { + try { + switch ($valueType) { + case 'DATE': + DateTimeParser::parseDate($value); + break; + case 'DATE-TIME': + DateTimeParser::parseDateTime($value); + break; + } + } catch (InvalidDataException $e) { + $messages[] = [ + 'level' => 3, + 'message' => 'The supplied value ('.$value.') is not a correct '.$valueType, + 'node' => $this, + ]; + break; + } + } + + return $messages; + } +} diff --git a/lib/vendor/sabre/vobject/lib/Property/ICalendar/Duration.php b/lib/vendor/sabre/vobject/lib/Property/ICalendar/Duration.php new file mode 100644 index 0000000..e18fe19 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Property/ICalendar/Duration.php @@ -0,0 +1,79 @@ +setValue(explode($this->delimiter, $val)); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return implode($this->delimiter, $this->getParts()); + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'DURATION'; + } + + /** + * Returns a DateInterval representation of the Duration property. + * + * If the property has more than one value, only the first is returned. + * + * @return \DateInterval + */ + public function getDateInterval() + { + $parts = $this->getParts(); + $value = $parts[0]; + + return DateTimeParser::parseDuration($value); + } +} diff --git a/lib/vendor/sabre/vobject/lib/Property/ICalendar/Period.php b/lib/vendor/sabre/vobject/lib/Property/ICalendar/Period.php new file mode 100644 index 0000000..ae8a789 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Property/ICalendar/Period.php @@ -0,0 +1,135 @@ +setValue(explode($this->delimiter, $val)); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return implode($this->delimiter, $this->getParts()); + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'PERIOD'; + } + + /** + * Sets the json value, as it would appear in a jCard or jCal object. + * + * The value must always be an array. + */ + public function setJsonValue(array $value) + { + $value = array_map( + function ($item) { + return strtr(implode('/', $item), [':' => '', '-' => '']); + }, + $value + ); + parent::setJsonValue($value); + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + $return = []; + foreach ($this->getParts() as $item) { + list($start, $end) = explode('/', $item, 2); + + $start = DateTimeParser::parseDateTime($start); + + // This is a duration value. + if ('P' === $end[0]) { + $return[] = [ + $start->format('Y-m-d\\TH:i:s'), + $end, + ]; + } else { + $end = DateTimeParser::parseDateTime($end); + $return[] = [ + $start->format('Y-m-d\\TH:i:s'), + $end->format('Y-m-d\\TH:i:s'), + ]; + } + } + + return $return; + } + + /** + * This method serializes only the value of a property. This is used to + * create xCard or xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + protected function xmlSerializeValue(Xml\Writer $writer) + { + $writer->startElement(strtolower($this->getValueType())); + $value = $this->getJsonValue(); + $writer->writeElement('start', $value[0][0]); + + if ('P' === $value[0][1][0]) { + $writer->writeElement('duration', $value[0][1]); + } else { + $writer->writeElement('end', $value[0][1]); + } + + $writer->endElement(); + } +} diff --git a/lib/vendor/sabre/vobject/lib/Property/ICalendar/Recur.php b/lib/vendor/sabre/vobject/lib/Property/ICalendar/Recur.php new file mode 100644 index 0000000..cd3d7a5 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Property/ICalendar/Recur.php @@ -0,0 +1,344 @@ +value array that is accessible using + * getParts, and may be set using setParts. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Recur extends Property +{ + /** + * Updates the current value. + * + * This may be either a single, or multiple strings in an array. + * + * @param string|array $value + */ + public function setValue($value) + { + // If we're getting the data from json, we'll be receiving an object + if ($value instanceof \StdClass) { + $value = (array) $value; + } + + if (is_array($value)) { + $newVal = []; + foreach ($value as $k => $v) { + if (is_string($v)) { + $v = strtoupper($v); + + // The value had multiple sub-values + if (false !== strpos($v, ',')) { + $v = explode(',', $v); + } + if (0 === strcmp($k, 'until')) { + $v = strtr($v, [':' => '', '-' => '']); + } + } elseif (is_array($v)) { + $v = array_map('strtoupper', $v); + } + + $newVal[strtoupper($k)] = $v; + } + $this->value = $newVal; + } elseif (is_string($value)) { + $this->value = self::stringToArray($value); + } else { + throw new \InvalidArgumentException('You must either pass a string, or a key=>value array'); + } + } + + /** + * Returns the current value. + * + * This method will always return a singular value. If this was a + * multi-value object, some decision will be made first on how to represent + * it as a string. + * + * To get the correct multi-value version, use getParts. + * + * @return string + */ + public function getValue() + { + $out = []; + foreach ($this->value as $key => $value) { + $out[] = $key.'='.(is_array($value) ? implode(',', $value) : $value); + } + + return strtoupper(implode(';', $out)); + } + + /** + * Sets a multi-valued property. + */ + public function setParts(array $parts) + { + $this->setValue($parts); + } + + /** + * Returns a multi-valued property. + * + * This method always returns an array, if there was only a single value, + * it will still be wrapped in an array. + * + * @return array + */ + public function getParts() + { + return $this->value; + } + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + * + * @param string $val + */ + public function setRawMimeDirValue($val) + { + $this->setValue($val); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return $this->getValue(); + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'RECUR'; + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + $values = []; + foreach ($this->getParts() as $k => $v) { + if (0 === strcmp($k, 'UNTIL')) { + $date = new DateTime($this->root, null, $v); + $values[strtolower($k)] = $date->getJsonValue()[0]; + } elseif (0 === strcmp($k, 'COUNT')) { + $values[strtolower($k)] = intval($v); + } else { + $values[strtolower($k)] = $v; + } + } + + return [$values]; + } + + /** + * This method serializes only the value of a property. This is used to + * create xCard or xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + protected function xmlSerializeValue(Xml\Writer $writer) + { + $valueType = strtolower($this->getValueType()); + + foreach ($this->getJsonValue() as $value) { + $writer->writeElement($valueType, $value); + } + } + + /** + * Parses an RRULE value string, and turns it into a struct-ish array. + * + * @param string $value + * + * @return array + */ + public static function stringToArray($value) + { + $value = strtoupper($value); + $newValue = []; + foreach (explode(';', $value) as $part) { + // Skipping empty parts. + if (empty($part)) { + continue; + } + + $parts = explode('=', $part); + + if (2 !== count($parts)) { + throw new InvalidDataException('The supplied iCalendar RRULE part is incorrect: '.$part); + } + + list($partName, $partValue) = $parts; + + // The value itself had multiple values.. + if (false !== strpos($partValue, ',')) { + $partValue = explode(',', $partValue); + } + $newValue[$partName] = $partValue; + } + + return $newValue; + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on) + * 2 - An inconsequential issue + * 3 - A severe issue. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $repair = ($options & self::REPAIR); + + $warnings = parent::validate($options); + $values = $this->getParts(); + + foreach ($values as $key => $value) { + if ('' === $value) { + $warnings[] = [ + 'level' => $repair ? 1 : 3, + 'message' => 'Invalid value for '.$key.' in '.$this->name, + 'node' => $this, + ]; + if ($repair) { + unset($values[$key]); + } + } elseif ('BYMONTH' == $key) { + $byMonth = (array) $value; + foreach ($byMonth as $i => $v) { + if (!is_numeric($v) || (int) $v < 1 || (int) $v > 12) { + $warnings[] = [ + 'level' => $repair ? 1 : 3, + 'message' => 'BYMONTH in RRULE must have value(s) between 1 and 12!', + 'node' => $this, + ]; + if ($repair) { + if (is_array($value)) { + unset($values[$key][$i]); + } else { + unset($values[$key]); + } + } + } + } + // if there is no valid entry left, remove the whole value + if (is_array($value) && empty($values[$key])) { + unset($values[$key]); + } + } elseif ('BYWEEKNO' == $key) { + $byWeekNo = (array) $value; + foreach ($byWeekNo as $i => $v) { + if (!is_numeric($v) || (int) $v < -53 || 0 == (int) $v || (int) $v > 53) { + $warnings[] = [ + 'level' => $repair ? 1 : 3, + 'message' => 'BYWEEKNO in RRULE must have value(s) from -53 to -1, or 1 to 53!', + 'node' => $this, + ]; + if ($repair) { + if (is_array($value)) { + unset($values[$key][$i]); + } else { + unset($values[$key]); + } + } + } + } + // if there is no valid entry left, remove the whole value + if (is_array($value) && empty($values[$key])) { + unset($values[$key]); + } + } elseif ('BYYEARDAY' == $key) { + $byYearDay = (array) $value; + foreach ($byYearDay as $i => $v) { + if (!is_numeric($v) || (int) $v < -366 || 0 == (int) $v || (int) $v > 366) { + $warnings[] = [ + 'level' => $repair ? 1 : 3, + 'message' => 'BYYEARDAY in RRULE must have value(s) from -366 to -1, or 1 to 366!', + 'node' => $this, + ]; + if ($repair) { + if (is_array($value)) { + unset($values[$key][$i]); + } else { + unset($values[$key]); + } + } + } + } + // if there is no valid entry left, remove the whole value + if (is_array($value) && empty($values[$key])) { + unset($values[$key]); + } + } + } + if (!isset($values['FREQ'])) { + $warnings[] = [ + 'level' => $repair ? 1 : 3, + 'message' => 'FREQ is required in '.$this->name, + 'node' => $this, + ]; + if ($repair) { + $this->parent->remove($this); + } + } + if ($repair) { + $this->setValue($values); + } + + return $warnings; + } +} diff --git a/lib/vendor/sabre/vobject/lib/Property/IntegerValue.php b/lib/vendor/sabre/vobject/lib/Property/IntegerValue.php new file mode 100644 index 0000000..3ae7752 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Property/IntegerValue.php @@ -0,0 +1,76 @@ +setValue((int) $val); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return $this->value; + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'INTEGER'; + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + return [(int) $this->getValue()]; + } + + /** + * Hydrate data from a XML subtree, as it would appear in a xCard or xCal + * object. + */ + public function setXmlValue(array $value) + { + $value = array_map('intval', $value); + parent::setXmlValue($value); + } +} diff --git a/lib/vendor/sabre/vobject/lib/Property/Text.php b/lib/vendor/sabre/vobject/lib/Property/Text.php new file mode 100644 index 0000000..16d2c07 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Property/Text.php @@ -0,0 +1,392 @@ + 5, + 'ADR' => 7, + ]; + + /** + * Creates the property. + * + * You can specify the parameters either in key=>value syntax, in which case + * parameters will automatically be created, or you can just pass a list of + * Parameter objects. + * + * @param Component $root The root document + * @param string $name + * @param string|array|null $value + * @param array $parameters List of parameters + * @param string $group The vcard property group + */ + public function __construct(Component $root, $name, $value = null, array $parameters = [], $group = null) + { + // There's two types of multi-valued text properties: + // 1. multivalue properties. + // 2. structured value properties + // + // The former is always separated by a comma, the latter by semi-colon. + if (in_array($name, $this->structuredValues)) { + $this->delimiter = ';'; + } + + parent::__construct($root, $name, $value, $parameters, $group); + } + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + * + * @param string $val + */ + public function setRawMimeDirValue($val) + { + $this->setValue(MimeDir::unescapeValue($val, $this->delimiter)); + } + + /** + * Sets the value as a quoted-printable encoded string. + * + * @param string $val + */ + public function setQuotedPrintableValue($val) + { + $val = quoted_printable_decode($val); + + // Quoted printable only appears in vCard 2.1, and the only character + // that may be escaped there is ;. So we are simply splitting on just + // that. + // + // We also don't have to unescape \\, so all we need to look for is a ; + // that's not preceded with a \. + $regex = '# (?setValue($matches); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + $val = $this->getParts(); + + if (isset($this->minimumPropertyValues[$this->name])) { + $val = array_pad($val, $this->minimumPropertyValues[$this->name], ''); + } + + foreach ($val as &$item) { + if (!is_array($item)) { + $item = [$item]; + } + + foreach ($item as &$subItem) { + if (!is_null($subItem)) { + $subItem = strtr( + $subItem, + [ + '\\' => '\\\\', + ';' => '\;', + ',' => '\,', + "\n" => '\n', + "\r" => '', + ] + ); + } + } + $item = implode(',', $item); + } + + return implode($this->delimiter, $val); + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + // Structured text values should always be returned as a single + // array-item. Multi-value text should be returned as multiple items in + // the top-array. + if (in_array($this->name, $this->structuredValues)) { + return [$this->getParts()]; + } + + return $this->getParts(); + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'TEXT'; + } + + /** + * Turns the object back into a serialized blob. + * + * @return string + */ + public function serialize() + { + // We need to kick in a special type of encoding, if it's a 2.1 vcard. + if (Document::VCARD21 !== $this->root->getDocumentType()) { + return parent::serialize(); + } + + $val = $this->getParts(); + + if (isset($this->minimumPropertyValues[$this->name])) { + $val = \array_pad($val, $this->minimumPropertyValues[$this->name], ''); + } + + // Imploding multiple parts into a single value, and splitting the + // values with ;. + if (\count($val) > 1) { + foreach ($val as $k => $v) { + $val[$k] = \str_replace(';', '\;', $v); + } + $val = \implode(';', $val); + } else { + $val = $val[0]; + } + + $str = $this->name; + if ($this->group) { + $str = $this->group.'.'.$this->name; + } + foreach ($this->parameters as $param) { + if ('QUOTED-PRINTABLE' === $param->getValue()) { + continue; + } + $str .= ';'.$param->serialize(); + } + + // If the resulting value contains a \n, we must encode it as + // quoted-printable. + if (false !== \strpos($val, "\n")) { + $str .= ';ENCODING=QUOTED-PRINTABLE:'; + $lastLine = $str; + $out = null; + + // The PHP built-in quoted-printable-encode does not correctly + // encode newlines for us. Specifically, the \r\n sequence must in + // vcards be encoded as =0D=OA and we must insert soft-newlines + // every 75 bytes. + for ($ii = 0; $ii < \strlen($val); ++$ii) { + $ord = \ord($val[$ii]); + // These characters are encoded as themselves. + if ($ord >= 32 && $ord <= 126) { + $lastLine .= $val[$ii]; + } else { + $lastLine .= '='.\strtoupper(\bin2hex($val[$ii])); + } + if (\strlen($lastLine) >= 75) { + // Soft line break + $out .= $lastLine."=\r\n "; + $lastLine = null; + } + } + if (!\is_null($lastLine)) { + $out .= $lastLine."\r\n"; + } + + return $out; + } else { + $str .= ':'.$val; + + $str = \preg_replace( + '/( + (?:^.)? # 1 additional byte in first line because of missing single space (see next line) + .{1,74} # max 75 bytes per line (1 byte is used for a single space added after every CRLF) + (?![\x80-\xbf]) # prevent splitting multibyte characters + )/x', + "$1\r\n ", + $str + ); + + // remove single space after last CRLF + return \substr($str, 0, -1); + } + } + + /** + * This method serializes only the value of a property. This is used to + * create xCard or xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + protected function xmlSerializeValue(Xml\Writer $writer) + { + $values = $this->getParts(); + + $map = function ($items) use ($values, $writer) { + foreach ($items as $i => $item) { + $writer->writeElement( + $item, + !empty($values[$i]) ? $values[$i] : null + ); + } + }; + + switch ($this->name) { + // Special-casing the REQUEST-STATUS property. + // + // See: + // http://tools.ietf.org/html/rfc6321#section-3.4.1.3 + case 'REQUEST-STATUS': + $writer->writeElement('code', $values[0]); + $writer->writeElement('description', $values[1]); + + if (isset($values[2])) { + $writer->writeElement('data', $values[2]); + } + break; + + case 'N': + $map([ + 'surname', + 'given', + 'additional', + 'prefix', + 'suffix', + ]); + break; + + case 'GENDER': + $map([ + 'sex', + 'text', + ]); + break; + + case 'ADR': + $map([ + 'pobox', + 'ext', + 'street', + 'locality', + 'region', + 'code', + 'country', + ]); + break; + + case 'CLIENTPIDMAP': + $map([ + 'sourceid', + 'uri', + ]); + break; + + default: + parent::xmlSerializeValue($writer); + } + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * - Node::REPAIR - If something is broken, and automatic repair may + * be attempted. + * + * An array is returned with warnings. + * + * Every item in the array has the following properties: + * * level - (number between 1 and 3 with severity information) + * * message - (human readable message) + * * node - (reference to the offending node) + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $warnings = parent::validate($options); + + if (isset($this->minimumPropertyValues[$this->name])) { + $minimum = $this->minimumPropertyValues[$this->name]; + $parts = $this->getParts(); + if (count($parts) < $minimum) { + $warnings[] = [ + 'level' => $options & self::REPAIR ? 1 : 3, + 'message' => 'The '.$this->name.' property must have at least '.$minimum.' values. It only has '.count($parts), + 'node' => $this, + ]; + if ($options & self::REPAIR) { + $parts = array_pad($parts, $minimum, ''); + $this->setParts($parts); + } + } + } + + return $warnings; + } +} diff --git a/lib/vendor/sabre/vobject/lib/Property/Time.php b/lib/vendor/sabre/vobject/lib/Property/Time.php new file mode 100644 index 0000000..1b81609 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Property/Time.php @@ -0,0 +1,131 @@ +setValue(reset($value)); + } else { + $this->setValue($value); + } + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + $parts = DateTimeParser::parseVCardTime($this->getValue()); + $timeStr = ''; + + // Hour + if (!is_null($parts['hour'])) { + $timeStr .= $parts['hour']; + + if (!is_null($parts['minute'])) { + $timeStr .= ':'; + } + } else { + // We know either minute or second _must_ be set, so we insert a + // dash for an empty value. + $timeStr .= '-'; + } + + // Minute + if (!is_null($parts['minute'])) { + $timeStr .= $parts['minute']; + + if (!is_null($parts['second'])) { + $timeStr .= ':'; + } + } else { + if (isset($parts['second'])) { + // Dash for empty minute + $timeStr .= '-'; + } + } + + // Second + if (!is_null($parts['second'])) { + $timeStr .= $parts['second']; + } + + // Timezone + if (!is_null($parts['timezone'])) { + if ('Z' === $parts['timezone']) { + $timeStr .= 'Z'; + } else { + $timeStr .= + preg_replace('/([0-9]{2})([0-9]{2})$/', '$1:$2', $parts['timezone']); + } + } + + return [$timeStr]; + } + + /** + * Hydrate data from a XML subtree, as it would appear in a xCard or xCal + * object. + */ + public function setXmlValue(array $value) + { + $value = array_map( + function ($value) { + return str_replace(':', '', $value); + }, + $value + ); + parent::setXmlValue($value); + } +} diff --git a/lib/vendor/sabre/vobject/lib/Property/Unknown.php b/lib/vendor/sabre/vobject/lib/Property/Unknown.php new file mode 100644 index 0000000..6f404c2 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Property/Unknown.php @@ -0,0 +1,41 @@ +getRawMimeDirValue()]; + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'UNKNOWN'; + } +} diff --git a/lib/vendor/sabre/vobject/lib/Property/Uri.php b/lib/vendor/sabre/vobject/lib/Property/Uri.php new file mode 100644 index 0000000..1ad1fb1 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Property/Uri.php @@ -0,0 +1,116 @@ +name, ['URL', 'PHOTO'])) { + // If we are encoding a URI value, and this URI value has no + // VALUE=URI parameter, we add it anyway. + // + // This is not required by any spec, but both Apple iCal and Apple + // AddressBook (at least in version 10.8) will trip over this if + // this is not set, and so it improves compatibility. + // + // See Issue #227 and #235 + $parameters['VALUE'] = new Parameter($this->root, 'VALUE', 'URI'); + } + + return $parameters; + } + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + * + * @param string $val + */ + public function setRawMimeDirValue($val) + { + // Normally we don't need to do any type of unescaping for these + // properties, however.. we've noticed that Google Contacts + // specifically escapes the colon (:) with a backslash. While I have + // no clue why they thought that was a good idea, I'm unescaping it + // anyway. + // + // Good thing backslashes are not allowed in urls. Makes it easy to + // assume that a backslash is always intended as an escape character. + if ('URL' === $this->name) { + $regex = '# (?: (\\\\ (?: \\\\ | : ) ) ) #x'; + $matches = preg_split($regex, $val, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + $newVal = ''; + foreach ($matches as $match) { + switch ($match) { + case '\:': + $newVal .= ':'; + break; + default: + $newVal .= $match; + break; + } + } + $this->value = $newVal; + } else { + $this->value = strtr($val, ['\,' => ',']); + } + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + if (is_array($this->value)) { + $value = $this->value[0]; + } else { + $value = $this->value; + } + + return strtr($value, [',' => '\,']); + } +} diff --git a/lib/vendor/sabre/vobject/lib/Property/UtcOffset.php b/lib/vendor/sabre/vobject/lib/Property/UtcOffset.php new file mode 100644 index 0000000..04b8844 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Property/UtcOffset.php @@ -0,0 +1,70 @@ +value = $dt->format('Ymd'); + } +} diff --git a/lib/vendor/sabre/vobject/lib/Property/VCard/DateAndOrTime.php b/lib/vendor/sabre/vobject/lib/Property/VCard/DateAndOrTime.php new file mode 100644 index 0000000..7bf79c4 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Property/VCard/DateAndOrTime.php @@ -0,0 +1,367 @@ + 1) { + throw new \InvalidArgumentException('Only one value allowed'); + } + if (isset($parts[0]) && $parts[0] instanceof DateTimeInterface) { + $this->setDateTime($parts[0]); + } else { + parent::setParts($parts); + } + } + + /** + * Updates the current value. + * + * This may be either a single, or multiple strings in an array. + * + * Instead of strings, you may also use DateTimeInterface here. + * + * @param string|array|DateTimeInterface $value + */ + public function setValue($value) + { + if ($value instanceof DateTimeInterface) { + $this->setDateTime($value); + } else { + parent::setValue($value); + } + } + + /** + * Sets the property as a DateTime object. + */ + public function setDateTime(DateTimeInterface $dt) + { + $tz = $dt->getTimeZone(); + $isUtc = in_array($tz->getName(), ['UTC', 'GMT', 'Z']); + + if ($isUtc) { + $value = $dt->format('Ymd\\THis\\Z'); + } else { + // Calculating the offset. + $value = $dt->format('Ymd\\THisO'); + } + + $this->value = $value; + } + + /** + * Returns a date-time value. + * + * Note that if this property contained more than 1 date-time, only the + * first will be returned. To get an array with multiple values, call + * getDateTimes. + * + * If no time was specified, we will always use midnight (in the default + * timezone) as the time. + * + * If parts of the date were omitted, such as the year, we will grab the + * current values for those. So at the time of writing, if the year was + * omitted, we would have filled in 2014. + * + * @return DateTimeImmutable + */ + public function getDateTime() + { + $now = new DateTime(); + + $tzFormat = 0 === $now->getTimezone()->getOffset($now) ? '\\Z' : 'O'; + $nowParts = DateTimeParser::parseVCardDateTime($now->format('Ymd\\This'.$tzFormat)); + + $dateParts = DateTimeParser::parseVCardDateTime($this->getValue()); + + // This sets all the missing parts to the current date/time. + // So if the year was missing for a birthday, we're making it 'this + // year'. + foreach ($dateParts as $k => $v) { + if (is_null($v)) { + $dateParts[$k] = $nowParts[$k]; + } + } + + return new DateTimeImmutable("$dateParts[year]-$dateParts[month]-$dateParts[date] $dateParts[hour]:$dateParts[minute]:$dateParts[second] $dateParts[timezone]"); + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + $parts = DateTimeParser::parseVCardDateTime($this->getValue()); + + $dateStr = ''; + + // Year + if (!is_null($parts['year'])) { + $dateStr .= $parts['year']; + + if (!is_null($parts['month'])) { + // If a year and a month is set, we need to insert a separator + // dash. + $dateStr .= '-'; + } + } else { + if (!is_null($parts['month']) || !is_null($parts['date'])) { + // Inserting two dashes + $dateStr .= '--'; + } + } + + // Month + if (!is_null($parts['month'])) { + $dateStr .= $parts['month']; + + if (isset($parts['date'])) { + // If month and date are set, we need the separator dash. + $dateStr .= '-'; + } + } elseif (isset($parts['date'])) { + // If the month is empty, and a date is set, we need a 'empty + // dash' + $dateStr .= '-'; + } + + // Date + if (!is_null($parts['date'])) { + $dateStr .= $parts['date']; + } + + // Early exit if we don't have a time string. + if (is_null($parts['hour']) && is_null($parts['minute']) && is_null($parts['second'])) { + return [$dateStr]; + } + + $dateStr .= 'T'; + + // Hour + if (!is_null($parts['hour'])) { + $dateStr .= $parts['hour']; + + if (!is_null($parts['minute'])) { + $dateStr .= ':'; + } + } else { + // We know either minute or second _must_ be set, so we insert a + // dash for an empty value. + $dateStr .= '-'; + } + + // Minute + if (!is_null($parts['minute'])) { + $dateStr .= $parts['minute']; + + if (!is_null($parts['second'])) { + $dateStr .= ':'; + } + } elseif (isset($parts['second'])) { + // Dash for empty minute + $dateStr .= '-'; + } + + // Second + if (!is_null($parts['second'])) { + $dateStr .= $parts['second']; + } + + // Timezone + if (!is_null($parts['timezone'])) { + $dateStr .= $parts['timezone']; + } + + return [$dateStr]; + } + + /** + * This method serializes only the value of a property. This is used to + * create xCard or xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + protected function xmlSerializeValue(Xml\Writer $writer) + { + $valueType = strtolower($this->getValueType()); + $parts = DateTimeParser::parseVCardDateAndOrTime($this->getValue()); + $value = ''; + + // $d = defined + $d = function ($part) use ($parts) { + return !is_null($parts[$part]); + }; + + // $r = read + $r = function ($part) use ($parts) { + return $parts[$part]; + }; + + // From the Relax NG Schema. + // + // # 4.3.1 + // value-date = element date { + // xsd:string { pattern = "\d{8}|\d{4}-\d\d|--\d\d(\d\d)?|---\d\d" } + // } + if (($d('year') || $d('month') || $d('date')) + && (!$d('hour') && !$d('minute') && !$d('second') && !$d('timezone'))) { + if ($d('year') && $d('month') && $d('date')) { + $value .= $r('year').$r('month').$r('date'); + } elseif ($d('year') && $d('month') && !$d('date')) { + $value .= $r('year').'-'.$r('month'); + } elseif (!$d('year') && $d('month')) { + $value .= '--'.$r('month').$r('date'); + } elseif (!$d('year') && !$d('month') && $d('date')) { + $value .= '---'.$r('date'); + } + + // # 4.3.2 + // value-time = element time { + // xsd:string { pattern = "(\d\d(\d\d(\d\d)?)?|-\d\d(\d\d?)|--\d\d)" + // ~ "(Z|[+\-]\d\d(\d\d)?)?" } + // } + } elseif ((!$d('year') && !$d('month') && !$d('date')) + && ($d('hour') || $d('minute') || $d('second'))) { + if ($d('hour')) { + $value .= $r('hour').$r('minute').$r('second'); + } elseif ($d('minute')) { + $value .= '-'.$r('minute').$r('second'); + } elseif ($d('second')) { + $value .= '--'.$r('second'); + } + + $value .= $r('timezone'); + + // # 4.3.3 + // value-date-time = element date-time { + // xsd:string { pattern = "(\d{8}|--\d{4}|---\d\d)T\d\d(\d\d(\d\d)?)?" + // ~ "(Z|[+\-]\d\d(\d\d)?)?" } + // } + } elseif ($d('date') && $d('hour')) { + if ($d('year') && $d('month') && $d('date')) { + $value .= $r('year').$r('month').$r('date'); + } elseif (!$d('year') && $d('month') && $d('date')) { + $value .= '--'.$r('month').$r('date'); + } elseif (!$d('year') && !$d('month') && $d('date')) { + $value .= '---'.$r('date'); + } + + $value .= 'T'.$r('hour').$r('minute').$r('second'). + $r('timezone'); + } + + $writer->writeElement($valueType, $value); + } + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + * + * @param string $val + */ + public function setRawMimeDirValue($val) + { + $this->setValue($val); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return implode($this->delimiter, $this->getParts()); + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on) + * 2 - An inconsequential issue + * 3 - A severe issue. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $messages = parent::validate($options); + $value = $this->getValue(); + + try { + DateTimeParser::parseVCardDateTime($value); + } catch (InvalidDataException $e) { + $messages[] = [ + 'level' => 3, + 'message' => 'The supplied value ('.$value.') is not a correct DATE-AND-OR-TIME property', + 'node' => $this, + ]; + } + + return $messages; + } +} diff --git a/lib/vendor/sabre/vobject/lib/Property/VCard/DateTime.php b/lib/vendor/sabre/vobject/lib/Property/VCard/DateTime.php new file mode 100644 index 0000000..49c1f35 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Property/VCard/DateTime.php @@ -0,0 +1,28 @@ +setValue($val); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return $this->getValue(); + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'LANGUAGE-TAG'; + } +} diff --git a/lib/vendor/sabre/vobject/lib/Property/VCard/PhoneNumber.php b/lib/vendor/sabre/vobject/lib/Property/VCard/PhoneNumber.php new file mode 100644 index 0000000..b714ffd --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Property/VCard/PhoneNumber.php @@ -0,0 +1,30 @@ + + */ +class PhoneNumber extends Property\Text +{ + protected $structuredValues = []; + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'PHONE-NUMBER'; + } +} diff --git a/lib/vendor/sabre/vobject/lib/Property/VCard/TimeStamp.php b/lib/vendor/sabre/vobject/lib/Property/VCard/TimeStamp.php new file mode 100644 index 0000000..da6ea3d --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Property/VCard/TimeStamp.php @@ -0,0 +1,81 @@ +getValue()); + + $dateStr = + $parts['year'].'-'. + $parts['month'].'-'. + $parts['date'].'T'. + $parts['hour'].':'. + $parts['minute'].':'. + $parts['second']; + + // Timezone + if (!is_null($parts['timezone'])) { + $dateStr .= $parts['timezone']; + } + + return [$dateStr]; + } + + /** + * This method serializes only the value of a property. This is used to + * create xCard or xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + protected function xmlSerializeValue(Xml\Writer $writer) + { + // xCard is the only XML and JSON format that has the same date and time + // format than vCard. + $valueType = strtolower($this->getValueType()); + $writer->writeElement($valueType, $this->getValue()); + } +} diff --git a/lib/vendor/sabre/vobject/lib/Reader.php b/lib/vendor/sabre/vobject/lib/Reader.php new file mode 100644 index 0000000..055d546 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Reader.php @@ -0,0 +1,95 @@ +setCharset($charset); + $result = $parser->parse($data, $options); + + return $result; + } + + /** + * Parses a jCard or jCal object, and returns the top component. + * + * The options argument is a bitfield. Pass any of the OPTIONS constant to + * alter the parsers' behaviour. + * + * You can either a string, a readable stream, or an array for its input. + * Specifying the array is useful if json_decode was already called on the + * input. + * + * @param string|resource|array $data + * @param int $options + * + * @return Document + */ + public static function readJson($data, $options = 0) + { + $parser = new Parser\Json(); + $result = $parser->parse($data, $options); + + return $result; + } + + /** + * Parses a xCard or xCal object, and returns the top component. + * + * The options argument is a bitfield. Pass any of the OPTIONS constant to + * alter the parsers' behaviour. + * + * You can either supply a string, or a readable stream for input. + * + * @param string|resource $data + * @param int $options + * + * @return Document + */ + public static function readXML($data, $options = 0) + { + $parser = new Parser\XML(); + $result = $parser->parse($data, $options); + + return $result; + } +} diff --git a/lib/vendor/sabre/vobject/lib/Recur/EventIterator.php b/lib/vendor/sabre/vobject/lib/Recur/EventIterator.php new file mode 100644 index 0000000..212c147 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Recur/EventIterator.php @@ -0,0 +1,501 @@ +timeZone = $timeZone; + + if (is_array($input)) { + $events = $input; + } elseif ($input instanceof VEvent) { + // Single instance mode. + $events = [$input]; + } else { + // Calendar + UID mode. + $uid = (string) $uid; + if (!$uid) { + throw new InvalidArgumentException('The UID argument is required when a VCALENDAR is passed to this constructor'); + } + if (!isset($input->VEVENT)) { + throw new InvalidArgumentException('No events found in this calendar'); + } + $events = $input->getByUID($uid); + } + + foreach ($events as $vevent) { + if (!isset($vevent->{'RECURRENCE-ID'})) { + $this->masterEvent = $vevent; + } else { + $this->exceptions[ + $vevent->{'RECURRENCE-ID'}->getDateTime($this->timeZone)->getTimeStamp() + ] = true; + $this->overriddenEvents[] = $vevent; + } + } + + if (!$this->masterEvent) { + // No base event was found. CalDAV does allow cases where only + // overridden instances are stored. + // + // In this particular case, we're just going to grab the first + // event and use that instead. This may not always give the + // desired result. + if (!count($this->overriddenEvents)) { + throw new InvalidArgumentException('This VCALENDAR did not have an event with UID: '.$uid); + } + $this->masterEvent = array_shift($this->overriddenEvents); + } + + $this->startDate = $this->masterEvent->DTSTART->getDateTime($this->timeZone); + $this->allDay = !$this->masterEvent->DTSTART->hasTime(); + + if (isset($this->masterEvent->EXDATE)) { + foreach ($this->masterEvent->EXDATE as $exDate) { + foreach ($exDate->getDateTimes($this->timeZone) as $dt) { + $this->exceptions[$dt->getTimeStamp()] = true; + } + } + } + + if (isset($this->masterEvent->DTEND)) { + $this->eventDuration = + $this->masterEvent->DTEND->getDateTime($this->timeZone)->getTimeStamp() - + $this->startDate->getTimeStamp(); + } elseif (isset($this->masterEvent->DURATION)) { + $duration = $this->masterEvent->DURATION->getDateInterval(); + $end = clone $this->startDate; + $end = $end->add($duration); + $this->eventDuration = $end->getTimeStamp() - $this->startDate->getTimeStamp(); + } elseif ($this->allDay) { + $this->eventDuration = 3600 * 24; + } else { + $this->eventDuration = 0; + } + + if (isset($this->masterEvent->RDATE)) { + $rdateValues = []; + foreach ($this->masterEvent->RDATE as $rdate) { + $rdateValues = array_merge($rdateValues, $rdate->getParts()); + } + $this->recurIterator = new RDateIterator( + $rdateValues, + $this->startDate + ); + } elseif (isset($this->masterEvent->RRULE)) { + $this->recurIterator = new RRuleIterator( + $this->masterEvent->RRULE->getParts(), + $this->startDate + ); + } else { + $this->recurIterator = new RRuleIterator( + [ + 'FREQ' => 'DAILY', + 'COUNT' => 1, + ], + $this->startDate + ); + } + + $this->rewind(); + if (!$this->valid()) { + throw new NoInstancesException('This recurrence rule does not generate any valid instances'); + } + } + + /** + * Returns the date for the current position of the iterator. + * + * @return DateTimeImmutable + */ + #[\ReturnTypeWillChange] + public function current() + { + if ($this->currentDate) { + return clone $this->currentDate; + } + } + + /** + * This method returns the start date for the current iteration of the + * event. + * + * @return DateTimeImmutable + */ + public function getDtStart() + { + if ($this->currentDate) { + return clone $this->currentDate; + } + } + + /** + * This method returns the end date for the current iteration of the + * event. + * + * @return DateTimeImmutable + */ + public function getDtEnd() + { + if (!$this->valid()) { + return; + } + if ($this->currentOverriddenEvent && $this->currentOverriddenEvent->DTEND) { + return $this->currentOverriddenEvent->DTEND->getDateTime($this->timeZone); + } else { + $end = clone $this->currentDate; + + return $end->modify('+'.$this->eventDuration.' seconds'); + } + } + + /** + * Returns a VEVENT for the current iterations of the event. + * + * This VEVENT will have a recurrence id, and its DTSTART and DTEND + * altered. + * + * @return VEvent + */ + public function getEventObject() + { + if ($this->currentOverriddenEvent) { + return $this->currentOverriddenEvent; + } + + $event = clone $this->masterEvent; + + // Ignoring the following block, because PHPUnit's code coverage + // ignores most of these lines, and this messes with our stats. + // + // @codeCoverageIgnoreStart + unset( + $event->RRULE, + $event->EXDATE, + $event->RDATE, + $event->EXRULE, + $event->{'RECURRENCE-ID'} + ); + // @codeCoverageIgnoreEnd + + $event->DTSTART->setDateTime($this->getDtStart(), $event->DTSTART->isFloating()); + if (isset($event->DTEND)) { + $event->DTEND->setDateTime($this->getDtEnd(), $event->DTEND->isFloating()); + } + $recurid = clone $event->DTSTART; + $recurid->name = 'RECURRENCE-ID'; + $event->add($recurid); + + return $event; + } + + /** + * Returns the current position of the iterator. + * + * This is for us simply a 0-based index. + * + * @return int + */ + #[\ReturnTypeWillChange] + public function key() + { + // The counter is always 1 ahead. + return $this->counter - 1; + } + + /** + * This is called after next, to see if the iterator is still at a valid + * position, or if it's at the end. + * + * @return bool + */ + #[\ReturnTypeWillChange] + public function valid() + { + if ($this->counter > Settings::$maxRecurrences && -1 !== Settings::$maxRecurrences) { + throw new MaxInstancesExceededException('Recurring events are only allowed to generate '.Settings::$maxRecurrences); + } + + return (bool) $this->currentDate; + } + + /** + * Sets the iterator back to the starting point. + * + * @return void + */ + #[\ReturnTypeWillChange] + public function rewind() + { + $this->recurIterator->rewind(); + // re-creating overridden event index. + $index = []; + foreach ($this->overriddenEvents as $key => $event) { + $stamp = $event->DTSTART->getDateTime($this->timeZone)->getTimeStamp(); + $index[$stamp][] = $key; + } + krsort($index); + $this->counter = 0; + $this->overriddenEventsIndex = $index; + $this->currentOverriddenEvent = null; + + $this->nextDate = null; + $this->currentDate = clone $this->startDate; + + $this->next(); + } + + /** + * Advances the iterator with one step. + * + * @return void + */ + #[\ReturnTypeWillChange] + public function next() + { + $this->currentOverriddenEvent = null; + ++$this->counter; + if ($this->nextDate) { + // We had a stored value. + $nextDate = $this->nextDate; + $this->nextDate = null; + } else { + // We need to ask rruleparser for the next date. + // We need to do this until we find a date that's not in the + // exception list. + do { + if (!$this->recurIterator->valid()) { + $nextDate = null; + break; + } + $nextDate = $this->recurIterator->current(); + $this->recurIterator->next(); + } while (isset($this->exceptions[$nextDate->getTimeStamp()])); + } + + // $nextDate now contains what rrule thinks is the next one, but an + // overridden event may cut ahead. + if ($this->overriddenEventsIndex) { + $offsets = end($this->overriddenEventsIndex); + $timestamp = key($this->overriddenEventsIndex); + $offset = end($offsets); + if (!$nextDate || $timestamp < $nextDate->getTimeStamp()) { + // Overridden event comes first. + $this->currentOverriddenEvent = $this->overriddenEvents[$offset]; + + // Putting the rrule next date aside. + $this->nextDate = $nextDate; + $this->currentDate = $this->currentOverriddenEvent->DTSTART->getDateTime($this->timeZone); + + // Ensuring that this item will only be used once. + array_pop($this->overriddenEventsIndex[$timestamp]); + if (!$this->overriddenEventsIndex[$timestamp]) { + array_pop($this->overriddenEventsIndex); + } + + // Exit point! + return; + } + } + + $this->currentDate = $nextDate; + } + + /** + * Quickly jump to a date in the future. + */ + public function fastForward(DateTimeInterface $dateTime) + { + while ($this->valid() && $this->getDtEnd() <= $dateTime) { + $this->next(); + } + } + + /** + * Returns true if this recurring event never ends. + * + * @return bool + */ + public function isInfinite() + { + return $this->recurIterator->isInfinite(); + } + + /** + * RRULE parser. + * + * @var RRuleIterator + */ + protected $recurIterator; + + /** + * The duration, in seconds, of the master event. + * + * We use this to calculate the DTEND for subsequent events. + */ + protected $eventDuration; + + /** + * A reference to the main (master) event. + * + * @var VEVENT + */ + protected $masterEvent; + + /** + * List of overridden events. + * + * @var array + */ + protected $overriddenEvents = []; + + /** + * Overridden event index. + * + * Key is timestamp, value is the list of indexes of the item in the $overriddenEvent + * property. + * + * @var array + */ + protected $overriddenEventsIndex; + + /** + * A list of recurrence-id's that are either part of EXDATE, or are + * overridden. + * + * @var array + */ + protected $exceptions = []; + + /** + * Internal event counter. + * + * @var int + */ + protected $counter; + + /** + * The very start of the iteration process. + * + * @var DateTimeImmutable + */ + protected $startDate; + + /** + * Where we are currently in the iteration process. + * + * @var DateTimeImmutable + */ + protected $currentDate; + + /** + * The next date from the rrule parser. + * + * Sometimes we need to temporary store the next date, because an + * overridden event came before. + * + * @var DateTimeImmutable + */ + protected $nextDate; + + /** + * The event that overwrites the current iteration. + * + * @var VEVENT + */ + protected $currentOverriddenEvent; +} diff --git a/lib/vendor/sabre/vobject/lib/Recur/MaxInstancesExceededException.php b/lib/vendor/sabre/vobject/lib/Recur/MaxInstancesExceededException.php new file mode 100644 index 0000000..cb08358 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Recur/MaxInstancesExceededException.php @@ -0,0 +1,17 @@ +startDate = $start; + $this->parseRDate($rrule); + $this->currentDate = clone $this->startDate; + } + + /* Implementation of the Iterator interface {{{ */ + + #[\ReturnTypeWillChange] + public function current() + { + if (!$this->valid()) { + return; + } + + return clone $this->currentDate; + } + + /** + * Returns the current item number. + * + * @return int + */ + #[\ReturnTypeWillChange] + public function key() + { + return $this->counter; + } + + /** + * Returns whether the current item is a valid item for the recurrence + * iterator. + * + * @return bool + */ + #[\ReturnTypeWillChange] + public function valid() + { + return $this->counter <= count($this->dates); + } + + /** + * Resets the iterator. + * + * @return void + */ + #[\ReturnTypeWillChange] + public function rewind() + { + $this->currentDate = clone $this->startDate; + $this->counter = 0; + } + + /** + * Goes on to the next iteration. + * + * @return void + */ + #[\ReturnTypeWillChange] + public function next() + { + ++$this->counter; + if (!$this->valid()) { + return; + } + + $this->currentDate = + DateTimeParser::parse( + $this->dates[$this->counter - 1], + $this->startDate->getTimezone() + ); + } + + /* End of Iterator implementation }}} */ + + /** + * Returns true if this recurring event never ends. + * + * @return bool + */ + public function isInfinite() + { + return false; + } + + /** + * This method allows you to quickly go to the next occurrence after the + * specified date. + */ + public function fastForward(DateTimeInterface $dt) + { + while ($this->valid() && $this->currentDate < $dt) { + $this->next(); + } + } + + /** + * The reference start date/time for the rrule. + * + * All calculations are based on this initial date. + * + * @var DateTimeInterface + */ + protected $startDate; + + /** + * The date of the current iteration. You can get this by calling + * ->current(). + * + * @var DateTimeInterface + */ + protected $currentDate; + + /** + * The current item in the list. + * + * You can get this number with the key() method. + * + * @var int + */ + protected $counter = 0; + + /* }}} */ + + /** + * This method receives a string from an RRULE property, and populates this + * class with all the values. + * + * @param string|array $rrule + */ + protected function parseRDate($rdate) + { + if (is_string($rdate)) { + $rdate = explode(',', $rdate); + } + + $this->dates = $rdate; + } + + /** + * Array with the RRULE dates. + * + * @var array + */ + protected $dates = []; +} diff --git a/lib/vendor/sabre/vobject/lib/Recur/RRuleIterator.php b/lib/vendor/sabre/vobject/lib/Recur/RRuleIterator.php new file mode 100644 index 0000000..ca53b63 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Recur/RRuleIterator.php @@ -0,0 +1,1079 @@ +startDate = $start; + $this->parseRRule($rrule); + $this->currentDate = clone $this->startDate; + } + + /* Implementation of the Iterator interface {{{ */ + + #[\ReturnTypeWillChange] + public function current() + { + if (!$this->valid()) { + return; + } + + return clone $this->currentDate; + } + + /** + * Returns the current item number. + * + * @return int + */ + #[\ReturnTypeWillChange] + public function key() + { + return $this->counter; + } + + /** + * Returns whether the current item is a valid item for the recurrence + * iterator. This will return false if we've gone beyond the UNTIL or COUNT + * statements. + * + * @return bool + */ + #[\ReturnTypeWillChange] + public function valid() + { + if (null === $this->currentDate) { + return false; + } + if (!is_null($this->count)) { + return $this->counter < $this->count; + } + + return is_null($this->until) || $this->currentDate <= $this->until; + } + + /** + * Resets the iterator. + * + * @return void + */ + #[\ReturnTypeWillChange] + public function rewind() + { + $this->currentDate = clone $this->startDate; + $this->counter = 0; + } + + /** + * Goes on to the next iteration. + * + * @return void + */ + #[\ReturnTypeWillChange] + public function next() + { + // Otherwise, we find the next event in the normal RRULE + // sequence. + switch ($this->frequency) { + case 'hourly': + $this->nextHourly(); + break; + + case 'daily': + $this->nextDaily(); + break; + + case 'weekly': + $this->nextWeekly(); + break; + + case 'monthly': + $this->nextMonthly(); + break; + + case 'yearly': + $this->nextYearly(); + break; + } + ++$this->counter; + } + + /* End of Iterator implementation }}} */ + + /** + * Returns true if this recurring event never ends. + * + * @return bool + */ + public function isInfinite() + { + return !$this->count && !$this->until; + } + + /** + * This method allows you to quickly go to the next occurrence after the + * specified date. + */ + public function fastForward(DateTimeInterface $dt) + { + while ($this->valid() && $this->currentDate < $dt) { + $this->next(); + } + } + + /** + * The reference start date/time for the rrule. + * + * All calculations are based on this initial date. + * + * @var DateTimeInterface + */ + protected $startDate; + + /** + * The date of the current iteration. You can get this by calling + * ->current(). + * + * @var DateTimeInterface + */ + protected $currentDate; + + /** + * The number of hours that the next occurrence of an event + * jumped forward, usually because summer time started and + * the requested time-of-day like 0230 did not exist on that + * day. And so the event was scheduled 1 hour later at 0330. + */ + protected $hourJump = 0; + + /** + * Frequency is one of: secondly, minutely, hourly, daily, weekly, monthly, + * yearly. + * + * @var string + */ + protected $frequency; + + /** + * The number of recurrences, or 'null' if infinitely recurring. + * + * @var int + */ + protected $count; + + /** + * The interval. + * + * If for example frequency is set to daily, interval = 2 would mean every + * 2 days. + * + * @var int + */ + protected $interval = 1; + + /** + * The last instance of this recurrence, inclusively. + * + * @var DateTimeInterface|null + */ + protected $until; + + /** + * Which seconds to recur. + * + * This is an array of integers (between 0 and 60) + * + * @var array + */ + protected $bySecond; + + /** + * Which minutes to recur. + * + * This is an array of integers (between 0 and 59) + * + * @var array + */ + protected $byMinute; + + /** + * Which hours to recur. + * + * This is an array of integers (between 0 and 23) + * + * @var array + */ + protected $byHour; + + /** + * The current item in the list. + * + * You can get this number with the key() method. + * + * @var int + */ + protected $counter = 0; + + /** + * Which weekdays to recur. + * + * This is an array of weekdays + * + * This may also be preceded by a positive or negative integer. If present, + * this indicates the nth occurrence of a specific day within the monthly or + * yearly rrule. For instance, -2TU indicates the second-last tuesday of + * the month, or year. + * + * @var array + */ + protected $byDay; + + /** + * Which days of the month to recur. + * + * This is an array of days of the months (1-31). The value can also be + * negative. -5 for instance means the 5th last day of the month. + * + * @var array + */ + protected $byMonthDay; + + /** + * Which days of the year to recur. + * + * This is an array with days of the year (1 to 366). The values can also + * be negative. For instance, -1 will always represent the last day of the + * year. (December 31st). + * + * @var array + */ + protected $byYearDay; + + /** + * Which week numbers to recur. + * + * This is an array of integers from 1 to 53. The values can also be + * negative. -1 will always refer to the last week of the year. + * + * @var array + */ + protected $byWeekNo; + + /** + * Which months to recur. + * + * This is an array of integers from 1 to 12. + * + * @var array + */ + protected $byMonth; + + /** + * Which items in an existing st to recur. + * + * These numbers work together with an existing by* rule. It specifies + * exactly which items of the existing by-rule to filter. + * + * Valid values are 1 to 366 and -1 to -366. As an example, this can be + * used to recur the last workday of the month. + * + * This would be done by setting frequency to 'monthly', byDay to + * 'MO,TU,WE,TH,FR' and bySetPos to -1. + * + * @var array + */ + protected $bySetPos; + + /** + * When the week starts. + * + * @var string + */ + protected $weekStart = 'MO'; + + /* Functions that advance the iterator {{{ */ + + /** + * Gets the original start time of the RRULE. + * + * The value is formatted as a string with 24-hour:minute:second + */ + protected function startTime(): string + { + return $this->startDate->format('H:i:s'); + } + + /** + * Advances currentDate by the interval. + * The time is set from the original startDate. + * If the recurrence is on a day when summer time started, then the + * time on that day may have jumped forward, for example, from 0230 to 0330. + * Using the original time means that the next recurrence will be calculated + * based on the original start time and the day/week/month/year interval. + * So the start time of the next occurrence can correctly revert to 0230. + */ + protected function advanceTheDate(string $interval): void + { + $this->currentDate = $this->currentDate->modify($interval.' '.$this->startTime()); + } + + /** + * Does the processing for adjusting the time of multi-hourly events when summer time starts. + */ + protected function adjustForTimeJumpsOfHourlyEvent(DateTimeInterface $previousEventDateTime): void + { + if (0 === $this->hourJump) { + // Remember if the clock time jumped forward on the next occurrence. + // That happens if the next event time is on a day when summer time starts + // and the event time is in the non-existent hour of the day. + // For example, an event that normally starts at 02:30 will + // have to start at 03:30 on that day. + // If the interval is just 1 hour, then there is no "jumping back" to do. + // The events that day will happen, for example, at 0030 0130 0330 0430 0530... + if ($this->interval > 1) { + $expectedHourOfNextDate = ((int) $previousEventDateTime->format('G') + $this->interval) % 24; + $actualHourOfNextDate = (int) $this->currentDate->format('G'); + $this->hourJump = $actualHourOfNextDate - $expectedHourOfNextDate; + } + } else { + // The hour "jumped" for the previous occurrence, to avoid the non-existent time. + // currentDate got set ahead by (usually) 1 hour on that day. + // Adjust it back for this next occurrence. + $this->currentDate = $this->currentDate->sub(new \DateInterval('PT'.$this->hourJump.'H')); + $this->hourJump = 0; + } + } + + /** + * Does the processing for advancing the iterator for hourly frequency. + */ + protected function nextHourly() + { + $previousEventDateTime = clone $this->currentDate; + $this->currentDate = $this->currentDate->modify('+'.$this->interval.' hours'); + $this->adjustForTimeJumpsOfHourlyEvent($previousEventDateTime); + } + + /** + * Does the processing for advancing the iterator for daily frequency. + */ + protected function nextDaily() + { + if (!$this->byHour && !$this->byDay) { + $this->advanceTheDate('+'.$this->interval.' days'); + + return; + } + + $recurrenceHours = []; + if (!empty($this->byHour)) { + $recurrenceHours = $this->getHours(); + } + + $recurrenceDays = []; + if (!empty($this->byDay)) { + $recurrenceDays = $this->getDays(); + } + + $recurrenceMonths = []; + if (!empty($this->byMonth)) { + $recurrenceMonths = $this->getMonths(); + } + + do { + if ($this->byHour) { + if ('23' == $this->currentDate->format('G')) { + // to obey the interval rule + $this->currentDate = $this->currentDate->modify('+'.($this->interval - 1).' days'); + } + + $this->currentDate = $this->currentDate->modify('+1 hours'); + } else { + $this->currentDate = $this->currentDate->modify('+'.$this->interval.' days'); + } + + // Current month of the year + $currentMonth = $this->currentDate->format('n'); + + // Current day of the week + $currentDay = $this->currentDate->format('w'); + + // Current hour of the day + $currentHour = $this->currentDate->format('G'); + + if ($this->currentDate->getTimestamp() > self::dateUpperLimit) { + $this->currentDate = null; + + return; + } + } while ( + ($this->byDay && !in_array($currentDay, $recurrenceDays)) || + ($this->byHour && !in_array($currentHour, $recurrenceHours)) || + ($this->byMonth && !in_array($currentMonth, $recurrenceMonths)) + ); + } + + /** + * Does the processing for advancing the iterator for weekly frequency. + */ + protected function nextWeekly() + { + if (!$this->byHour && !$this->byDay) { + $this->advanceTheDate('+'.$this->interval.' weeks'); + + return; + } + + $recurrenceHours = []; + if ($this->byHour) { + $recurrenceHours = $this->getHours(); + } + + $recurrenceDays = []; + if ($this->byDay) { + $recurrenceDays = $this->getDays(); + } + + // First day of the week: + $firstDay = $this->dayMap[$this->weekStart]; + + do { + if ($this->byHour) { + $this->currentDate = $this->currentDate->modify('+1 hours'); + } else { + $this->advanceTheDate('+1 days'); + } + + // Current day of the week + $currentDay = (int) $this->currentDate->format('w'); + + // Current hour of the day + $currentHour = (int) $this->currentDate->format('G'); + + // We need to roll over to the next week + if ($currentDay === $firstDay && (!$this->byHour || '0' == $currentHour)) { + $this->currentDate = $this->currentDate->modify('+'.($this->interval - 1).' weeks'); + + // We need to go to the first day of this week, but only if we + // are not already on this first day of this week. + if ($this->currentDate->format('w') != $firstDay) { + $this->currentDate = $this->currentDate->modify('last '.$this->dayNames[$this->dayMap[$this->weekStart]]); + } + } + + // We have a match + } while (($this->byDay && !in_array($currentDay, $recurrenceDays)) || ($this->byHour && !in_array($currentHour, $recurrenceHours))); + } + + /** + * Does the processing for advancing the iterator for monthly frequency. + */ + protected function nextMonthly() + { + $currentDayOfMonth = $this->currentDate->format('j'); + if (!$this->byMonthDay && !$this->byDay) { + // If the current day is higher than the 28th, rollover can + // occur to the next month. We Must skip these invalid + // entries. + if ($currentDayOfMonth < 29) { + $this->advanceTheDate('+'.$this->interval.' months'); + } else { + $increase = 0; + do { + ++$increase; + $tempDate = clone $this->currentDate; + $tempDate = $tempDate->modify('+ '.($this->interval * $increase).' months '.$this->startTime()); + } while ($tempDate->format('j') != $currentDayOfMonth); + $this->currentDate = $tempDate; + } + + return; + } + + $occurrence = -1; + while (true) { + $occurrences = $this->getMonthlyOccurrences(); + + foreach ($occurrences as $occurrence) { + // The first occurrence thats higher than the current + // day of the month wins. + if ($occurrence > $currentDayOfMonth) { + break 2; + } + } + + // If we made it all the way here, it means there were no + // valid occurrences, and we need to advance to the next + // month. + // + // This line does not currently work in hhvm. Temporary workaround + // follows: + // $this->currentDate->modify('first day of this month'); + $this->currentDate = new DateTimeImmutable($this->currentDate->format('Y-m-1 H:i:s'), $this->currentDate->getTimezone()); + // end of workaround + $this->currentDate = $this->currentDate->modify('+ '.$this->interval.' months'); + + // This goes to 0 because we need to start counting at the + // beginning. + $currentDayOfMonth = 0; + + // For some reason the "until" parameter was not being used here, + // that's why the workaround of the 10000 year bug was needed at all + // let's stop it before the "until" parameter date + if ($this->until && $this->currentDate->getTimestamp() >= $this->until->getTimestamp()) { + return; + } + + // To prevent running this forever (better: until we hit the max date of DateTimeImmutable) we simply + // stop at 9999-12-31. Looks like the year 10000 problem is not solved in php .... + if ($this->currentDate->getTimestamp() > self::dateUpperLimit) { + $this->currentDate = null; + + return; + } + } + + // Set the currentDate to the year and month that we are in, and the day of the month that we have selected. + // That day could be a day when summer time starts, and if the time of the event is, for example, 0230, + // then 0230 will not be a valid time on that day. So always apply the start time from the original startDate. + // The "modify" method will set the time forward to 0330, for example, if needed. + $this->currentDate = $this->currentDate->setDate( + (int) $this->currentDate->format('Y'), + (int) $this->currentDate->format('n'), + (int) $occurrence + )->modify($this->startTime()); + } + + /** + * Does the processing for advancing the iterator for yearly frequency. + */ + protected function nextYearly() + { + $currentMonth = $this->currentDate->format('n'); + $currentYear = $this->currentDate->format('Y'); + $currentDayOfMonth = $this->currentDate->format('j'); + + // No sub-rules, so we just advance by year + if (empty($this->byMonth)) { + // Unless it was a leap day! + if (2 == $currentMonth && 29 == $currentDayOfMonth) { + $counter = 0; + do { + ++$counter; + // Here we increase the year count by the interval, until + // we hit a date that's also in a leap year. + // + // We could just find the next interval that's dividable by + // 4, but that would ignore the rule that there's no leap + // year every year that's dividable by a 100, but not by + // 400. (1800, 1900, 2100). So we just rely on the datetime + // functions instead. + $nextDate = clone $this->currentDate; + $nextDate = $nextDate->modify('+ '.($this->interval * $counter).' years'); + } while (2 != $nextDate->format('n')); + + $this->currentDate = $nextDate; + + return; + } + + if (null !== $this->byWeekNo) { // byWeekNo is an array with values from -53 to -1, or 1 to 53 + $dayOffsets = []; + if ($this->byDay) { + foreach ($this->byDay as $byDay) { + $dayOffsets[] = $this->dayMap[$byDay]; + } + } else { // default is Monday + $dayOffsets[] = 1; + } + + $currentYear = $this->currentDate->format('Y'); + + while (true) { + $checkDates = []; + + // loop through all WeekNo and Days to check all the combinations + foreach ($this->byWeekNo as $byWeekNo) { + foreach ($dayOffsets as $dayOffset) { + $date = clone $this->currentDate; + $date = $date->setISODate($currentYear, $byWeekNo, $dayOffset); + + if ($date > $this->currentDate) { + $checkDates[] = $date; + } + } + } + + if (count($checkDates) > 0) { + $this->currentDate = min($checkDates); + + return; + } + + // if there is no date found, check the next year + $currentYear += $this->interval; + } + } + + if (null !== $this->byYearDay) { // byYearDay is an array with values from -366 to -1, or 1 to 366 + $dayOffsets = []; + if ($this->byDay) { + foreach ($this->byDay as $byDay) { + $dayOffsets[] = $this->dayMap[$byDay]; + } + } else { // default is Monday-Sunday + $dayOffsets = [1, 2, 3, 4, 5, 6, 7]; + } + + $currentYear = $this->currentDate->format('Y'); + + while (true) { + $checkDates = []; + + // loop through all YearDay and Days to check all the combinations + foreach ($this->byYearDay as $byYearDay) { + $date = clone $this->currentDate; + if ($byYearDay > 0) { + $date = $date->setDate($currentYear, 1, 1); + $date = $date->add(new \DateInterval('P'.($byYearDay - 1).'D')); + } else { + $date = $date->setDate($currentYear, 12, 31); + $date = $date->sub(new \DateInterval('P'.abs($byYearDay + 1).'D')); + } + + if ($date > $this->currentDate && in_array($date->format('N'), $dayOffsets)) { + $checkDates[] = $date; + } + } + + if (count($checkDates) > 0) { + $this->currentDate = min($checkDates); + + return; + } + + // if there is no date found, check the next year + $currentYear += $this->interval; + } + } + + // The easiest form + $this->advanceTheDate('+'.$this->interval.' years'); + + return; + } + + $currentMonth = $this->currentDate->format('n'); + $currentYear = $this->currentDate->format('Y'); + $currentDayOfMonth = $this->currentDate->format('j'); + + $advancedToNewMonth = false; + + // If we got a byDay or getMonthDay filter, we must first expand + // further. + if ($this->byDay || $this->byMonthDay) { + $occurrence = -1; + while (true) { + $occurrences = $this->getMonthlyOccurrences(); + + foreach ($occurrences as $occurrence) { + // The first occurrence that's higher than the current + // day of the month wins. + // If we advanced to the next month or year, the first + // occurrence is always correct. + if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) { + // only consider byMonth matches, + // otherwise, we don't follow RRule correctly + if (in_array($currentMonth, $this->byMonth)) { + break 2; + } + } + } + + // If we made it here, it means we need to advance to + // the next month or year. + $currentDayOfMonth = 1; + $advancedToNewMonth = true; + do { + ++$currentMonth; + if ($currentMonth > 12) { + $currentYear += $this->interval; + $currentMonth = 1; + } + } while (!in_array($currentMonth, $this->byMonth)); + + $this->currentDate = $this->currentDate->setDate( + (int) $currentYear, + (int) $currentMonth, + (int) $currentDayOfMonth + ); + + // To prevent running this forever (better: until we hit the max date of DateTimeImmutable) we simply + // stop at 9999-12-31. Looks like the year 10000 problem is not solved in php .... + if ($this->currentDate->getTimestamp() > self::dateUpperLimit) { + $this->currentDate = null; + + return; + } + } + + // If we made it here, it means we got a valid occurrence + $this->currentDate = $this->currentDate->setDate( + (int) $currentYear, + (int) $currentMonth, + (int) $occurrence + )->modify($this->startTime()); + + return; + } else { + // These are the 'byMonth' rules, if there are no byDay or + // byMonthDay sub-rules. + do { + ++$currentMonth; + if ($currentMonth > 12) { + $currentYear += $this->interval; + $currentMonth = 1; + } + } while (!in_array($currentMonth, $this->byMonth)); + $this->currentDate = $this->currentDate->setDate( + (int) $currentYear, + (int) $currentMonth, + (int) $currentDayOfMonth + )->modify($this->startTime()); + + return; + } + } + + /* }}} */ + + /** + * This method receives a string from an RRULE property, and populates this + * class with all the values. + * + * @param string|array $rrule + */ + protected function parseRRule($rrule) + { + if (is_string($rrule)) { + $rrule = Property\ICalendar\Recur::stringToArray($rrule); + } + + foreach ($rrule as $key => $value) { + $key = strtoupper($key); + switch ($key) { + case 'FREQ': + $value = strtolower($value); + if (!in_array( + $value, + ['secondly', 'minutely', 'hourly', 'daily', 'weekly', 'monthly', 'yearly'] + )) { + throw new InvalidDataException('Unknown value for FREQ='.strtoupper($value)); + } + $this->frequency = $value; + break; + + case 'UNTIL': + $this->until = DateTimeParser::parse($value, $this->startDate->getTimezone()); + + // In some cases events are generated with an UNTIL= + // parameter before the actual start of the event. + // + // Not sure why this is happening. We assume that the + // intention was that the event only recurs once. + // + // So we are modifying the parameter so our code doesn't + // break. + if ($this->until < $this->startDate) { + $this->until = $this->startDate; + } + break; + + case 'INTERVAL': + case 'COUNT': + $val = (int) $value; + if ($val < 1) { + throw new InvalidDataException(strtoupper($key).' in RRULE must be a positive integer!'); + } + $key = strtolower($key); + $this->$key = $val; + break; + + case 'BYSECOND': + $this->bySecond = (array) $value; + break; + + case 'BYMINUTE': + $this->byMinute = (array) $value; + break; + + case 'BYHOUR': + $this->byHour = (array) $value; + break; + + case 'BYDAY': + $value = (array) $value; + foreach ($value as $part) { + if (!preg_match('#^ (-|\+)? ([1-5])? (MO|TU|WE|TH|FR|SA|SU) $# xi', $part)) { + throw new InvalidDataException('Invalid part in BYDAY clause: '.$part); + } + } + $this->byDay = $value; + break; + + case 'BYMONTHDAY': + $this->byMonthDay = (array) $value; + break; + + case 'BYYEARDAY': + $this->byYearDay = (array) $value; + foreach ($this->byYearDay as $byYearDay) { + if (!is_numeric($byYearDay) || (int) $byYearDay < -366 || 0 == (int) $byYearDay || (int) $byYearDay > 366) { + throw new InvalidDataException('BYYEARDAY in RRULE must have value(s) from 1 to 366, or -366 to -1!'); + } + } + break; + + case 'BYWEEKNO': + $this->byWeekNo = (array) $value; + foreach ($this->byWeekNo as $byWeekNo) { + if (!is_numeric($byWeekNo) || (int) $byWeekNo < -53 || 0 == (int) $byWeekNo || (int) $byWeekNo > 53) { + throw new InvalidDataException('BYWEEKNO in RRULE must have value(s) from 1 to 53, or -53 to -1!'); + } + } + break; + + case 'BYMONTH': + $this->byMonth = (array) $value; + foreach ($this->byMonth as $byMonth) { + if (!is_numeric($byMonth) || (int) $byMonth < 1 || (int) $byMonth > 12) { + throw new InvalidDataException('BYMONTH in RRULE must have value(s) between 1 and 12!'); + } + } + break; + + case 'BYSETPOS': + $this->bySetPos = (array) $value; + break; + + case 'WKST': + $this->weekStart = strtoupper($value); + break; + + default: + throw new InvalidDataException('Not supported: '.strtoupper($key)); + } + } + } + + /** + * Mappings between the day number and english day name. + * + * @var array + */ + protected $dayNames = [ + 0 => 'Sunday', + 1 => 'Monday', + 2 => 'Tuesday', + 3 => 'Wednesday', + 4 => 'Thursday', + 5 => 'Friday', + 6 => 'Saturday', + ]; + + /** + * Returns all the occurrences for a monthly frequency with a 'byDay' or + * 'byMonthDay' expansion for the current month. + * + * The returned list is an array of integers with the day of month (1-31). + * + * @return array + */ + protected function getMonthlyOccurrences() + { + $startDate = clone $this->currentDate; + + $byDayResults = []; + + // Our strategy is to simply go through the byDays, advance the date to + // that point and add it to the results. + if ($this->byDay) { + foreach ($this->byDay as $day) { + $dayName = $this->dayNames[$this->dayMap[substr($day, -2)]]; + + // Dayname will be something like 'wednesday'. Now we need to find + // all wednesdays in this month. + $dayHits = []; + + // workaround for missing 'first day of the month' support in hhvm + $checkDate = new \DateTime($startDate->format('Y-m-1')); + // workaround modify always advancing the date even if the current day is a $dayName in hhvm + if ($checkDate->format('l') !== $dayName) { + $checkDate = $checkDate->modify($dayName); + } + + do { + $dayHits[] = $checkDate->format('j'); + $checkDate = $checkDate->modify('next '.$dayName); + } while ($checkDate->format('n') === $startDate->format('n')); + + // So now we have 'all wednesdays' for month. It is however + // possible that the user only really wanted the 1st, 2nd or last + // wednesday. + if (strlen($day) > 2) { + $offset = (int) substr($day, 0, -2); + + if ($offset > 0) { + // It is possible that the day does not exist, such as a + // 5th or 6th wednesday of the month. + if (isset($dayHits[$offset - 1])) { + $byDayResults[] = $dayHits[$offset - 1]; + } + } else { + // if it was negative we count from the end of the array + // might not exist, fx. -5th tuesday + if (isset($dayHits[count($dayHits) + $offset])) { + $byDayResults[] = $dayHits[count($dayHits) + $offset]; + } + } + } else { + // There was no counter (first, second, last wednesdays), so we + // just need to add the all to the list). + $byDayResults = array_merge($byDayResults, $dayHits); + } + } + } + + $byMonthDayResults = []; + if ($this->byMonthDay) { + foreach ($this->byMonthDay as $monthDay) { + // Removing values that are out of range for this month + if ($monthDay > $startDate->format('t') || + $monthDay < 0 - $startDate->format('t')) { + continue; + } + if ($monthDay > 0) { + $byMonthDayResults[] = $monthDay; + } else { + // Negative values + $byMonthDayResults[] = $startDate->format('t') + 1 + $monthDay; + } + } + } + + // If there was just byDay or just byMonthDay, they just specify our + // (almost) final list. If both were provided, then byDay limits the + // list. + if ($this->byMonthDay && $this->byDay) { + $result = array_intersect($byMonthDayResults, $byDayResults); + } elseif ($this->byMonthDay) { + $result = $byMonthDayResults; + } else { + $result = $byDayResults; + } + $result = array_unique($result); + sort($result, SORT_NUMERIC); + + // The last thing that needs checking is the BYSETPOS. If it's set, it + // means only certain items in the set survive the filter. + if (!$this->bySetPos) { + return $result; + } + + $filteredResult = []; + foreach ($this->bySetPos as $setPos) { + if ($setPos < 0) { + $setPos = count($result) + ($setPos + 1); + } + if (isset($result[$setPos - 1])) { + $filteredResult[] = $result[$setPos - 1]; + } + } + + sort($filteredResult, SORT_NUMERIC); + + return $filteredResult; + } + + /** + * Simple mapping from iCalendar day names to day numbers. + * + * @var array + */ + protected $dayMap = [ + 'SU' => 0, + 'MO' => 1, + 'TU' => 2, + 'WE' => 3, + 'TH' => 4, + 'FR' => 5, + 'SA' => 6, + ]; + + protected function getHours() + { + $recurrenceHours = []; + foreach ($this->byHour as $byHour) { + $recurrenceHours[] = $byHour; + } + + return $recurrenceHours; + } + + protected function getDays() + { + $recurrenceDays = []; + foreach ($this->byDay as $byDay) { + // The day may be preceded with a positive (+n) or + // negative (-n) integer. However, this does not make + // sense in 'weekly' so we ignore it here. + $recurrenceDays[] = $this->dayMap[substr($byDay, -2)]; + } + + return $recurrenceDays; + } + + protected function getMonths() + { + $recurrenceMonths = []; + foreach ($this->byMonth as $byMonth) { + $recurrenceMonths[] = $byMonth; + } + + return $recurrenceMonths; + } +} diff --git a/lib/vendor/sabre/vobject/lib/Settings.php b/lib/vendor/sabre/vobject/lib/Settings.php new file mode 100644 index 0000000..b0bb80a --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Settings.php @@ -0,0 +1,55 @@ +children() as $component) { + if (!$component instanceof VObject\Component) { + continue; + } + + // Get all timezones + if ('VTIMEZONE' === $component->name) { + $this->vtimezones[(string) $component->TZID] = $component; + continue; + } + + // Get component UID for recurring Events search + if (!$component->UID) { + $component->UID = sha1(microtime()).'-vobjectimport'; + } + $uid = (string) $component->UID; + + // Take care of recurring events + if (!array_key_exists($uid, $this->objects)) { + $this->objects[$uid] = new VCalendar(); + } + + $this->objects[$uid]->add(clone $component); + } + } + + /** + * Every time getNext() is called, a new object will be parsed, until we + * hit the end of the stream. + * + * When the end is reached, null will be returned. + * + * @return \Sabre\VObject\Component|null + */ + public function getNext() + { + if ($object = array_shift($this->objects)) { + // create our baseobject + $object->version = '2.0'; + $object->prodid = '-//Sabre//Sabre VObject '.VObject\Version::VERSION.'//EN'; + $object->calscale = 'GREGORIAN'; + + // add vtimezone information to obj (if we have it) + foreach ($this->vtimezones as $vtimezone) { + $object->add($vtimezone); + } + + return $object; + } else { + return; + } + } +} diff --git a/lib/vendor/sabre/vobject/lib/Splitter/SplitterInterface.php b/lib/vendor/sabre/vobject/lib/Splitter/SplitterInterface.php new file mode 100644 index 0000000..c845ac5 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Splitter/SplitterInterface.php @@ -0,0 +1,38 @@ +input = $input; + $this->parser = new MimeDir($input, $options); + } + + /** + * Every time getNext() is called, a new object will be parsed, until we + * hit the end of the stream. + * + * When the end is reached, null will be returned. + * + * @return \Sabre\VObject\Component|null + */ + public function getNext() + { + try { + $object = $this->parser->parse(); + + if (!$object instanceof VObject\Component\VCard) { + throw new VObject\ParseException('The supplied input contained non-VCARD data.'); + } + } catch (VObject\EofException $e) { + return; + } + + return $object; + } +} diff --git a/lib/vendor/sabre/vobject/lib/StringUtil.php b/lib/vendor/sabre/vobject/lib/StringUtil.php new file mode 100644 index 0000000..b04539e --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/StringUtil.php @@ -0,0 +1,50 @@ +addGuesser('lic', new GuessFromLicEntry()); + $this->addGuesser('msTzId', new GuessFromMsTzId()); + $this->addFinder('tzid', new FindFromTimezoneIdentifier()); + $this->addFinder('tzmap', new FindFromTimezoneMap()); + $this->addFinder('offset', new FindFromOffset()); + } + + private static function getInstance(): self + { + if (null === self::$instance) { + self::$instance = new self(); + } + + return self::$instance; + } + + private function addGuesser(string $key, TimezoneGuesser $guesser): void + { + $this->timezoneGuessers[$key] = $guesser; + } + + private function addFinder(string $key, TimezoneFinder $finder): void + { + $this->timezoneFinders[$key] = $finder; + } + + /** + * This method will try to find out the correct timezone for an iCalendar + * date-time value. + * + * You must pass the contents of the TZID parameter, as well as the full + * calendar. + * + * If the lookup fails, this method will return the default PHP timezone + * (as configured using date_default_timezone_set, or the date.timezone ini + * setting). + * + * Alternatively, if $failIfUncertain is set to true, it will throw an + * exception if we cannot accurately determine the timezone. + */ + private function findTimeZone(string $tzid, ?Component $vcalendar = null, bool $failIfUncertain = false): DateTimeZone + { + foreach ($this->timezoneFinders as $timezoneFinder) { + $timezone = $timezoneFinder->find($tzid, $failIfUncertain); + if (!$timezone instanceof DateTimeZone) { + continue; + } + + return $timezone; + } + + if ($vcalendar) { + // If that didn't work, we will scan VTIMEZONE objects + foreach ($vcalendar->select('VTIMEZONE') as $vtimezone) { + if ((string) $vtimezone->TZID === $tzid) { + foreach ($this->timezoneGuessers as $timezoneGuesser) { + $timezone = $timezoneGuesser->guess($vtimezone, $failIfUncertain); + if (!$timezone instanceof DateTimeZone) { + continue; + } + + return $timezone; + } + } + } + } + + if ($failIfUncertain) { + throw new InvalidArgumentException('We were unable to determine the correct PHP timezone for tzid: '.$tzid); + } + + // If we got all the way here, we default to whatever has been set as the PHP default timezone. + return new DateTimeZone(date_default_timezone_get()); + } + + public static function addTimezoneGuesser(string $key, TimezoneGuesser $guesser): void + { + self::getInstance()->addGuesser($key, $guesser); + } + + public static function addTimezoneFinder(string $key, TimezoneFinder $finder): void + { + self::getInstance()->addFinder($key, $finder); + } + + /** + * @param string $tzid + * @param false $failIfUncertain + * + * @return DateTimeZone + */ + public static function getTimeZone($tzid, ?Component $vcalendar = null, $failIfUncertain = false) + { + return self::getInstance()->findTimeZone($tzid, $vcalendar, $failIfUncertain); + } + + public static function clean(): void + { + self::$instance = null; + } + + // Keeping things for backwards compatibility + /** + * @var array|null + * + * @deprecated + */ + public static $map = null; + + /** + * List of microsoft exchange timezone ids. + * + * Source: http://msdn.microsoft.com/en-us/library/aa563018(loband).aspx + * + * @deprecated + */ + public static $microsoftExchangeMap = [ + 0 => 'UTC', + 31 => 'Africa/Casablanca', + // Insanely, id #2 is used for both Europe/Lisbon, and Europe/Sarajevo. + // I'm not even kidding.. We handle this special case in the + // getTimeZone method. + 2 => 'Europe/Lisbon', + 1 => 'Europe/London', + 4 => 'Europe/Berlin', + 6 => 'Europe/Prague', + 3 => 'Europe/Paris', + 69 => 'Africa/Luanda', // This was a best guess + 7 => 'Europe/Athens', + 5 => 'Europe/Bucharest', + 49 => 'Africa/Cairo', + 50 => 'Africa/Harare', + 59 => 'Europe/Helsinki', + 27 => 'Asia/Jerusalem', + 26 => 'Asia/Baghdad', + 74 => 'Asia/Kuwait', + 51 => 'Europe/Moscow', + 56 => 'Africa/Nairobi', + 25 => 'Asia/Tehran', + 24 => 'Asia/Muscat', // Best guess + 54 => 'Asia/Baku', + 48 => 'Asia/Kabul', + 58 => 'Asia/Yekaterinburg', + 47 => 'Asia/Karachi', + 23 => 'Asia/Calcutta', + 62 => 'Asia/Kathmandu', + 46 => 'Asia/Almaty', + 71 => 'Asia/Dhaka', + 66 => 'Asia/Colombo', + 61 => 'Asia/Rangoon', + 22 => 'Asia/Bangkok', + 64 => 'Asia/Krasnoyarsk', + 45 => 'Asia/Shanghai', + 63 => 'Asia/Irkutsk', + 21 => 'Asia/Singapore', + 73 => 'Australia/Perth', + 75 => 'Asia/Taipei', + 20 => 'Asia/Tokyo', + 72 => 'Asia/Seoul', + 70 => 'Asia/Yakutsk', + 19 => 'Australia/Adelaide', + 44 => 'Australia/Darwin', + 18 => 'Australia/Brisbane', + 76 => 'Australia/Sydney', + 43 => 'Pacific/Guam', + 42 => 'Australia/Hobart', + 68 => 'Asia/Vladivostok', + 41 => 'Asia/Magadan', + 17 => 'Pacific/Auckland', + 40 => 'Pacific/Fiji', + 67 => 'Pacific/Tongatapu', + 29 => 'Atlantic/Azores', + 53 => 'Atlantic/Cape_Verde', + 30 => 'America/Noronha', + 8 => 'America/Sao_Paulo', // Best guess + 32 => 'America/Argentina/Buenos_Aires', + 60 => 'America/Godthab', + 28 => 'America/St_Johns', + 9 => 'America/Halifax', + 33 => 'America/Caracas', + 65 => 'America/Santiago', + 35 => 'America/Bogota', + 10 => 'America/New_York', + 34 => 'America/Indiana/Indianapolis', + 55 => 'America/Guatemala', + 11 => 'America/Chicago', + 37 => 'America/Mexico_City', + 36 => 'America/Edmonton', + 38 => 'America/Phoenix', + 12 => 'America/Denver', // Best guess + 13 => 'America/Los_Angeles', // Best guess + 14 => 'America/Anchorage', + 15 => 'Pacific/Honolulu', + 16 => 'Pacific/Midway', + 39 => 'Pacific/Kwajalein', + ]; + + /** + * This method will load in all the tz mapping information, if it's not yet + * done. + * + * @deprecated + */ + public static function loadTzMaps() + { + if (!is_null(self::$map)) { + return; + } + + self::$map = array_merge( + include __DIR__.'/timezonedata/windowszones.php', + include __DIR__.'/timezonedata/lotuszones.php', + include __DIR__.'/timezonedata/exchangezones.php', + include __DIR__.'/timezonedata/php-workaround.php' + ); + } + + /** + * This method returns an array of timezone identifiers, that are supported + * by DateTimeZone(), but not returned by DateTimeZone::listIdentifiers(). + * + * We're not using DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC) because: + * - It's not supported by some PHP versions as well as HHVM. + * - It also returns identifiers, that are invalid values for new DateTimeZone() on some PHP versions. + * (See timezonedata/php-bc.php and timezonedata php-workaround.php) + * + * @return array + * + * @deprecated + */ + public static function getIdentifiersBC() + { + return include __DIR__.'/timezonedata/php-bc.php'; + } +} diff --git a/lib/vendor/sabre/vobject/lib/TimezoneGuesser/FindFromOffset.php b/lib/vendor/sabre/vobject/lib/TimezoneGuesser/FindFromOffset.php new file mode 100644 index 0000000..990ac96 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/TimezoneGuesser/FindFromOffset.php @@ -0,0 +1,31 @@ +getIdentifiersBC())) + ) { + return new DateTimeZone($tzid); + } + } catch (Exception $e) { + } + + return null; + } + + /** + * This method returns an array of timezone identifiers, that are supported + * by DateTimeZone(), but not returned by DateTimeZone::listIdentifiers(). + * + * We're not using DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC) because: + * - It's not supported by some PHP versions as well as HHVM. + * - It also returns identifiers, that are invalid values for new DateTimeZone() on some PHP versions. + * (See timezonedata/php-bc.php and timezonedata php-workaround.php) + * + * @return array + */ + private function getIdentifiersBC() + { + return include __DIR__.'/../timezonedata/php-bc.php'; + } +} diff --git a/lib/vendor/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneMap.php b/lib/vendor/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneMap.php new file mode 100644 index 0000000..b52ba6a --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneMap.php @@ -0,0 +1,78 @@ +hasTzInMap($tzid)) { + return new DateTimeZone($this->getTzFromMap($tzid)); + } + + // Some Microsoft products prefix the offset first, so let's strip that off + // and see if it is our tzid map. We don't want to check for this first just + // in case there are overrides in our tzid map. + foreach ($this->patterns as $pattern) { + if (!preg_match($pattern, $tzid, $matches)) { + continue; + } + $tzidAlternate = $matches[3]; + if ($this->hasTzInMap($tzidAlternate)) { + return new DateTimeZone($this->getTzFromMap($tzidAlternate)); + } + } + + return null; + } + + /** + * This method returns an array of timezone identifiers, that are supported + * by DateTimeZone(), but not returned by DateTimeZone::listIdentifiers(). + * + * We're not using DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC) because: + * - It's not supported by some PHP versions as well as HHVM. + * - It also returns identifiers, that are invalid values for new DateTimeZone() on some PHP versions. + * (See timezonedata/php-bc.php and timezonedata php-workaround.php) + * + * @return array + */ + private function getTzMaps() + { + if ([] === $this->map) { + $this->map = array_merge( + include __DIR__.'/../timezonedata/windowszones.php', + include __DIR__.'/../timezonedata/lotuszones.php', + include __DIR__.'/../timezonedata/exchangezones.php', + include __DIR__.'/../timezonedata/php-workaround.php' + ); + } + + return $this->map; + } + + private function getTzFromMap(string $tzid): string + { + return $this->getTzMaps()[$tzid]; + } + + private function hasTzInMap(string $tzid): bool + { + return isset($this->getTzMaps()[$tzid]); + } +} diff --git a/lib/vendor/sabre/vobject/lib/TimezoneGuesser/GuessFromLicEntry.php b/lib/vendor/sabre/vobject/lib/TimezoneGuesser/GuessFromLicEntry.php new file mode 100644 index 0000000..f340a39 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/TimezoneGuesser/GuessFromLicEntry.php @@ -0,0 +1,33 @@ +{'X-LIC-LOCATION'})) { + return null; + } + + $lic = (string) $vtimezone->{'X-LIC-LOCATION'}; + + // Libical generators may specify strings like + // "SystemV/EST5EDT". For those we must remove the + // SystemV part. + if ('SystemV/' === substr($lic, 0, 8)) { + $lic = substr($lic, 8); + } + + return TimeZoneUtil::getTimeZone($lic, null, $failIfUncertain); + } +} diff --git a/lib/vendor/sabre/vobject/lib/TimezoneGuesser/GuessFromMsTzId.php b/lib/vendor/sabre/vobject/lib/TimezoneGuesser/GuessFromMsTzId.php new file mode 100644 index 0000000..b11ce18 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/TimezoneGuesser/GuessFromMsTzId.php @@ -0,0 +1,119 @@ + 'UTC', + 31 => 'Africa/Casablanca', + + // Insanely, id #2 is used for both Europe/Lisbon, and Europe/Sarajevo. + // I'm not even kidding.. We handle this special case in the + // getTimeZone method. + 2 => 'Europe/Lisbon', + 1 => 'Europe/London', + 4 => 'Europe/Berlin', + 6 => 'Europe/Prague', + 3 => 'Europe/Paris', + 69 => 'Africa/Luanda', // This was a best guess + 7 => 'Europe/Athens', + 5 => 'Europe/Bucharest', + 49 => 'Africa/Cairo', + 50 => 'Africa/Harare', + 59 => 'Europe/Helsinki', + 27 => 'Asia/Jerusalem', + 26 => 'Asia/Baghdad', + 74 => 'Asia/Kuwait', + 51 => 'Europe/Moscow', + 56 => 'Africa/Nairobi', + 25 => 'Asia/Tehran', + 24 => 'Asia/Muscat', // Best guess + 54 => 'Asia/Baku', + 48 => 'Asia/Kabul', + 58 => 'Asia/Yekaterinburg', + 47 => 'Asia/Karachi', + 23 => 'Asia/Calcutta', + 62 => 'Asia/Kathmandu', + 46 => 'Asia/Almaty', + 71 => 'Asia/Dhaka', + 66 => 'Asia/Colombo', + 61 => 'Asia/Rangoon', + 22 => 'Asia/Bangkok', + 64 => 'Asia/Krasnoyarsk', + 45 => 'Asia/Shanghai', + 63 => 'Asia/Irkutsk', + 21 => 'Asia/Singapore', + 73 => 'Australia/Perth', + 75 => 'Asia/Taipei', + 20 => 'Asia/Tokyo', + 72 => 'Asia/Seoul', + 70 => 'Asia/Yakutsk', + 19 => 'Australia/Adelaide', + 44 => 'Australia/Darwin', + 18 => 'Australia/Brisbane', + 76 => 'Australia/Sydney', + 43 => 'Pacific/Guam', + 42 => 'Australia/Hobart', + 68 => 'Asia/Vladivostok', + 41 => 'Asia/Magadan', + 17 => 'Pacific/Auckland', + 40 => 'Pacific/Fiji', + 67 => 'Pacific/Tongatapu', + 29 => 'Atlantic/Azores', + 53 => 'Atlantic/Cape_Verde', + 30 => 'America/Noronha', + 8 => 'America/Sao_Paulo', // Best guess + 32 => 'America/Argentina/Buenos_Aires', + 60 => 'America/Godthab', + 28 => 'America/St_Johns', + 9 => 'America/Halifax', + 33 => 'America/Caracas', + 65 => 'America/Santiago', + 35 => 'America/Bogota', + 10 => 'America/New_York', + 34 => 'America/Indiana/Indianapolis', + 55 => 'America/Guatemala', + 11 => 'America/Chicago', + 37 => 'America/Mexico_City', + 36 => 'America/Edmonton', + 38 => 'America/Phoenix', + 12 => 'America/Denver', // Best guess + 13 => 'America/Los_Angeles', // Best guess + 14 => 'America/Anchorage', + 15 => 'Pacific/Honolulu', + 16 => 'Pacific/Midway', + 39 => 'Pacific/Kwajalein', + ]; + + public function guess(VTimeZone $vtimezone, bool $throwIfUnsure = false): ?DateTimeZone + { + // Microsoft may add a magic number, which we also have an + // answer for. + if (!isset($vtimezone->{'X-MICROSOFT-CDO-TZID'})) { + return null; + } + $cdoId = (int) $vtimezone->{'X-MICROSOFT-CDO-TZID'}->getValue(); + + // 2 can mean both Europe/Lisbon and Europe/Sarajevo. + if (2 === $cdoId && false !== strpos((string) $vtimezone->TZID, 'Sarajevo')) { + return new DateTimeZone('Europe/Sarajevo'); + } + + if (isset(self::$microsoftExchangeMap[$cdoId])) { + return new DateTimeZone(self::$microsoftExchangeMap[$cdoId]); + } + + return null; + } +} diff --git a/lib/vendor/sabre/vobject/lib/TimezoneGuesser/TimezoneFinder.php b/lib/vendor/sabre/vobject/lib/TimezoneGuesser/TimezoneFinder.php new file mode 100644 index 0000000..5aa880a --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/TimezoneGuesser/TimezoneFinder.php @@ -0,0 +1,10 @@ +getDocumentType(); + if ($inputVersion === $targetVersion) { + return clone $input; + } + + if (!in_array($inputVersion, [Document::VCARD21, Document::VCARD30, Document::VCARD40])) { + throw new \InvalidArgumentException('Only vCard 2.1, 3.0 and 4.0 are supported for the input data'); + } + if (!in_array($targetVersion, [Document::VCARD30, Document::VCARD40])) { + throw new \InvalidArgumentException('You can only use vCard 3.0 or 4.0 for the target version'); + } + + $newVersion = Document::VCARD40 === $targetVersion ? '4.0' : '3.0'; + + $output = new Component\VCard([ + 'VERSION' => $newVersion, + ]); + + // We might have generated a default UID. Remove it! + unset($output->UID); + + foreach ($input->children() as $property) { + $this->convertProperty($input, $output, $property, $targetVersion); + } + + return $output; + } + + /** + * Handles conversion of a single property. + * + * @param int $targetVersion + */ + protected function convertProperty(Component\VCard $input, Component\VCard $output, Property $property, $targetVersion) + { + // Skipping these, those are automatically added. + if (in_array($property->name, ['VERSION', 'PRODID'])) { + return; + } + + $parameters = $property->parameters(); + $valueType = null; + if (isset($parameters['VALUE'])) { + $valueType = $parameters['VALUE']->getValue(); + unset($parameters['VALUE']); + } + if (!$valueType) { + $valueType = $property->getValueType(); + } + if (Document::VCARD30 !== $targetVersion && 'PHONE-NUMBER' === $valueType) { + $valueType = null; + } + $newProperty = $output->createProperty( + $property->name, + $property->getParts(), + [], // parameters will get added a bit later. + $valueType + ); + + if (Document::VCARD30 === $targetVersion) { + if ($property instanceof Property\Uri && in_array($property->name, ['PHOTO', 'LOGO', 'SOUND'])) { + $newProperty = $this->convertUriToBinary($output, $newProperty); + } elseif ($property instanceof Property\VCard\DateAndOrTime) { + // In vCard 4, the birth year may be optional. This is not the + // case for vCard 3. Apple has a workaround for this that + // allows applications that support Apple's extension still + // omit birthyears in vCard 3, but applications that do not + // support this, will just use a random birthyear. We're + // choosing 1604 for the birthyear, because that's what apple + // uses. + $parts = DateTimeParser::parseVCardDateTime($property->getValue()); + if (is_null($parts['year'])) { + $newValue = '1604-'.$parts['month'].'-'.$parts['date']; + $newProperty->setValue($newValue); + $newProperty['X-APPLE-OMIT-YEAR'] = '1604'; + } + + if ('ANNIVERSARY' == $newProperty->name) { + // Microsoft non-standard anniversary + $newProperty->name = 'X-ANNIVERSARY'; + + // We also need to add a new apple property for the same + // purpose. This apple property needs a 'label' in the same + // group, so we first need to find a groupname that doesn't + // exist yet. + $x = 1; + while ($output->select('ITEM'.$x.'.')) { + ++$x; + } + $output->add('ITEM'.$x.'.X-ABDATE', $newProperty->getValue(), ['VALUE' => 'DATE-AND-OR-TIME']); + $output->add('ITEM'.$x.'.X-ABLABEL', '_$!!$_'); + } + } elseif ('KIND' === $property->name) { + switch (strtolower($property->getValue())) { + case 'org': + // vCard 3.0 does not have an equivalent to KIND:ORG, + // but apple has an extension that means the same + // thing. + $newProperty = $output->createProperty('X-ABSHOWAS', 'COMPANY'); + break; + + case 'individual': + // Individual is implicit, so we skip it. + return; + + case 'group': + // OS X addressbook property + $newProperty = $output->createProperty('X-ADDRESSBOOKSERVER-KIND', 'GROUP'); + break; + } + } elseif ('MEMBER' === $property->name) { + $newProperty = $output->createProperty('X-ADDRESSBOOKSERVER-MEMBER', $property->getValue()); + } + } elseif (Document::VCARD40 === $targetVersion) { + // These properties were removed in vCard 4.0 + if (in_array($property->name, ['NAME', 'MAILER', 'LABEL', 'CLASS'])) { + return; + } + + if ($property instanceof Property\Binary) { + $newProperty = $this->convertBinaryToUri($output, $newProperty, $parameters); + } elseif ($property instanceof Property\VCard\DateAndOrTime && isset($parameters['X-APPLE-OMIT-YEAR'])) { + // If a property such as BDAY contained 'X-APPLE-OMIT-YEAR', + // then we're stripping the year from the vcard 4 value. + $parts = DateTimeParser::parseVCardDateTime($property->getValue()); + if ($parts['year'] === $property['X-APPLE-OMIT-YEAR']->getValue()) { + $newValue = '--'.$parts['month'].'-'.$parts['date']; + $newProperty->setValue($newValue); + } + + // Regardless if the year matched or not, we do need to strip + // X-APPLE-OMIT-YEAR. + unset($parameters['X-APPLE-OMIT-YEAR']); + } + switch ($property->name) { + case 'X-ABSHOWAS': + if ('COMPANY' === strtoupper($property->getValue())) { + $newProperty = $output->createProperty('KIND', 'ORG'); + } + break; + case 'X-ADDRESSBOOKSERVER-KIND': + if ('GROUP' === strtoupper($property->getValue())) { + $newProperty = $output->createProperty('KIND', 'GROUP'); + } + break; + case 'X-ADDRESSBOOKSERVER-MEMBER': + $newProperty = $output->createProperty('MEMBER', $property->getValue()); + break; + case 'X-ANNIVERSARY': + $newProperty->name = 'ANNIVERSARY'; + // If we already have an anniversary property with the same + // value, ignore. + foreach ($output->select('ANNIVERSARY') as $anniversary) { + if ($anniversary->getValue() === $newProperty->getValue()) { + return; + } + } + break; + case 'X-ABDATE': + // Find out what the label was, if it exists. + if (!$property->group) { + break; + } + $label = $input->{$property->group.'.X-ABLABEL'}; + + // We only support converting anniversaries. + if (!$label || '_$!!$_' !== $label->getValue()) { + break; + } + + // If we already have an anniversary property with the same + // value, ignore. + foreach ($output->select('ANNIVERSARY') as $anniversary) { + if ($anniversary->getValue() === $newProperty->getValue()) { + return; + } + } + $newProperty->name = 'ANNIVERSARY'; + break; + // Apple's per-property label system. + case 'X-ABLABEL': + if ('_$!!$_' === $newProperty->getValue()) { + // We can safely remove these, as they are converted to + // ANNIVERSARY properties. + return; + } + break; + } + } + + // set property group + $newProperty->group = $property->group; + + if (Document::VCARD40 === $targetVersion) { + $this->convertParameters40($newProperty, $parameters); + } else { + $this->convertParameters30($newProperty, $parameters); + } + + // Lastly, we need to see if there's a need for a VALUE parameter. + // + // We can do that by instantiating a empty property with that name, and + // seeing if the default valueType is identical to the current one. + $tempProperty = $output->createProperty($newProperty->name); + if ($tempProperty->getValueType() !== $newProperty->getValueType()) { + $newProperty['VALUE'] = $newProperty->getValueType(); + } + + $output->add($newProperty); + } + + /** + * Converts a BINARY property to a URI property. + * + * vCard 4.0 no longer supports BINARY properties. + * + * @param Property\Uri $property the input property + * @param $parameters list of parameters that will eventually be added to + * the new property + * + * @return Property\Uri + */ + protected function convertBinaryToUri(Component\VCard $output, Property\Binary $newProperty, array &$parameters) + { + $value = $newProperty->getValue(); + $newProperty = $output->createProperty( + $newProperty->name, + null, // no value + [], // no parameters yet + 'URI' // Forcing the BINARY type + ); + + $mimeType = 'application/octet-stream'; + + // See if we can find a better mimetype. + if (isset($parameters['TYPE'])) { + $newTypes = []; + foreach ($parameters['TYPE']->getParts() as $typePart) { + if (in_array( + strtoupper($typePart), + ['JPEG', 'PNG', 'GIF'] + )) { + $mimeType = 'image/'.strtolower($typePart); + } else { + $newTypes[] = $typePart; + } + } + + // If there were any parameters we're not converting to a + // mime-type, we need to keep them. + if ($newTypes) { + $parameters['TYPE']->setParts($newTypes); + } else { + unset($parameters['TYPE']); + } + } + + $newProperty->setValue('data:'.$mimeType.';base64,'.base64_encode($value)); + + return $newProperty; + } + + /** + * Converts a URI property to a BINARY property. + * + * In vCard 4.0 attachments are encoded as data: uri. Even though these may + * be valid in vCard 3.0 as well, we should convert those to BINARY if + * possible, to improve compatibility. + * + * @param Property\Uri $property the input property + * + * @return Property\Binary|null + */ + protected function convertUriToBinary(Component\VCard $output, Property\Uri $newProperty) + { + $value = $newProperty->getValue(); + + // Only converting data: uris + if ('data:' !== substr($value, 0, 5)) { + return $newProperty; + } + + $newProperty = $output->createProperty( + $newProperty->name, + null, // no value + [], // no parameters yet + 'BINARY' + ); + + $mimeType = substr($value, 5, strpos($value, ',') - 5); + if (strpos($mimeType, ';')) { + $mimeType = substr($mimeType, 0, strpos($mimeType, ';')); + $newProperty->setValue(base64_decode(substr($value, strpos($value, ',') + 1))); + } else { + $newProperty->setValue(substr($value, strpos($value, ',') + 1)); + } + unset($value); + + $newProperty['ENCODING'] = 'b'; + switch ($mimeType) { + case 'image/jpeg': + $newProperty['TYPE'] = 'JPEG'; + break; + case 'image/png': + $newProperty['TYPE'] = 'PNG'; + break; + case 'image/gif': + $newProperty['TYPE'] = 'GIF'; + break; + } + + return $newProperty; + } + + /** + * Adds parameters to a new property for vCard 4.0. + */ + protected function convertParameters40(Property $newProperty, array $parameters) + { + // Adding all parameters. + foreach ($parameters as $param) { + // vCard 2.1 allowed parameters with no name + if ($param->noName) { + $param->noName = false; + } + + switch ($param->name) { + // We need to see if there's any TYPE=PREF, because in vCard 4 + // that's now PREF=1. + case 'TYPE': + foreach ($param->getParts() as $paramPart) { + if ('PREF' === strtoupper($paramPart)) { + $newProperty->add('PREF', '1'); + } else { + $newProperty->add($param->name, $paramPart); + } + } + break; + // These no longer exist in vCard 4 + case 'ENCODING': + case 'CHARSET': + break; + + default: + $newProperty->add($param->name, $param->getParts()); + break; + } + } + } + + /** + * Adds parameters to a new property for vCard 3.0. + */ + protected function convertParameters30(Property $newProperty, array $parameters) + { + // Adding all parameters. + foreach ($parameters as $param) { + // vCard 2.1 allowed parameters with no name + if ($param->noName) { + $param->noName = false; + } + + switch ($param->name) { + case 'ENCODING': + // This value only existed in vCard 2.1, and should be + // removed for anything else. + if ('QUOTED-PRINTABLE' !== strtoupper($param->getValue())) { + $newProperty->add($param->name, $param->getParts()); + } + break; + + /* + * Converting PREF=1 to TYPE=PREF. + * + * Any other PREF numbers we'll drop. + */ + case 'PREF': + if ('1' == $param->getValue()) { + $newProperty->add('TYPE', 'PREF'); + } + break; + + default: + $newProperty->add($param->name, $param->getParts()); + break; + } + } + } +} diff --git a/lib/vendor/sabre/vobject/lib/Version.php b/lib/vendor/sabre/vobject/lib/Version.php new file mode 100644 index 0000000..decd424 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/Version.php @@ -0,0 +1,18 @@ +serialize(); + } + + /** + * Serializes a jCal or jCard object. + * + * @param int $options + * + * @return string + */ + public static function writeJson(Component $component, $options = 0) + { + return json_encode($component, $options); + } + + /** + * Serializes a xCal or xCard object. + * + * @return string + */ + public static function writeXml(Component $component) + { + $writer = new Xml\Writer(); + $writer->openMemory(); + $writer->setIndent(true); + + $writer->startDocument('1.0', 'utf-8'); + + if ($component instanceof Component\VCalendar) { + $writer->startElement('icalendar'); + $writer->writeAttribute('xmlns', Parser\XML::XCAL_NAMESPACE); + } else { + $writer->startElement('vcards'); + $writer->writeAttribute('xmlns', Parser\XML::XCARD_NAMESPACE); + } + + $component->xmlSerialize($writer); + + $writer->endElement(); + + return $writer->outputMemory(); + } +} diff --git a/lib/vendor/sabre/vobject/lib/timezonedata/exchangezones.php b/lib/vendor/sabre/vobject/lib/timezonedata/exchangezones.php new file mode 100644 index 0000000..3e7eace --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/timezonedata/exchangezones.php @@ -0,0 +1,95 @@ + 'UTC', + 'Casablanca, Monrovia' => 'Africa/Casablanca', + 'Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London' => 'Europe/Lisbon', + 'Greenwich Mean Time; Dublin, Edinburgh, London' => 'Europe/London', + 'Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna' => 'Europe/Berlin', + 'Amsterdam, Berlin, Bern, Rom, Stockholm, Wien' => 'Europe/Berlin', + 'Belgrade, Pozsony, Budapest, Ljubljana, Prague' => 'Europe/Prague', + 'Brussels, Copenhagen, Madrid, Paris' => 'Europe/Paris', + 'Paris, Madrid, Brussels, Copenhagen' => 'Europe/Paris', + 'Prague, Central Europe' => 'Europe/Prague', + 'Sarajevo, Skopje, Sofija, Vilnius, Warsaw, Zagreb' => 'Europe/Sarajevo', + 'West Central Africa' => 'Africa/Luanda', // This was a best guess + 'Athens, Istanbul, Minsk' => 'Europe/Athens', + 'Bucharest' => 'Europe/Bucharest', + 'Cairo' => 'Africa/Cairo', + 'Harare, Pretoria' => 'Africa/Harare', + 'Helsinki, Riga, Tallinn' => 'Europe/Helsinki', + 'Israel, Jerusalem Standard Time' => 'Asia/Jerusalem', + 'Baghdad' => 'Asia/Baghdad', + 'Arab, Kuwait, Riyadh' => 'Asia/Kuwait', + 'Moscow, St. Petersburg, Volgograd' => 'Europe/Moscow', + 'East Africa, Nairobi' => 'Africa/Nairobi', + 'Tehran' => 'Asia/Tehran', + 'Abu Dhabi, Muscat' => 'Asia/Muscat', // Best guess + 'Baku, Tbilisi, Yerevan' => 'Asia/Baku', + 'Kabul' => 'Asia/Kabul', + 'Ekaterinburg' => 'Asia/Yekaterinburg', + 'Islamabad, Karachi, Tashkent' => 'Asia/Karachi', + 'Kolkata, Chennai, Mumbai, New Delhi, India Standard Time' => 'Asia/Calcutta', + 'Kathmandu, Nepal' => 'Asia/Kathmandu', + 'Almaty, Novosibirsk, North Central Asia' => 'Asia/Almaty', + 'Astana, Dhaka' => 'Asia/Dhaka', + 'Sri Jayawardenepura, Sri Lanka' => 'Asia/Colombo', + 'Rangoon' => 'Asia/Rangoon', + 'Bangkok, Hanoi, Jakarta' => 'Asia/Bangkok', + 'Krasnoyarsk' => 'Asia/Krasnoyarsk', + 'Beijing, Chongqing, Hong Kong SAR, Urumqi' => 'Asia/Shanghai', + 'Irkutsk, Ulaan Bataar' => 'Asia/Irkutsk', + 'Kuala Lumpur, Singapore' => 'Asia/Singapore', + 'Perth, Western Australia' => 'Australia/Perth', + 'Taipei' => 'Asia/Taipei', + 'Osaka, Sapporo, Tokyo' => 'Asia/Tokyo', + 'Seoul, Korea Standard time' => 'Asia/Seoul', + 'Yakutsk' => 'Asia/Yakutsk', + 'Adelaide, Central Australia' => 'Australia/Adelaide', + 'Darwin' => 'Australia/Darwin', + 'Brisbane, East Australia' => 'Australia/Brisbane', + 'Canberra, Melbourne, Sydney, Hobart (year 2000 only)' => 'Australia/Sydney', + 'Guam, Port Moresby' => 'Pacific/Guam', + 'Hobart, Tasmania' => 'Australia/Hobart', + 'Vladivostok' => 'Asia/Vladivostok', + 'Magadan, Solomon Is., New Caledonia' => 'Asia/Magadan', + 'Auckland, Wellington' => 'Pacific/Auckland', + 'Fiji Islands, Kamchatka, Marshall Is.' => 'Pacific/Fiji', + 'Nuku\'alofa, Tonga' => 'Pacific/Tongatapu', + 'Azores' => 'Atlantic/Azores', + 'Cape Verde Is.' => 'Atlantic/Cape_Verde', + 'Mid-Atlantic' => 'America/Noronha', + 'Brasilia' => 'America/Sao_Paulo', // Best guess + 'Buenos Aires' => 'America/Argentina/Buenos_Aires', + 'Greenland' => 'America/Godthab', + 'Newfoundland' => 'America/St_Johns', + 'Atlantic Time (Canada)' => 'America/Halifax', + 'Caracas, La Paz' => 'America/Caracas', + 'Santiago' => 'America/Santiago', + 'Bogota, Lima, Quito' => 'America/Bogota', + 'Eastern Time (US & Canada)' => 'America/New_York', + 'Indiana (East)' => 'America/Indiana/Indianapolis', + 'Central America' => 'America/Guatemala', + 'Central Time (US & Canada)' => 'America/Chicago', + 'Mexico City, Tegucigalpa' => 'America/Mexico_City', + 'Saskatchewan' => 'America/Edmonton', + 'Arizona' => 'America/Phoenix', + 'Mountain Time (US & Canada)' => 'America/Denver', // Best guess + 'Pacific Time (US & Canada)' => 'America/Los_Angeles', // Best guess + 'Pacific Time (US & Canada); Tijuana' => 'America/Los_Angeles', // Best guess + 'Alaska' => 'America/Anchorage', + 'Hawaii' => 'Pacific/Honolulu', + 'Midway Island, Samoa' => 'Pacific/Midway', + 'Eniwetok, Kwajalein, Dateline Time' => 'Pacific/Kwajalein', +]; diff --git a/lib/vendor/sabre/vobject/lib/timezonedata/lotuszones.php b/lib/vendor/sabre/vobject/lib/timezonedata/lotuszones.php new file mode 100644 index 0000000..4b50808 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/timezonedata/lotuszones.php @@ -0,0 +1,101 @@ + 'Etc/GMT-12', + 'Samoa' => 'Pacific/Apia', + 'Hawaiian' => 'Pacific/Honolulu', + 'Alaskan' => 'America/Anchorage', + 'Pacific' => 'America/Los_Angeles', + 'Pacific Standard Time' => 'America/Los_Angeles', + 'Mexico Standard Time 2' => 'America/Chihuahua', + 'Mountain' => 'America/Denver', + // 'Mountain Standard Time' => 'America/Chihuahua', // conflict with windows timezones. + 'US Mountain' => 'America/Phoenix', + 'Canada Central' => 'America/Edmonton', + 'Central America' => 'America/Guatemala', + 'Central' => 'America/Chicago', + // 'Central Standard Time' => 'America/Mexico_City', // conflict with windows timezones. + 'Mexico' => 'America/Mexico_City', + 'Eastern' => 'America/New_York', + 'SA Pacific' => 'America/Bogota', + 'US Eastern' => 'America/Indiana/Indianapolis', + 'Venezuela' => 'America/Caracas', + 'Atlantic' => 'America/Halifax', + 'Central Brazilian' => 'America/Manaus', + 'Pacific SA' => 'America/Santiago', + 'SA Western' => 'America/La_Paz', + 'Newfoundland' => 'America/St_Johns', + 'Argentina' => 'America/Argentina/Buenos_Aires', + 'E. South America' => 'America/Belem', + 'Greenland' => 'America/Godthab', + 'Montevideo' => 'America/Montevideo', + 'SA Eastern' => 'America/Belem', + // 'Mid-Atlantic' => 'Etc/GMT-2', // conflict with windows timezones. + 'Azores' => 'Atlantic/Azores', + 'Cape Verde' => 'Atlantic/Cape_Verde', + 'Greenwich' => 'Atlantic/Reykjavik', // No I'm serious.. Greenwich is not GMT. + 'Morocco' => 'Africa/Casablanca', + 'Central Europe' => 'Europe/Prague', + 'Central European' => 'Europe/Sarajevo', + 'Romance' => 'Europe/Paris', + 'W. Central Africa' => 'Africa/Lagos', // Best guess + 'W. Europe' => 'Europe/Amsterdam', + 'E. Europe' => 'Europe/Minsk', + 'Egypt' => 'Africa/Cairo', + 'FLE' => 'Europe/Helsinki', + 'GTB' => 'Europe/Athens', + 'Israel' => 'Asia/Jerusalem', + 'Jordan' => 'Asia/Amman', + 'Middle East' => 'Asia/Beirut', + 'Namibia' => 'Africa/Windhoek', + 'South Africa' => 'Africa/Harare', + 'Arab' => 'Asia/Kuwait', + 'Arabic' => 'Asia/Baghdad', + 'E. Africa' => 'Africa/Nairobi', + 'Georgian' => 'Asia/Tbilisi', + 'Russian' => 'Europe/Moscow', + 'Iran' => 'Asia/Tehran', + 'Arabian' => 'Asia/Muscat', + 'Armenian' => 'Asia/Yerevan', + 'Azerbijan' => 'Asia/Baku', + 'Caucasus' => 'Asia/Yerevan', + 'Mauritius' => 'Indian/Mauritius', + 'Afghanistan' => 'Asia/Kabul', + 'Ekaterinburg' => 'Asia/Yekaterinburg', + 'Pakistan' => 'Asia/Karachi', + 'West Asia' => 'Asia/Tashkent', + 'India' => 'Asia/Calcutta', + 'Sri Lanka' => 'Asia/Colombo', + 'Nepal' => 'Asia/Kathmandu', + 'Central Asia' => 'Asia/Dhaka', + 'N. Central Asia' => 'Asia/Almaty', + 'Myanmar' => 'Asia/Rangoon', + 'North Asia' => 'Asia/Krasnoyarsk', + 'SE Asia' => 'Asia/Bangkok', + 'China' => 'Asia/Shanghai', + 'North Asia East' => 'Asia/Irkutsk', + 'Singapore' => 'Asia/Singapore', + 'Taipei' => 'Asia/Taipei', + 'W. Australia' => 'Australia/Perth', + 'Korea' => 'Asia/Seoul', + 'Tokyo' => 'Asia/Tokyo', + 'Yakutsk' => 'Asia/Yakutsk', + 'AUS Central' => 'Australia/Darwin', + 'Cen. Australia' => 'Australia/Adelaide', + 'AUS Eastern' => 'Australia/Sydney', + 'E. Australia' => 'Australia/Brisbane', + 'Tasmania' => 'Australia/Hobart', + 'Vladivostok' => 'Asia/Vladivostok', + 'West Pacific' => 'Pacific/Guam', + 'Central Pacific' => 'Asia/Magadan', + 'Fiji' => 'Pacific/Fiji', + 'New Zealand' => 'Pacific/Auckland', + 'Tonga' => 'Pacific/Tongatapu', +]; diff --git a/lib/vendor/sabre/vobject/lib/timezonedata/php-bc.php b/lib/vendor/sabre/vobject/lib/timezonedata/php-bc.php new file mode 100644 index 0000000..3116c68 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/timezonedata/php-bc.php @@ -0,0 +1,152 @@ + 'America/Chicago', + 'Cuba' => 'America/Havana', + 'Egypt' => 'Africa/Cairo', + 'Eire' => 'Europe/Dublin', + 'EST5EDT' => 'America/New_York', + 'Factory' => 'UTC', + 'GB-Eire' => 'Europe/London', + 'GMT0' => 'UTC', + 'Greenwich' => 'UTC', + 'Hongkong' => 'Asia/Hong_Kong', + 'Iceland' => 'Atlantic/Reykjavik', + 'Iran' => 'Asia/Tehran', + 'Israel' => 'Asia/Jerusalem', + 'Jamaica' => 'America/Jamaica', + 'Japan' => 'Asia/Tokyo', + 'Kwajalein' => 'Pacific/Kwajalein', + 'Libya' => 'Africa/Tripoli', + 'MST7MDT' => 'America/Denver', + 'Navajo' => 'America/Denver', + 'NZ-CHAT' => 'Pacific/Chatham', + 'Poland' => 'Europe/Warsaw', + 'Portugal' => 'Europe/Lisbon', + 'PST8PDT' => 'America/Los_Angeles', + 'Singapore' => 'Asia/Singapore', + 'Turkey' => 'Europe/Istanbul', + 'Universal' => 'UTC', + 'W-SU' => 'Europe/Moscow', + 'Zulu' => 'UTC', +]; diff --git a/lib/vendor/sabre/vobject/lib/timezonedata/windowszones.php b/lib/vendor/sabre/vobject/lib/timezonedata/windowszones.php new file mode 100644 index 0000000..2049a95 --- /dev/null +++ b/lib/vendor/sabre/vobject/lib/timezonedata/windowszones.php @@ -0,0 +1,152 @@ + 'Australia/Darwin', + 'AUS Eastern Standard Time' => 'Australia/Sydney', + 'Afghanistan Standard Time' => 'Asia/Kabul', + 'Alaskan Standard Time' => 'America/Anchorage', + 'Aleutian Standard Time' => 'America/Adak', + 'Altai Standard Time' => 'Asia/Barnaul', + 'Arab Standard Time' => 'Asia/Riyadh', + 'Arabian Standard Time' => 'Asia/Dubai', + 'Arabic Standard Time' => 'Asia/Baghdad', + 'Argentina Standard Time' => 'America/Buenos_Aires', + 'Astrakhan Standard Time' => 'Europe/Astrakhan', + 'Atlantic Standard Time' => 'America/Halifax', + 'Aus Central W. Standard Time' => 'Australia/Eucla', + 'Azerbaijan Standard Time' => 'Asia/Baku', + 'Azores Standard Time' => 'Atlantic/Azores', + 'Bahia Standard Time' => 'America/Bahia', + 'Bangladesh Standard Time' => 'Asia/Dhaka', + 'Belarus Standard Time' => 'Europe/Minsk', + 'Bougainville Standard Time' => 'Pacific/Bougainville', + 'Canada Central Standard Time' => 'America/Regina', + 'Cape Verde Standard Time' => 'Atlantic/Cape_Verde', + 'Caucasus Standard Time' => 'Asia/Yerevan', + 'Cen. Australia Standard Time' => 'Australia/Adelaide', + 'Central America Standard Time' => 'America/Guatemala', + 'Central Asia Standard Time' => 'Asia/Almaty', + 'Central Brazilian Standard Time' => 'America/Cuiaba', + 'Central Europe Standard Time' => 'Europe/Budapest', + 'Central European Standard Time' => 'Europe/Warsaw', + 'Central Pacific Standard Time' => 'Pacific/Guadalcanal', + 'Central Standard Time' => 'America/Chicago', + 'Central Standard Time (Mexico)' => 'America/Mexico_City', + 'Chatham Islands Standard Time' => 'Pacific/Chatham', + 'China Standard Time' => 'Asia/Shanghai', + 'Cuba Standard Time' => 'America/Havana', + 'Dateline Standard Time' => 'Etc/GMT+12', + 'E. Africa Standard Time' => 'Africa/Nairobi', + 'E. Australia Standard Time' => 'Australia/Brisbane', + 'E. Europe Standard Time' => 'Europe/Chisinau', + 'E. South America Standard Time' => 'America/Sao_Paulo', + 'Easter Island Standard Time' => 'Pacific/Easter', + 'Eastern Standard Time' => 'America/New_York', + 'Eastern Standard Time (Mexico)' => 'America/Cancun', + 'Egypt Standard Time' => 'Africa/Cairo', + 'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg', + 'FLE Standard Time' => 'Europe/Kiev', + 'Fiji Standard Time' => 'Pacific/Fiji', + 'GMT Standard Time' => 'Europe/London', + 'GTB Standard Time' => 'Europe/Bucharest', + 'Georgian Standard Time' => 'Asia/Tbilisi', + 'Greenland Standard Time' => 'America/Godthab', + 'Greenwich Standard Time' => 'Atlantic/Reykjavik', + 'Haiti Standard Time' => 'America/Port-au-Prince', + 'Hawaiian Standard Time' => 'Pacific/Honolulu', + 'India Standard Time' => 'Asia/Calcutta', + 'Iran Standard Time' => 'Asia/Tehran', + 'Israel Standard Time' => 'Asia/Jerusalem', + 'Jordan Standard Time' => 'Asia/Amman', + 'Kaliningrad Standard Time' => 'Europe/Kaliningrad', + 'Korea Standard Time' => 'Asia/Seoul', + 'Libya Standard Time' => 'Africa/Tripoli', + 'Line Islands Standard Time' => 'Pacific/Kiritimati', + 'Lord Howe Standard Time' => 'Australia/Lord_Howe', + 'Magadan Standard Time' => 'Asia/Magadan', + 'Magallanes Standard Time' => 'America/Punta_Arenas', + 'Marquesas Standard Time' => 'Pacific/Marquesas', + 'Mauritius Standard Time' => 'Indian/Mauritius', + 'Middle East Standard Time' => 'Asia/Beirut', + 'Montevideo Standard Time' => 'America/Montevideo', + 'Morocco Standard Time' => 'Africa/Casablanca', + 'Mountain Standard Time' => 'America/Denver', + 'Mountain Standard Time (Mexico)' => 'America/Chihuahua', + 'Myanmar Standard Time' => 'Asia/Rangoon', + 'N. Central Asia Standard Time' => 'Asia/Novosibirsk', + 'Namibia Standard Time' => 'Africa/Windhoek', + 'Nepal Standard Time' => 'Asia/Katmandu', + 'New Zealand Standard Time' => 'Pacific/Auckland', + 'Newfoundland Standard Time' => 'America/St_Johns', + 'Norfolk Standard Time' => 'Pacific/Norfolk', + 'North Asia East Standard Time' => 'Asia/Irkutsk', + 'North Asia Standard Time' => 'Asia/Krasnoyarsk', + 'North Korea Standard Time' => 'Asia/Pyongyang', + 'Omsk Standard Time' => 'Asia/Omsk', + 'Pacific SA Standard Time' => 'America/Santiago', + 'Pacific Standard Time' => 'America/Los_Angeles', + 'Pacific Standard Time (Mexico)' => 'America/Tijuana', + 'Pakistan Standard Time' => 'Asia/Karachi', + 'Paraguay Standard Time' => 'America/Asuncion', + 'Qyzylorda Standard Time' => 'Asia/Qyzylorda', + 'Romance Standard Time' => 'Europe/Paris', + 'Russia Time Zone 10' => 'Asia/Srednekolymsk', + 'Russia Time Zone 11' => 'Asia/Kamchatka', + 'Russia Time Zone 3' => 'Europe/Samara', + 'Russian Standard Time' => 'Europe/Moscow', + 'SA Eastern Standard Time' => 'America/Cayenne', + 'SA Pacific Standard Time' => 'America/Bogota', + 'SA Western Standard Time' => 'America/La_Paz', + 'SE Asia Standard Time' => 'Asia/Bangkok', + 'Saint Pierre Standard Time' => 'America/Miquelon', + 'Sakhalin Standard Time' => 'Asia/Sakhalin', + 'Samoa Standard Time' => 'Pacific/Apia', + 'Sao Tome Standard Time' => 'Africa/Sao_Tome', + 'Saratov Standard Time' => 'Europe/Saratov', + 'Singapore Standard Time' => 'Asia/Singapore', + 'South Africa Standard Time' => 'Africa/Johannesburg', + 'Sri Lanka Standard Time' => 'Asia/Colombo', + 'Sudan Standard Time' => 'Africa/Khartoum', + 'Syria Standard Time' => 'Asia/Damascus', + 'Taipei Standard Time' => 'Asia/Taipei', + 'Tasmania Standard Time' => 'Australia/Hobart', + 'Tocantins Standard Time' => 'America/Araguaina', + 'Tokyo Standard Time' => 'Asia/Tokyo', + 'Tomsk Standard Time' => 'Asia/Tomsk', + 'Tonga Standard Time' => 'Pacific/Tongatapu', + 'Transbaikal Standard Time' => 'Asia/Chita', + 'Turkey Standard Time' => 'Europe/Istanbul', + 'Turks And Caicos Standard Time' => 'America/Grand_Turk', + 'US Eastern Standard Time' => 'America/Indianapolis', + 'US Mountain Standard Time' => 'America/Phoenix', + 'UTC' => 'Etc/GMT', + 'UTC+12' => 'Etc/GMT-12', + 'UTC+13' => 'Etc/GMT-13', + 'UTC-02' => 'Etc/GMT+2', + 'UTC-08' => 'Etc/GMT+8', + 'UTC-09' => 'Etc/GMT+9', + 'UTC-11' => 'Etc/GMT+11', + 'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar', + 'Venezuela Standard Time' => 'America/Caracas', + 'Vladivostok Standard Time' => 'Asia/Vladivostok', + 'Volgograd Standard Time' => 'Europe/Volgograd', + 'W. Australia Standard Time' => 'Australia/Perth', + 'W. Central Africa Standard Time' => 'Africa/Lagos', + 'W. Europe Standard Time' => 'Europe/Berlin', + 'W. Mongolia Standard Time' => 'Asia/Hovd', + 'West Asia Standard Time' => 'Asia/Tashkent', + 'West Bank Standard Time' => 'Asia/Hebron', + 'West Pacific Standard Time' => 'Pacific/Port_Moresby', + 'Yakutsk Standard Time' => 'Asia/Yakutsk', + 'Yukon Standard Time' => 'America/Whitehorse', +]; diff --git a/lib/vendor/sabre/vobject/resources/schema/xcal.rng b/lib/vendor/sabre/vobject/resources/schema/xcal.rng new file mode 100644 index 0000000..4a51460 --- /dev/null +++ b/lib/vendor/sabre/vobject/resources/schema/xcal.rng @@ -0,0 +1,1192 @@ +# RELAX NG Schema for iCalendar in XML +# Extract from RFC6321. +# Erratum 3042 applied. +# Erratum 3050 applied. +# Erratum 3314 applied. + +default namespace = "urn:ietf:params:xml:ns:icalendar-2.0" + +# 3.2 Property Parameters + +# 3.2.1 Alternate Text Representation + +altrepparam = element altrep { + value-uri +} + +# 3.2.2 Common Name + +cnparam = element cn { + value-text +} + +# 3.2.3 Calendar User Type + +cutypeparam = element cutype { + element text { + "INDIVIDUAL" | + "GROUP" | + "RESOURCE" | + "ROOM" | + "UNKNOWN" + } +} + +# 3.2.4 Delegators + +delfromparam = element delegated-from { + value-cal-address+ +} + +# 3.2.5 Delegatees + +deltoparam = element delegated-to { + value-cal-address+ +} + +# 3.2.6 Directory Entry Reference + +dirparam = element dir { + value-uri +} + +# 3.2.7 Inline Encoding + +encodingparam = element encoding { + element text { + "8BIT" | + "BASE64" + } +} + +# 3.2.8 Format Type + +fmttypeparam = element fmttype { + value-text +} + +# 3.2.9 Free/Busy Time Type + +fbtypeparam = element fbtype { + element text { + "FREE" | + "BUSY" | + "BUSY-UNAVAILABLE" | + "BUSY-TENTATIVE" + } +} + +# 3.2.10 Language + +languageparam = element language { + value-text +} + +# 3.2.11 Group or List Membership + +memberparam = element member { + value-cal-address+ +} + +# 3.2.12 Participation Status + +partstatparam = element partstat { + type-partstat-event | + type-partstat-todo | + type-partstat-jour +} + +type-partstat-event = ( + element text { + "NEEDS-ACTION" | + "ACCEPTED" | + "DECLINED" | + "TENTATIVE" | + "DELEGATED" + } +) + +type-partstat-todo = ( + element text { + "NEEDS-ACTION" | + "ACCEPTED" | + "DECLINED" | + "TENTATIVE" | + "DELEGATED" | + "COMPLETED" | + "IN-PROCESS" + } +) + +type-partstat-jour = ( + element text { + "NEEDS-ACTION" | + "ACCEPTED" | + "DECLINED" + } +) + +# 3.2.13 Recurrence Identifier Range + +rangeparam = element range { + element text { + "THISANDFUTURE" + } +} + +# 3.2.14 Alarm Trigger Relationship + +trigrelparam = element related { + element text { + "START" | + "END" + } +} + +# 3.2.15 Relationship Type + +reltypeparam = element reltype { + element text { + "PARENT" | + "CHILD" | + "SIBLING" + } +} + +# 3.2.16 Participation Role + +roleparam = element role { + element text { + "CHAIR" | + "REQ-PARTICIPANT" | + "OPT-PARTICIPANT" | + "NON-PARTICIPANT" + } +} + +# 3.2.17 RSVP Expectation + +rsvpparam = element rsvp { + value-boolean +} + +# 3.2.18 Sent By + +sentbyparam = element sent-by { + value-cal-address +} + +# 3.2.19 Time Zone Identifier + +tzidparam = element tzid { + value-text +} + +# 3.3 Property Value Data Types + +# 3.3.1 BINARY + +value-binary = element binary { + xsd:string +} + +# 3.3.2 BOOLEAN + +value-boolean = element boolean { + xsd:boolean +} + +# 3.3.3 CAL-ADDRESS + +value-cal-address = element cal-address { + xsd:anyURI +} + +# 3.3.4 DATE + +pattern-date = xsd:string { + pattern = "\d\d\d\d-\d\d-\d\d" +} + +value-date = element date { + pattern-date +} + +# 3.3.5 DATE-TIME + +pattern-date-time = xsd:string { + pattern = "\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ?" +} + +value-date-time = element date-time { + pattern-date-time +} + +# 3.3.6 DURATION + +pattern-duration = xsd:string { + pattern = "(+|-)?P(\d+W)|(\d+D)?" + ~ "(T(\d+H(\d+M)?(\d+S)?)|" + ~ "(\d+M(\d+S)?)|" + ~ "(\d+S))?" +} + +value-duration = element duration { + pattern-duration +} + +# 3.3.7 FLOAT + +value-float = element float { + xsd:float +} + +# 3.3.8 INTEGER + +value-integer = element integer { + xsd:integer +} + +# 3.3.9 PERIOD + +value-period = element period { + element start { + pattern-date-time + }, + ( + element end { + pattern-date-time + } | + element duration { + pattern-duration + } + ) +} + +# 3.3.10 RECUR + +value-recur = element recur { + type-freq, + (type-until | type-count)?, + element interval { + xsd:positiveInteger + }?, + type-bysecond*, + type-byminute*, + type-byhour*, + type-byday*, + type-bymonthday*, + type-byyearday*, + type-byweekno*, + type-bymonth*, + type-bysetpos*, + element wkst { type-weekday }? +} + +type-freq = element freq { + "SECONDLY" | + "MINUTELY" | + "HOURLY" | + "DAILY" | + "WEEKLY" | + "MONTHLY" | + "YEARLY" +} + +type-until = element until { + type-date | + type-date-time +} + +type-count = element count { + xsd:positiveInteger +} + +type-bysecond = element bysecond { + xsd:nonNegativeInteger +} + +type-byminute = element byminute { + xsd:nonNegativeInteger +} + +type-byhour = element byhour { + xsd:nonNegativeInteger +} + +type-weekday = ( + "SU" | + "MO" | + "TU" | + "WE" | + "TH" | + "FR" | + "SA" +) + +type-byday = element byday { + xsd:integer?, + type-weekday +} + +type-bymonthday = element bymonthday { + xsd:integer +} + +type-byyearday = element byyearday { + xsd:integer +} + +type-byweekno = element byweekno { + xsd:integer +} + +type-bymonth = element bymonth { + xsd:positiveInteger +} + +type-bysetpos = element bysetpos { + xsd:integer +} + +# 3.3.11 TEXT + +value-text = element text { + xsd:string +} + +# 3.3.12 TIME + +pattern-time = xsd:string { + pattern = "\d\d:\d\d:\d\dZ?" +} + +value-time = element time { + pattern-time +} + +# 3.3.13 URI + +value-uri = element uri { + xsd:anyURI +} + +# 3.3.14 UTC-OFFSET + +value-utc-offset = element utc-offset { + xsd:string { pattern = "(+|-)\d\d:\d\d(:\d\d)?" } +} + +# UNKNOWN + +value-unknown = element unknown { + xsd:string +} + +# 3.4 iCalendar Stream + +start = element icalendar { + vcalendar+ +} + +# 3.6 Calendar Components + +vcalendar = element vcalendar { + type-calprops, + type-component +} + +type-calprops = element properties { + property-prodid & + property-version & + property-calscale? & + property-method? +} + +type-component = element components { + ( + component-vevent | + component-vtodo | + component-vjournal | + component-vfreebusy | + component-vtimezone + )* +} + +# 3.6.1 Event Component + +component-vevent = element vevent { + type-eventprop, + element components { + component-valarm+ + }? +} + +type-eventprop = element properties { + property-dtstamp & + property-dtstart & + property-uid & + + property-class? & + property-created? & + property-description? & + property-geo? & + property-last-mod? & + property-location? & + property-organizer? & + property-priority? & + property-seq? & + property-status-event? & + property-summary? & + property-transp? & + property-url? & + property-recurid? & + + property-rrule? & + + (property-dtend | property-duration)? & + + property-attach* & + property-attendee* & + property-categories* & + property-comment* & + property-contact* & + property-exdate* & + property-rstatus* & + property-related* & + property-resources* & + property-rdate* +} + +# 3.6.2 To-do Component + +component-vtodo = element vtodo { + type-todoprop, + element components { + component-valarm+ + }? +} + +type-todoprop = element properties { + property-dtstamp & + property-uid & + + property-class? & + property-completed? & + property-created? & + property-description? & + property-geo? & + property-last-mod? & + property-location? & + property-organizer? & + property-percent? & + property-priority? & + property-recurid? & + property-seq? & + property-status-todo? & + property-summary? & + property-url? & + + property-rrule? & + + ( + (property-dtstart?, property-dtend? ) | + (property-dtstart, property-duration)? + ) & + + property-attach* & + property-attendee* & + property-categories* & + property-comment* & + property-contact* & + property-exdate* & + property-rstatus* & + property-related* & + property-resources* & + property-rdate* +} + +# 3.6.3 Journal Component + +component-vjournal = element vjournal { + type-jourprop +} + +type-jourprop = element properties { + property-dtstamp & + property-uid & + + property-class? & + property-created? & + property-dtstart? & + property-last-mod? & + property-organizer? & + property-recurid? & + property-seq? & + property-status-jour? & + property-summary? & + property-url? & + + property-rrule? & + + property-attach* & + property-attendee* & + property-categories* & + property-comment* & + property-contact* & + property-description? & + property-exdate* & + property-related* & + property-rdate* & + property-rstatus* +} + +# 3.6.4 Free/Busy Component + +component-vfreebusy = element vfreebusy { + type-fbprop +} + +type-fbprop = element properties { + property-dtstamp & + property-uid & + + property-contact? & + property-dtstart? & + property-dtend? & + property-duration? & + property-organizer? & + property-url? & + + property-attendee* & + property-comment* & + property-freebusy* & + property-rstatus* +} + +# 3.6.5 Time Zone Component + +component-vtimezone = element vtimezone { + element properties { + property-tzid & + + property-last-mod? & + property-tzurl? + }, + element components { + (component-standard | component-daylight) & + component-standard* & + component-daylight* + } +} + +component-standard = element standard { + type-tzprop +} + +component-daylight = element daylight { + type-tzprop +} + +type-tzprop = element properties { + property-dtstart & + property-tzoffsetto & + property-tzoffsetfrom & + + property-rrule? & + + property-comment* & + property-rdate* & + property-tzname* +} + +# 3.6.6 Alarm Component + +component-valarm = element valarm { + type-audioprop | type-dispprop | type-emailprop +} + +type-audioprop = element properties { + property-action & + + property-trigger & + + (property-duration, property-repeat)? & + + property-attach? +} + +type-emailprop = element properties { + property-action & + property-description & + property-trigger & + property-summary & + + property-attendee+ & + + (property-duration, property-repeat)? & + + property-attach* +} + +type-dispprop = element properties { + property-action & + property-description & + property-trigger & + + (property-duration, property-repeat)? +} + +# 3.7 Calendar Properties + +# 3.7.1 Calendar Scale + +property-calscale = element calscale { + + element parameters { empty }?, + + element text { "GREGORIAN" } +} + +# 3.7.2 Method + +property-method = element method { + + element parameters { empty }?, + + value-text +} + +# 3.7.3 Product Identifier + +property-prodid = element prodid { + + element parameters { empty }?, + + value-text +} + +# 3.7.4 Version + +property-version = element version { + + element parameters { empty }?, + + element text { "2.0" } +} + +# 3.8 Component Properties + +# 3.8.1 Descriptive Component Properties + +# 3.8.1.1 Attachment + +property-attach = element attach { + + element parameters { + fmttypeparam? & + encodingparam? + }?, + + value-uri | value-binary +} + +# 3.8.1.2 Categories + +property-categories = element categories { + + element parameters { + languageparam? & + }?, + + value-text+ +} + +# 3.8.1.3 Classification + +property-class = element class { + + element parameters { empty }?, + + element text { + "PUBLIC" | + "PRIVATE" | + "CONFIDENTIAL" + } +} + +# 3.8.1.4 Comment + +property-comment = element comment { + + element parameters { + altrepparam? & + languageparam? + }?, + + value-text +} + +# 3.8.1.5 Description + +property-description = element description { + + element parameters { + altrepparam? & + languageparam? + }?, + + value-text +} + +# 3.8.1.6 Geographic Position + +property-geo = element geo { + + element parameters { empty }?, + + element latitude { xsd:float }, + element longitude { xsd:float } +} + +# 3.8.1.7 Location + +property-location = element location { + + element parameters { + + altrepparam? & + languageparam? + }?, + + value-text +} + +# 3.8.1.8 Percent Complete + +property-percent = element percent-complete { + + element parameters { empty }?, + + value-integer +} + +# 3.8.1.9 Priority + +property-priority = element priority { + + element parameters { empty }?, + + value-integer +} + +# 3.8.1.10 Resources + +property-resources = element resources { + + element parameters { + altrepparam? & + languageparam? + }?, + + value-text+ +} + +# 3.8.1.11 Status + +property-status-event = element status { + + element parameters { empty }?, + + element text { + "TENTATIVE" | + "CONFIRMED" | + "CANCELLED" + } +} + +property-status-todo = element status { + + element parameters { empty }?, + + element text { + "NEEDS-ACTION" | + "COMPLETED" | + "IN-PROCESS" | + "CANCELLED" + } +} + +property-status-jour = element status { + + element parameters { empty }?, + + element text { + "DRAFT" | + "FINAL" | + "CANCELLED" + } +} + +# 3.8.1.12 Summary + +property-summary = element summary { + + element parameters { + altrepparam? & + languageparam? + }?, + + value-text +} + +# 3.8.2 Date and Time Component Properties + +# 3.8.2.1 Date/Time Completed + +property-completed = element completed { + + element parameters { empty }?, + + value-date-time +} + +# 3.8.2.2 Date/Time End + +property-dtend = element dtend { + + element parameters { + tzidparam? + }?, + + value-date-time | + value-date +} + +# 3.8.2.3 Date/Time Due + +property-due = element due { + + element parameters { + tzidparam? + }?, + + value-date-time | + value-date +} + +# 3.8.2.4 Date/Time Start + +property-dtstart = element dtstart { + + element parameters { + tzidparam? + }?, + + value-date-time | + value-date +} + +# 3.8.2.5 Duration + +property-duration = element duration { + + element parameters { empty }?, + + value-duration +} + +# 3.8.2.6 Free/Busy Time + +property-freebusy = element freebusy { + + element parameters { + fbtypeparam? + }?, + + + value-period+ +} + +# 3.8.2.7 Time Transparency + +property-transp = element transp { + + element parameters { empty }?, + + element text { + "OPAQUE" | + "TRANSPARENT" + } +} + +# 3.8.3 Time Zone Component Properties + +# 3.8.3.1 Time Zone Identifier + +property-tzid = element tzid { + + element parameters { empty }?, + + value-text +} + +# 3.8.3.2 Time Zone Name + +property-tzname = element tzname { + + element parameters { + languageparam? + }?, + + value-text +} + +# 3.8.3.3 Time Zone Offset From + +property-tzoffsetfrom = element tzoffsetfrom { + + element parameters { empty }?, + + value-utc-offset +} + +# 3.8.3.4 Time Zone Offset To + +property-tzoffsetto = element tzoffsetto { + + element parameters { empty }?, + + value-utc-offset +} + +# 3.8.3.5 Time Zone URL + +property-tzurl = element tzurl { + + element parameters { empty }?, + + value-uri +} + +# 3.8.4 Relationship Component Properties + +# 3.8.4.1 Attendee + +property-attendee = element attendee { + + element parameters { + cutypeparam? & + memberparam? & + roleparam? & + partstatparam? & + rsvpparam? & + deltoparam? & + delfromparam? & + sentbyparam? & + cnparam? & + dirparam? & + languageparam? + }?, + + value-cal-address +} + +# 3.8.4.2 Contact + +property-contact = element contact { + + element parameters { + altrepparam? & + languageparam? + }?, + + value-text +} + +# 3.8.4.3 Organizer + +property-organizer = element organizer { + + element parameters { + cnparam? & + dirparam? & + sentbyparam? & + languageparam? + }?, + + value-cal-address +} + +# 3.8.4.4 Recurrence ID + +property-recurid = element recurrence-id { + + element parameters { + tzidparam? & + rangeparam? + }?, + + value-date-time | + value-date +} + +# 3.8.4.5 Related-To + +property-related = element related-to { + + element parameters { + reltypeparam? + }?, + + value-text +} + +# 3.8.4.6 Uniform Resource Locator + +property-url = element url { + + element parameters { empty }?, + + value-uri +} + +# 3.8.4.7 Unique Identifier + +property-uid = element uid { + + element parameters { empty }?, + + value-text +} + +# 3.8.5 Recurrence Component Properties + +# 3.8.5.1 Exception Date/Times + +property-exdate = element exdate { + + element parameters { + tzidparam? + }?, + + value-date-time+ | + value-date+ +} + +# 3.8.5.2 Recurrence Date/Times + +property-rdate = element rdate { + + element parameters { + tzidparam? + }?, + + value-date-time+ | + value-date+ | + value-period+ +} + +# 3.8.5.3 Recurrence Rule + +property-rrule = element rrule { + + element parameters { empty }?, + + value-recur +} + +# 3.8.6 Alarm Component Properties + +# 3.8.6.1 Action + +property-action = element action { + + element parameters { empty }?, + + element text { + "AUDIO" | + "DISPLAY" | + "EMAIL" + } +} + +# 3.8.6.2 Repeat Count + +property-repeat = element repeat { + + element parameters { empty }?, + + value-integer +} + +# 3.8.6.3 Trigger + +property-trigger = element trigger { + + ( + element parameters { + trigrelparam? + }?, + + value-duration + ) | + ( + element parameters { empty }?, + + value-date-time + ) +} + +# 3.8.7 Change Management Component Properties + +# 3.8.7.1 Date/Time Created + +property-created = element created { + + element parameters { empty }?, + + value-date-time +} + +# 3.8.7.2 Date/Time Stamp + +property-dtstamp = element dtstamp { + + element parameters { empty }?, + + value-date-time +} + +# 3.8.7.3 Last Modified + +property-last-mod = element last-modified { + + element parameters { empty }?, + + value-date-time +} + +# 3.8.7.4 Sequence Number + +property-seq = element sequence { + + element parameters { empty }?, + + value-integer +} + +# 3.8.8 Miscellaneous Component Properties + +# 3.8.8.3 Request Status + +property-rstatus = element request-status { + + element parameters { + languageparam? + }?, + + element code { xsd:string }, + element description { xsd:string }, + element data { xsd:string }? +} diff --git a/lib/vendor/sabre/vobject/resources/schema/xcard.rng b/lib/vendor/sabre/vobject/resources/schema/xcard.rng new file mode 100644 index 0000000..c0b7cfb --- /dev/null +++ b/lib/vendor/sabre/vobject/resources/schema/xcard.rng @@ -0,0 +1,388 @@ +# RELAX NG Schema for vCard in XML +# Extract from RFC6351. +# Erratum 2994 applied. +# Erratum 3047 applied. +# Erratum 3008 applied. +# Erratum 4247 applied. + +default namespace = "urn:ietf:params:xml:ns:vcard-4.0" + +### Section 3.3: vCard Format Specification +# +# 3.3 +iana-token = xsd:string { pattern = "[a-zA-Z0-9\-]+" } +x-name = xsd:string { pattern = "x-[a-zA-Z0-9\-]+" } + +### Section 4: Value types +# +# 4.1 +value-text = element text { text } +value-text-list = value-text+ + +# 4.2 +value-uri = element uri { xsd:anyURI } + +# 4.3.1 +value-date = element date { + xsd:string { pattern = "\d{8}|\d{4}-\d\d|--\d\d(\d\d)?|---\d\d" } + } + +# 4.3.2 +value-time = element time { + xsd:string { pattern = "(\d\d(\d\d(\d\d)?)?|-\d\d(\d\d)?|--\d\d)" + ~ "(Z|[+\-]\d\d(\d\d)?)?" } + } + +# 4.3.3 +value-date-time = element date-time { + xsd:string { pattern = "(\d{8}|--\d{4}|---\d\d)T\d\d(\d\d(\d\d)?)?" + ~ "(Z|[+\-]\d\d(\d\d)?)?" } + } + +# 4.3.4 +value-date-and-or-time = value-date | value-date-time | value-time + +# 4.3.5 +value-timestamp = element timestamp { + xsd:string { pattern = "\d{8}T\d{6}(Z|[+\-]\d\d(\d\d)?)?" } + } + +# 4.4 +value-boolean = element boolean { xsd:boolean } + +# 4.5 +value-integer = element integer { xsd:integer } + +# 4.6 +value-float = element float { xsd:float } + +# 4.7 +value-utc-offset = element utc-offset { + xsd:string { pattern = "[+\-]\d\d(\d\d)?" } + } + +# 4.8 +value-language-tag = element language-tag { + xsd:string { pattern = "([a-z]{2,3}((-[a-z]{3}){0,3})?|[a-z]{4,8})" + ~ "(-[a-z]{4})?(-([a-z]{2}|\d{3}))?" + ~ "(-([0-9a-z]{5,8}|\d[0-9a-z]{3}))*" + ~ "(-[0-9a-wyz](-[0-9a-z]{2,8})+)*" + ~ "(-x(-[0-9a-z]{1,8})+)?|x(-[0-9a-z]{1,8})+|" + ~ "[a-z]{1,3}(-[0-9a-z]{2,8}){1,2}" } + } + +### Section 5: Parameters +# +# 5.1 +param-language = element language { value-language-tag }? + +# 5.2 +param-pref = element pref { + element integer { + xsd:integer { minInclusive = "1" maxInclusive = "100" } + } + }? + +# 5.4 +param-altid = element altid { value-text }? + +# 5.5 +param-pid = element pid { + element text { xsd:string { pattern = "\d+(\.\d+)?" } }+ + }? + +# 5.6 +param-type = element type { element text { "work" | "home" }+ }? + +# 5.7 +param-mediatype = element mediatype { value-text }? + +# 5.8 +param-calscale = element calscale { element text { "gregorian" } }? + +# 5.9 +param-sort-as = element sort-as { value-text+ }? + +# 5.10 +param-geo = element geo { value-uri }? + +# 5.11 +param-tz = element tz { value-text | value-uri }? + +### Section 6: Properties +# +# 6.1.3 +property-source = element source { + element parameters { param-altid, param-pid, param-pref, + param-mediatype }?, + value-uri + } + +# 6.1.4 +property-kind = element kind { + element text { "individual" | "group" | "org" | "location" | + x-name | iana-token }* + } + +# 6.2.1 +property-fn = element fn { + element parameters { param-language, param-altid, param-pid, + param-pref, param-type }?, + value-text + } + +# 6.2.2 +property-n = element n { + element parameters { param-language, param-sort-as, param-altid }?, + element surname { text }+, + element given { text }+, + element additional { text }+, + element prefix { text }+, + element suffix { text }+ + } + +# 6.2.3 +property-nickname = element nickname { + element parameters { param-language, param-altid, param-pid, + param-pref, param-type }?, + value-text-list + } + +# 6.2.4 +property-photo = element photo { + element parameters { param-altid, param-pid, param-pref, param-type, + param-mediatype }?, + value-uri + } + +# 6.2.5 +property-bday = element bday { + element parameters { param-altid, param-calscale }?, + (value-date-and-or-time | value-text) + } + +# 6.2.6 +property-anniversary = element anniversary { + element parameters { param-altid, param-calscale }?, + (value-date-and-or-time | value-text) + } + +# 6.2.7 +property-gender = element gender { + element sex { "" | "M" | "F" | "O" | "N" | "U" }, + element identity { text }? + } + +# 6.3.1 +param-label = element label { value-text }? +property-adr = element adr { + element parameters { param-language, param-altid, param-pid, + param-pref, param-type, param-geo, param-tz, + param-label }?, + element pobox { text }+, + element ext { text }+, + element street { text }+, + element locality { text }+, + element region { text }+, + element code { text }+, + element country { text }+ + } + +# 6.4.1 +property-tel = element tel { + element parameters { + param-altid, + param-pid, + param-pref, + element type { + element text { "work" | "home" | "text" | "voice" + | "fax" | "cell" | "video" | "pager" + | "textphone" | x-name | iana-token }+ + }?, + param-mediatype + }?, + (value-text | value-uri) + } + +# 6.4.2 +property-email = element email { + element parameters { param-altid, param-pid, param-pref, + param-type }?, + value-text + } + +# 6.4.3 +property-impp = element impp { + element parameters { param-altid, param-pid, param-pref, + param-type, param-mediatype }?, + value-uri + } + +# 6.4.4 +property-lang = element lang { + element parameters { param-altid, param-pid, param-pref, + param-type }?, + value-language-tag + } + +# 6.5.1 +property-tz = element tz { + element parameters { param-altid, param-pid, param-pref, + param-type, param-mediatype }?, + (value-text | value-uri | value-utc-offset) + } + +# 6.5.2 +property-geo = element geo { + element parameters { param-altid, param-pid, param-pref, + param-type, param-mediatype }?, + value-uri + } + +# 6.6.1 +property-title = element title { + element parameters { param-language, param-altid, param-pid, + param-pref, param-type }?, + value-text + } + +# 6.6.2 +property-role = element role { + element parameters { param-language, param-altid, param-pid, + param-pref, param-type }?, + value-text + } + +# 6.6.3 +property-logo = element logo { + element parameters { param-language, param-altid, param-pid, + param-pref, param-type, param-mediatype }?, + value-uri + } + +# 6.6.4 +property-org = element org { + element parameters { param-language, param-altid, param-pid, + param-pref, param-type, param-sort-as }?, + value-text-list + } + +# 6.6.5 +property-member = element member { + element parameters { param-altid, param-pid, param-pref, + param-mediatype }?, + value-uri + } + +# 6.6.6 +property-related = element related { + element parameters { + param-altid, + param-pid, + param-pref, + element type { + element text { + "work" | "home" | "contact" | "acquaintance" | + "friend" | "met" | "co-worker" | "colleague" | "co-resident" | + "neighbor" | "child" | "parent" | "sibling" | "spouse" | + "kin" | "muse" | "crush" | "date" | "sweetheart" | "me" | + "agent" | "emergency" + }+ + }?, + param-mediatype + }?, + (value-uri | value-text) + } + +# 6.7.1 +property-categories = element categories { + element parameters { param-altid, param-pid, param-pref, + param-type }?, + value-text-list + } + +# 6.7.2 +property-note = element note { + element parameters { param-language, param-altid, param-pid, + param-pref, param-type }?, + value-text + } + +# 6.7.3 +property-prodid = element prodid { value-text } + +# 6.7.4 +property-rev = element rev { value-timestamp } + +# 6.7.5 +property-sound = element sound { + element parameters { param-language, param-altid, param-pid, + param-pref, param-type, param-mediatype }?, + value-uri + } + +# 6.7.6 +property-uid = element uid { value-uri } + +# 6.7.7 +property-clientpidmap = element clientpidmap { + element sourceid { xsd:positiveInteger }, + value-uri + } + +# 6.7.8 +property-url = element url { + element parameters { param-altid, param-pid, param-pref, + param-type, param-mediatype }?, + value-uri + } + +# 6.8.1 +property-key = element key { + element parameters { param-altid, param-pid, param-pref, + param-type, param-mediatype }?, + (value-uri | value-text) + } + +# 6.9.1 +property-fburl = element fburl { + element parameters { param-altid, param-pid, param-pref, + param-type, param-mediatype }?, + value-uri + } + +# 6.9.2 +property-caladruri = element caladruri { + element parameters { param-altid, param-pid, param-pref, + param-type, param-mediatype }?, + value-uri + } + +# 6.9.3 +property-caluri = element caluri { + element parameters { param-altid, param-pid, param-pref, + param-type, param-mediatype }?, + value-uri + } + +# Top-level grammar +property = property-adr | property-anniversary | property-bday + | property-caladruri | property-caluri | property-categories + | property-clientpidmap | property-email | property-fburl + | property-fn | property-geo | property-impp | property-key + | property-kind | property-lang | property-logo + | property-member | property-n | property-nickname + | property-note | property-org | property-photo + | property-prodid | property-related | property-rev + | property-role | property-gender | property-sound + | property-source | property-tel | property-title + | property-tz | property-uid | property-url +start = element vcards { + element vcard { + (property + | element group { + attribute name { text }, + property* + })+ + }+ + } diff --git a/lib/vendor/sabre/xml/.github/workflows/ci.yml b/lib/vendor/sabre/xml/.github/workflows/ci.yml new file mode 100644 index 0000000..5775aba --- /dev/null +++ b/lib/vendor/sabre/xml/.github/workflows/ci.yml @@ -0,0 +1,69 @@ +name: continuous-integration +on: + push: + branches: + - master + - release/* + pull_request: +jobs: + unit-testing: + name: PHPUnit (PHP ${{ matrix.php-versions }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php-versions: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3'] + coverage: ['pcov'] + code-style: ['yes'] + code-analysis: ['no'] + include: + - php-versions: '7.1' + coverage: 'none' + code-style: 'yes' + code-analysis: 'yes' + - php-versions: '8.4' + coverage: 'pcov' + code-style: 'yes' + code-analysis: 'yes' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP, with composer and extensions + uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php + with: + php-version: ${{ matrix.php-versions }} + extensions: mbstring, dom, fileinfo, mysql, redis, opcache + coverage: ${{ matrix.coverage }} + tools: composer + + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache composer dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + # Use composer.json for key, if composer.lock is not committed. + # key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install composer dependencies + run: composer install --no-progress --prefer-dist --optimize-autoloader + + - name: Code Analysis (PHP CS-Fixer) + if: matrix.code-style == 'yes' + run: PHP_CS_FIXER_IGNORE_ENV=true php vendor/bin/php-cs-fixer fix --dry-run --diff + + - name: Code Analysis (PHPStan) + if: matrix.code-analysis == 'yes' + run: composer phpstan + + - name: Test with phpunit + run: vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-clover clover.xml + + - name: Code Coverage + uses: codecov/codecov-action@v3 + if: matrix.coverage != 'none' diff --git a/lib/vendor/sabre/xml/.php-cs-fixer.dist.php b/lib/vendor/sabre/xml/.php-cs-fixer.dist.php new file mode 100644 index 0000000..f9d4b7a --- /dev/null +++ b/lib/vendor/sabre/xml/.php-cs-fixer.dist.php @@ -0,0 +1,17 @@ +exclude('vendor') + ->in(__DIR__); + +$config = new PhpCsFixer\Config(); +$config->setRules([ + '@PSR1' => true, + '@Symfony' => true, + 'nullable_type_declaration' => [ + 'syntax' => 'question_mark', + ], + 'nullable_type_declaration_for_default_null_value' => true, +]); +$config->setFinder($finder); +return $config; \ No newline at end of file diff --git a/lib/vendor/sabre/xml/LICENSE b/lib/vendor/sabre/xml/LICENSE new file mode 100644 index 0000000..c9faf40 --- /dev/null +++ b/lib/vendor/sabre/xml/LICENSE @@ -0,0 +1,27 @@ +Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/) + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name Sabre nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/vendor/sabre/xml/README.md b/lib/vendor/sabre/xml/README.md new file mode 100644 index 0000000..55af24f --- /dev/null +++ b/lib/vendor/sabre/xml/README.md @@ -0,0 +1,25 @@ +sabre/xml +========= + +[![Build Status](https://secure.travis-ci.org/sabre-io/xml.svg?branch=master)](http://travis-ci.org/sabre-io/xml) + +The sabre/xml library is a specialized XML reader and writer. + +Documentation +------------- + +* [Introduction](http://sabre.io/xml/). +* [Installation](http://sabre.io/xml/install/). +* [Reading XML](http://sabre.io/xml/reading/). +* [Writing XML](http://sabre.io/xml/writing/). + + +Support +------- + +Head over to the [SabreDAV mailing list](http://groups.google.com/group/sabredav-discuss) for any questions. + +Made at fruux +------------- + +This library is being developed by [fruux](https://fruux.com/). Drop us a line for commercial services or enterprise support. diff --git a/lib/vendor/sabre/xml/bin/.empty b/lib/vendor/sabre/xml/bin/.empty new file mode 100644 index 0000000..e69de29 diff --git a/lib/vendor/sabre/xml/composer.json b/lib/vendor/sabre/xml/composer.json new file mode 100644 index 0000000..d7577c2 --- /dev/null +++ b/lib/vendor/sabre/xml/composer.json @@ -0,0 +1,67 @@ +{ + "name": "sabre/xml", + "description" : "sabre/xml is an XML library that you may not hate.", + "keywords" : [ "XML", "XMLReader", "XMLWriter", "DOM" ], + "homepage" : "https://sabre.io/xml/", + "license" : "BSD-3-Clause", + "require" : { + "php" : "^7.1 || ^8.0", + "ext-xmlwriter" : "*", + "ext-xmlreader" : "*", + "ext-dom" : "*", + "lib-libxml" : ">=2.6.20", + "sabre/uri" : ">=1.0,<3.0.0" + }, + "authors" : [ + { + "name" : "Evert Pot", + "email" : "me@evertpot.com", + "homepage" : "http://evertpot.com/", + "role" : "Developer" + }, + { + "name": "Markus Staab", + "email": "markus.staab@redaxo.de", + "role" : "Developer" + } + ], + "support" : { + "forum" : "https://groups.google.com/group/sabredav-discuss", + "source" : "https://github.com/fruux/sabre-xml" + }, + "autoload" : { + "psr-4" : { + "Sabre\\Xml\\" : "lib/" + }, + "files": [ + "lib/Deserializer/functions.php", + "lib/Serializer/functions.php" + ] + }, + "autoload-dev" : { + "psr-4" : { + "Sabre\\Xml\\" : "tests/Sabre/Xml/" + } + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1||3.63.2", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit" : "^7.5 || ^8.5 || ^9.6" + }, + "scripts": { + "phpstan": [ + "phpstan analyse lib tests" + ], + "cs-fixer": [ + "PHP_CS_FIXER_IGNORE_ENV=true php-cs-fixer fix" + ], + "phpunit": [ + "phpunit --configuration tests/phpunit.xml" + ], + "test": [ + "composer phpstan", + "composer cs-fixer", + "composer phpunit" + ] + } +} diff --git a/lib/vendor/sabre/xml/lib/ContextStackTrait.php b/lib/vendor/sabre/xml/lib/ContextStackTrait.php new file mode 100644 index 0000000..4e15bd4 --- /dev/null +++ b/lib/vendor/sabre/xml/lib/ContextStackTrait.php @@ -0,0 +1,118 @@ +contextStack[] = [ + $this->elementMap, + $this->contextUri, + $this->namespaceMap, + $this->classMap, + ]; + } + + /** + * Restore the previous "context". + */ + public function popContext() + { + list( + $this->elementMap, + $this->contextUri, + $this->namespaceMap, + $this->classMap + ) = array_pop($this->contextStack); + } +} diff --git a/lib/vendor/sabre/xml/lib/Deserializer/functions.php b/lib/vendor/sabre/xml/lib/Deserializer/functions.php new file mode 100644 index 0000000..5081809 --- /dev/null +++ b/lib/vendor/sabre/xml/lib/Deserializer/functions.php @@ -0,0 +1,360 @@ +value" array. + * + * For example, keyvalue will parse: + * + * + * + * value1 + * value2 + * + * + * + * Into: + * + * [ + * "{http://sabredav.org/ns}elem1" => "value1", + * "{http://sabredav.org/ns}elem2" => "value2", + * "{http://sabredav.org/ns}elem3" => null, + * ]; + * + * If you specify the 'namespace' argument, the deserializer will remove + * the namespaces of the keys that match that namespace. + * + * For example, if you call keyValue like this: + * + * keyValue($reader, 'http://sabredav.org/ns') + * + * it's output will instead be: + * + * [ + * "elem1" => "value1", + * "elem2" => "value2", + * "elem3" => null, + * ]; + * + * Attributes will be removed from the top-level elements. If elements with + * the same name appear twice in the list, only the last one will be kept. + */ +function keyValue(Reader $reader, ?string $namespace = null): array +{ + // If there's no children, we don't do anything. + if ($reader->isEmptyElement) { + $reader->next(); + + return []; + } + + if (!$reader->read()) { + $reader->next(); + + return []; + } + + if (Reader::END_ELEMENT === $reader->nodeType) { + $reader->next(); + + return []; + } + + $values = []; + + do { + if (Reader::ELEMENT === $reader->nodeType) { + if (null !== $namespace && $reader->namespaceURI === $namespace) { + $values[$reader->localName] = $reader->parseCurrentElement()['value']; + } else { + $clark = $reader->getClark(); + $values[$clark] = $reader->parseCurrentElement()['value']; + } + } else { + if (!$reader->read()) { + break; + } + } + } while (Reader::END_ELEMENT !== $reader->nodeType); + + $reader->read(); + + return $values; +} + +/** + * The 'enum' deserializer parses elements into a simple list + * without values or attributes. + * + * For example, Elements will parse: + * + * + * + * + * + * + * content + * + * + * + * Into: + * + * [ + * "{http://sabredav.org/ns}elem1", + * "{http://sabredav.org/ns}elem2", + * "{http://sabredav.org/ns}elem3", + * "{http://sabredav.org/ns}elem4", + * "{http://sabredav.org/ns}elem5", + * ]; + * + * This is useful for 'enum'-like structures. + * + * If the $namespace argument is specified, it will strip the namespace + * for all elements that match that. + * + * For example, + * + * enum($reader, 'http://sabredav.org/ns') + * + * would return: + * + * [ + * "elem1", + * "elem2", + * "elem3", + * "elem4", + * "elem5", + * ]; + * + * @return string[] + */ +function enum(Reader $reader, ?string $namespace = null): array +{ + // If there's no children, we don't do anything. + if ($reader->isEmptyElement) { + $reader->next(); + + return []; + } + if (!$reader->read()) { + $reader->next(); + + return []; + } + + if (Reader::END_ELEMENT === $reader->nodeType) { + $reader->next(); + + return []; + } + $currentDepth = $reader->depth; + + $values = []; + do { + if (Reader::ELEMENT !== $reader->nodeType) { + continue; + } + if (!is_null($namespace) && $namespace === $reader->namespaceURI) { + $values[] = $reader->localName; + } else { + $values[] = (string) $reader->getClark(); + } + } while ($reader->depth >= $currentDepth && $reader->next()); + + $reader->next(); + + return $values; +} + +/** + * The valueObject deserializer turns an xml element into a PHP object of + * a specific class. + * + * This is primarily used by the mapValueObject function from the Service + * class, but it can also easily be used for more specific situations. + * + * @return object + */ +function valueObject(Reader $reader, string $className, string $namespace) +{ + $valueObject = new $className(); + if ($reader->isEmptyElement) { + $reader->next(); + + return $valueObject; + } + + $defaultProperties = get_class_vars($className); + + $reader->read(); + do { + if (Reader::ELEMENT === $reader->nodeType && $reader->namespaceURI == $namespace) { + if (property_exists($valueObject, $reader->localName)) { + if (is_array($defaultProperties[$reader->localName])) { + $valueObject->{$reader->localName}[] = $reader->parseCurrentElement()['value']; + } else { + $valueObject->{$reader->localName} = $reader->parseCurrentElement()['value']; + } + } else { + // Ignore property + $reader->next(); + } + } elseif (Reader::ELEMENT === $reader->nodeType) { + // Skipping element from different namespace + $reader->next(); + } else { + if (Reader::END_ELEMENT !== $reader->nodeType && !$reader->read()) { + break; + } + } + } while (Reader::END_ELEMENT !== $reader->nodeType); + + $reader->read(); + + return $valueObject; +} + +/** + * This deserializer helps you deserialize xml structures that look like + * this:. + * + * + * ... + * ... + * ... + * + * + * Many XML documents use patterns like that, and this deserializer + * allow you to get all the 'items' as an array. + * + * In that previous example, you would register the deserializer as such: + * + * $reader->elementMap['{}collection'] = function($reader) { + * return repeatingElements($reader, '{}item'); + * } + * + * The repeatingElements deserializer simply returns everything as an array. + * + * $childElementName must either be a a clark-notation element name, or if no + * namespace is used, the bare element name. + */ +function repeatingElements(Reader $reader, string $childElementName): array +{ + if ('{' !== $childElementName[0]) { + $childElementName = '{}'.$childElementName; + } + $result = []; + + foreach ($reader->parseGetElements() as $element) { + if ($element['name'] === $childElementName) { + $result[] = $element['value']; + } + } + + return $result; +} + +/** + * This deserializer helps you to deserialize structures which contain mixed content like this:. + * + *

    some text and a inline tagand even more text

    + * + * The above example will return + * + * [ + * 'some text', + * [ + * 'name' => '{}extref', + * 'value' => 'and a inline tag', + * 'attributes' => [] + * ], + * 'and even more text' + * ] + * + * In strict XML documents you wont find this kind of markup but in html this is a quite common pattern. + */ +function mixedContent(Reader $reader): array +{ + // If there's no children, we don't do anything. + if ($reader->isEmptyElement) { + $reader->next(); + + return []; + } + + $previousDepth = $reader->depth; + + $content = []; + $reader->read(); + while (true) { + if (Reader::ELEMENT == $reader->nodeType) { + $content[] = $reader->parseCurrentElement(); + } elseif ($reader->depth >= $previousDepth && in_array($reader->nodeType, [Reader::TEXT, Reader::CDATA, Reader::WHITESPACE])) { + $content[] = $reader->value; + $reader->read(); + } elseif (Reader::END_ELEMENT == $reader->nodeType) { + // Ensuring we are moving the cursor after the end element. + $reader->read(); + break; + } else { + $reader->read(); + } + } + + return $content; +} + +/** + * The functionCaller deserializer turns an xml element into whatever your callable return. + * + * You can use, e.g., a named constructor (factory method) to create an object using + * this function. + */ +function functionCaller(Reader $reader, callable $func, string $namespace) +{ + if ($reader->isEmptyElement) { + $reader->next(); + + return null; + } + + $funcArgs = []; + $func = is_string($func) && false !== strpos($func, '::') ? explode('::', $func) : $func; + $ref = is_array($func) ? new \ReflectionMethod($func[0], $func[1]) : new \ReflectionFunction($func); + foreach ($ref->getParameters() as $parameter) { + $funcArgs[$parameter->getName()] = null; + } + + $reader->read(); + do { + if (Reader::ELEMENT === $reader->nodeType && $reader->namespaceURI == $namespace) { + if (array_key_exists($reader->localName, $funcArgs)) { + $funcArgs[$reader->localName] = $reader->parseCurrentElement()['value']; + } else { + // Ignore property + $reader->next(); + } + } else { + $reader->read(); + } + } while (Reader::END_ELEMENT !== $reader->nodeType); + $reader->read(); + + return $func(...array_values($funcArgs)); +} diff --git a/lib/vendor/sabre/xml/lib/Element.php b/lib/vendor/sabre/xml/lib/Element.php new file mode 100644 index 0000000..559eb54 --- /dev/null +++ b/lib/vendor/sabre/xml/lib/Element.php @@ -0,0 +1,22 @@ +value = $value; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Xml\Writer $writer) + { + $writer->write($this->value); + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * Important note 2: You are responsible for advancing the reader to the + * next element. Not doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + */ + public static function xmlDeserialize(Xml\Reader $reader) + { + $subTree = $reader->parseInnerTree(); + + return $subTree; + } +} diff --git a/lib/vendor/sabre/xml/lib/Element/Cdata.php b/lib/vendor/sabre/xml/lib/Element/Cdata.php new file mode 100644 index 0000000..1367343 --- /dev/null +++ b/lib/vendor/sabre/xml/lib/Element/Cdata.php @@ -0,0 +1,59 @@ +value = $value; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Xml\Writer $writer) + { + $writer->writeCData($this->value); + } +} diff --git a/lib/vendor/sabre/xml/lib/Element/Elements.php b/lib/vendor/sabre/xml/lib/Element/Elements.php new file mode 100644 index 0000000..6915fd4 --- /dev/null +++ b/lib/vendor/sabre/xml/lib/Element/Elements.php @@ -0,0 +1,98 @@ + + * + * + * + * + * content + * + * + * + * Into: + * + * [ + * "{http://sabredav.org/ns}elem1", + * "{http://sabredav.org/ns}elem2", + * "{http://sabredav.org/ns}elem3", + * "{http://sabredav.org/ns}elem4", + * "{http://sabredav.org/ns}elem5", + * ]; + * + * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Elements implements Xml\Element +{ + /** + * Value to serialize. + * + * @var array + */ + protected $value; + + /** + * Constructor. + */ + public function __construct(array $value = []) + { + $this->value = $value; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Xml\Writer $writer) + { + Serializer\enum($writer, $this->value); + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * Important note 2: You are responsible for advancing the reader to the + * next element. Not doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseSubTree() will parse the entire sub-tree, and advance to + * the next element. + */ + public static function xmlDeserialize(Xml\Reader $reader) + { + return Deserializer\enum($reader); + } +} diff --git a/lib/vendor/sabre/xml/lib/Element/KeyValue.php b/lib/vendor/sabre/xml/lib/Element/KeyValue.php new file mode 100644 index 0000000..7d75a3a --- /dev/null +++ b/lib/vendor/sabre/xml/lib/Element/KeyValue.php @@ -0,0 +1,98 @@ +value struct. + * + * Attributes will be removed, and duplicate child elements are discarded. + * Complex values within the elements will be parsed by the 'standard' parser. + * + * For example, KeyValue will parse: + * + * + * + * value1 + * value2 + * + * + * + * Into: + * + * [ + * "{http://sabredav.org/ns}elem1" => "value1", + * "{http://sabredav.org/ns}elem2" => "value2", + * "{http://sabredav.org/ns}elem3" => null, + * ]; + * + * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class KeyValue implements Xml\Element +{ + /** + * Value to serialize. + * + * @var array + */ + protected $value; + + /** + * Constructor. + */ + public function __construct(array $value = []) + { + $this->value = $value; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Xml\Writer $writer) + { + $writer->write($this->value); + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * Important note 2: You are responsible for advancing the reader to the + * next element. Not doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + */ + public static function xmlDeserialize(Xml\Reader $reader) + { + return Deserializer\keyValue($reader); + } +} diff --git a/lib/vendor/sabre/xml/lib/Element/Uri.php b/lib/vendor/sabre/xml/lib/Element/Uri.php new file mode 100644 index 0000000..6527638 --- /dev/null +++ b/lib/vendor/sabre/xml/lib/Element/Uri.php @@ -0,0 +1,97 @@ +/foo/bar + * http://example.org/hi + * + * If the uri is relative, it will be automatically expanded to an absolute + * url during writing and reading, if the contextUri property is set on the + * reader and/or writer. + * + * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Uri implements Xml\Element +{ + /** + * Uri element value. + * + * @var string + */ + protected $value; + + /** + * Constructor. + * + * @param string $value + */ + public function __construct($value) + { + $this->value = $value; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Xml\Writer $writer) + { + $writer->text( + \Sabre\Uri\resolve( + $writer->contextUri, + $this->value + ) + ); + } + + /** + * This method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * Important note 2: You are responsible for advancing the reader to the + * next element. Not doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseSubTree() will parse the entire sub-tree, and advance to + * the next element. + */ + public static function xmlDeserialize(Xml\Reader $reader) + { + return new self( + \Sabre\Uri\resolve( + (string) $reader->contextUri, + $reader->readText() + ) + ); + } +} diff --git a/lib/vendor/sabre/xml/lib/Element/XmlFragment.php b/lib/vendor/sabre/xml/lib/Element/XmlFragment.php new file mode 100644 index 0000000..99d1f87 --- /dev/null +++ b/lib/vendor/sabre/xml/lib/Element/XmlFragment.php @@ -0,0 +1,146 @@ +xml = $xml; + } + + /** + * Returns the inner XML document. + */ + public function getXml(): string + { + return $this->xml; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + $reader = new Reader(); + + // Wrapping the xml in a container, so root-less values can still be + // parsed. + $xml = << +{$this->getXml()} +XML; + + $reader->xml($xml); + + while ($reader->read()) { + if ($reader->depth < 1) { + // Skipping the root node. + continue; + } + + switch ($reader->nodeType) { + case Reader::ELEMENT: + $writer->startElement( + (string) $reader->getClark() + ); + $empty = $reader->isEmptyElement; + while ($reader->moveToNextAttribute()) { + switch ($reader->namespaceURI) { + case '': + $writer->writeAttribute($reader->localName, $reader->value); + break; + case 'http://www.w3.org/2000/xmlns/': + // Skip namespace declarations + break; + default: + $writer->writeAttribute((string) $reader->getClark(), $reader->value); + break; + } + } + if ($empty) { + $writer->endElement(); + } + break; + case Reader::CDATA: + case Reader::TEXT: + $writer->text( + $reader->value + ); + break; + case Reader::END_ELEMENT: + $writer->endElement(); + break; + } + } + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + */ + public static function xmlDeserialize(Reader $reader) + { + $result = new self($reader->readInnerXml()); + $reader->next(); + + return $result; + } +} diff --git a/lib/vendor/sabre/xml/lib/LibXMLException.php b/lib/vendor/sabre/xml/lib/LibXMLException.php new file mode 100644 index 0000000..993f95f --- /dev/null +++ b/lib/vendor/sabre/xml/lib/LibXMLException.php @@ -0,0 +1,47 @@ +errors = $errors; + parent::__construct($errors[0]->message.' on line '.$errors[0]->line.', column '.$errors[0]->column, $code, $previousException); + } + + /** + * Returns the LibXML errors. + */ + public function getErrors(): array + { + return $this->errors; + } +} diff --git a/lib/vendor/sabre/xml/lib/ParseException.php b/lib/vendor/sabre/xml/lib/ParseException.php new file mode 100644 index 0000000..158cf01 --- /dev/null +++ b/lib/vendor/sabre/xml/lib/ParseException.php @@ -0,0 +1,18 @@ +localName) { + return null; + } + + return '{'.$this->namespaceURI.'}'.$this->localName; + } + + /** + * Reads the entire document. + * + * This function returns an array with the following three elements: + * * name - The root element name. + * * value - The value for the root element. + * * attributes - An array of attributes. + * + * This function will also disable the standard libxml error handler (which + * usually just results in PHP errors), and throw exceptions instead. + */ + public function parse(): array + { + $previousEntityState = null; + $shouldCallLibxmlDisableEntityLoader = (\LIBXML_VERSION < 20900); + if ($shouldCallLibxmlDisableEntityLoader) { + $previousEntityState = libxml_disable_entity_loader(true); + } + $previousSetting = libxml_use_internal_errors(true); + + try { + while (self::ELEMENT !== $this->nodeType) { + if (!$this->read()) { + $errors = libxml_get_errors(); + libxml_clear_errors(); + if ($errors) { + throw new LibXMLException($errors); + } + } + } + $result = $this->parseCurrentElement(); + + // last line of defense in case errors did occur above + $errors = libxml_get_errors(); + libxml_clear_errors(); + if ($errors) { + throw new LibXMLException($errors); + } + } finally { + libxml_use_internal_errors($previousSetting); + if ($shouldCallLibxmlDisableEntityLoader) { + libxml_disable_entity_loader($previousEntityState); + } + } + + return $result; + } + + /** + * parseGetElements parses everything in the current sub-tree, + * and returns an array of elements. + * + * Each element has a 'name', 'value' and 'attributes' key. + * + * If the element didn't contain sub-elements, an empty array is always + * returned. If there was any text inside the element, it will be + * discarded. + * + * If the $elementMap argument is specified, the existing elementMap will + * be overridden while parsing the tree, and restored after this process. + */ + public function parseGetElements(?array $elementMap = null): array + { + $result = $this->parseInnerTree($elementMap); + if (!is_array($result)) { + return []; + } + + return $result; + } + + /** + * Parses all elements below the current element. + * + * This method will return a string if this was a text-node, or an array if + * there were sub-elements. + * + * If there's both text and sub-elements, the text will be discarded. + * + * If the $elementMap argument is specified, the existing elementMap will + * be overridden while parsing the tree, and restored after this process. + * + * @return array|string|null + */ + public function parseInnerTree(?array $elementMap = null) + { + $text = null; + $elements = []; + + if (self::ELEMENT === $this->nodeType && $this->isEmptyElement) { + // Easy! + $this->next(); + + return null; + } + + if (!is_null($elementMap)) { + $this->pushContext(); + $this->elementMap = $elementMap; + } + + try { + if (!$this->read()) { + $errors = libxml_get_errors(); + libxml_clear_errors(); + if ($errors) { + throw new LibXMLException($errors); + } + throw new ParseException('This should never happen (famous last words)'); + } + + $keepOnParsing = true; + + while ($keepOnParsing) { + if (!$this->isValid()) { + $errors = libxml_get_errors(); + + if ($errors) { + libxml_clear_errors(); + throw new LibXMLException($errors); + } + } + + switch ($this->nodeType) { + case self::ELEMENT: + $elements[] = $this->parseCurrentElement(); + break; + case self::TEXT: + case self::CDATA: + $text .= $this->value; + $this->read(); + break; + case self::END_ELEMENT: + // Ensuring we are moving the cursor after the end element. + $this->read(); + $keepOnParsing = false; + break; + case self::NONE: + throw new ParseException('We hit the end of the document prematurely. This likely means that some parser "eats" too many elements. Do not attempt to continue parsing.'); + default: + // Advance to the next element + $this->read(); + break; + } + } + } finally { + if (!is_null($elementMap)) { + $this->popContext(); + } + } + + return $elements ? $elements : $text; + } + + /** + * Reads all text below the current element, and returns this as a string. + */ + public function readText(): string + { + $result = ''; + $previousDepth = $this->depth; + + while ($this->read() && $this->depth != $previousDepth) { + if (in_array($this->nodeType, [\XMLReader::TEXT, \XMLReader::CDATA, \XMLReader::WHITESPACE])) { + $result .= $this->value; + } + } + + return $result; + } + + /** + * Parses the current XML element. + * + * This method returns arn array with 3 properties: + * * name - A clark-notation XML element name. + * * value - The parsed value. + * * attributes - A key-value list of attributes. + */ + public function parseCurrentElement(): array + { + $name = $this->getClark(); + + $attributes = []; + + if ($this->hasAttributes) { + $attributes = $this->parseAttributes(); + } + + $value = call_user_func( + $this->getDeserializerForElementName((string) $name), + $this + ); + + return [ + 'name' => $name, + 'value' => $value, + 'attributes' => $attributes, + ]; + } + + /** + * Grabs all the attributes from the current element, and returns them as a + * key-value array. + * + * If the attributes are part of the same namespace, they will simply be + * short keys. If they are defined on a different namespace, the attribute + * name will be returned in clark-notation. + */ + public function parseAttributes(): array + { + $attributes = []; + + while ($this->moveToNextAttribute()) { + if ($this->namespaceURI) { + // Ignoring 'xmlns', it doesn't make any sense. + if ('http://www.w3.org/2000/xmlns/' === $this->namespaceURI) { + continue; + } + + $name = $this->getClark(); + $attributes[$name] = $this->value; + } else { + $attributes[$this->localName] = $this->value; + } + } + $this->moveToElement(); + + return $attributes; + } + + /** + * Returns the function that should be used to parse the element identified + * by its clark-notation name. + */ + public function getDeserializerForElementName(string $name): callable + { + if (!array_key_exists($name, $this->elementMap)) { + if ('{}' == substr($name, 0, 2) && array_key_exists(substr($name, 2), $this->elementMap)) { + $name = substr($name, 2); + } else { + return ['Sabre\\Xml\\Element\\Base', 'xmlDeserialize']; + } + } + + $deserializer = $this->elementMap[$name]; + if (is_subclass_of($deserializer, 'Sabre\\Xml\\XmlDeserializable')) { + return [$deserializer, 'xmlDeserialize']; + } + + if (is_callable($deserializer)) { + return $deserializer; + } + + $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.' for element: '.$name); + } +} diff --git a/lib/vendor/sabre/xml/lib/Serializer/functions.php b/lib/vendor/sabre/xml/lib/Serializer/functions.php new file mode 100644 index 0000000..23f22d4 --- /dev/null +++ b/lib/vendor/sabre/xml/lib/Serializer/functions.php @@ -0,0 +1,207 @@ + + * + * + * content + * + * + * @param string[] $values + */ +function enum(Writer $writer, array $values) +{ + foreach ($values as $value) { + $writer->writeElement($value); + } +} + +/** + * The valueObject serializer turns a simple PHP object into a classname. + * + * Every public property will be encoded as an xml element with the same + * name, in the XML namespace as specified. + * + * Values that are set to null or an empty array are not serialized. To + * serialize empty properties, you must specify them as an empty string. + * + * @param object $valueObject + */ +function valueObject(Writer $writer, $valueObject, string $namespace) +{ + foreach (get_object_vars($valueObject) as $key => $val) { + if (is_array($val)) { + // If $val is an array, it has a special meaning. We need to + // generate one child element for each item in $val + foreach ($val as $child) { + $writer->writeElement('{'.$namespace.'}'.$key, $child); + } + } elseif (null !== $val) { + $writer->writeElement('{'.$namespace.'}'.$key, $val); + } + } +} + +/** + * This serializer helps you serialize xml structures that look like + * this:. + * + * + * ... + * ... + * ... + * + * + * In that previous example, this serializer just serializes the item element, + * and this could be called like this: + * + * repeatingElements($writer, $items, '{}item'); + */ +function repeatingElements(Writer $writer, array $items, string $childElementName) +{ + foreach ($items as $item) { + $writer->writeElement($childElementName, $item); + } +} + +/** + * This function is the 'default' serializer that is able to serialize most + * things, and delegates to other serializers if needed. + * + * The standardSerializer supports a wide-array of values. + * + * $value may be a string or integer, it will just write out the string as text. + * $value may be an instance of XmlSerializable or Element, in which case it + * calls it's xmlSerialize() method. + * $value may be a PHP callback/function/closure, in case we call the callback + * and give it the Writer as an argument. + * $value may be a an object, and if it's in the classMap we automatically call + * the correct serializer for it. + * $value may be null, in which case we do nothing. + * + * If $value is an array, the array must look like this: + * + * [ + * [ + * 'name' => '{namespaceUri}element-name', + * 'value' => '...', + * 'attributes' => [ 'attName' => 'attValue' ] + * ] + * [, + * 'name' => '{namespaceUri}element-name2', + * 'value' => '...', + * ] + * ] + * + * This would result in xml like: + * + * + * ... + * + * + * ... + * + * + * The value property may be any value standardSerializer supports, so you can + * nest data-structures this way. Both value and attributes are optional. + * + * Alternatively, you can also specify the array using this syntax: + * + * [ + * [ + * '{namespaceUri}element-name' => '...', + * '{namespaceUri}element-name2' => '...', + * ] + * ] + * + * This is excellent for simple key->value structures, and here you can also + * specify anything for the value. + * + * You can even mix the two array syntaxes. + * + * @param string|int|float|bool|array|object $value + */ +function standardSerializer(Writer $writer, $value) +{ + if (is_scalar($value)) { + // String, integer, float, boolean + $writer->text((string) $value); + } elseif ($value instanceof XmlSerializable) { + // XmlSerializable classes or Element classes. + $value->xmlSerialize($writer); + } elseif (is_object($value) && isset($writer->classMap[get_class($value)])) { + // It's an object which class appears in the classmap. + $writer->classMap[get_class($value)]($writer, $value); + } elseif (is_callable($value)) { + // A callback + $value($writer); + } elseif (is_array($value) && array_key_exists('name', $value)) { + // if the array had a 'name' element, we assume that this array + // describes a 'name' and optionally 'attributes' and 'value'. + + $name = $value['name']; + $attributes = isset($value['attributes']) ? $value['attributes'] : []; + $value = isset($value['value']) ? $value['value'] : null; + + $writer->startElement($name); + $writer->writeAttributes($attributes); + $writer->write($value); + $writer->endElement(); + } elseif (is_array($value)) { + foreach ($value as $name => $item) { + if (is_int($name)) { + // This item has a numeric index. We just loop through the + // array and throw it back in the writer. + standardSerializer($writer, $item); + } elseif (is_string($name) && is_array($item) && isset($item['attributes'])) { + // The key is used for a name, but $item has 'attributes' and + // possibly 'value' + $writer->startElement($name); + $writer->writeAttributes($item['attributes']); + if (isset($item['value'])) { + $writer->write($item['value']); + } + $writer->endElement(); + } elseif (is_string($name)) { + // This was a plain key-value array. + $writer->startElement($name); + $writer->write($item); + $writer->endElement(); + } else { + throw new \InvalidArgumentException('The writer does not know how to serialize arrays with keys of type: '.gettype($name)); + } + } + } elseif (is_object($value)) { + throw new \InvalidArgumentException('The writer cannot serialize objects of class: '.get_class($value)); + } elseif (!is_null($value)) { + throw new \InvalidArgumentException('The writer cannot serialize values of type: '.gettype($value)); + } +} diff --git a/lib/vendor/sabre/xml/lib/Service.php b/lib/vendor/sabre/xml/lib/Service.php new file mode 100644 index 0000000..6e52263 --- /dev/null +++ b/lib/vendor/sabre/xml/lib/Service.php @@ -0,0 +1,326 @@ +elementMap = $this->elementMap; + + return $r; + } + + /** + * Returns a fresh xml writer. + */ + public function getWriter(): Writer + { + $w = new Writer(); + $w->namespaceMap = $this->namespaceMap; + $w->classMap = $this->classMap; + + return $w; + } + + /** + * Parses a document in full. + * + * Input may be specified as a string or readable stream resource. + * The returned value is the value of the root document. + * + * Specifying the $contextUri allows the parser to figure out what the URI + * of the document was. This allows relative URIs within the document to be + * expanded easily. + * + * The $rootElementName is specified by reference and will be populated + * with the root element name of the document. + * + * @param string|resource $input + * + * @return array|object|string + * + * @throws ParseException + */ + public function parse($input, ?string $contextUri = null, ?string &$rootElementName = null) + { + if (!is_string($input)) { + // Unfortunately the XMLReader doesn't support streams. When it + // does, we can optimize this. + if (is_resource($input)) { + $input = (string) stream_get_contents($input); + } else { + // Input is not a string and not a resource. + // Therefore, it has to be a closed resource. + // Effectively empty input has been passed in. + $input = ''; + } + } + + // If input is empty, then it's safe to throw an exception + if (empty($input)) { + throw new ParseException('The input element to parse is empty. Do not attempt to parse'); + } + + $r = $this->getReader(); + $r->contextUri = $contextUri; + $r->XML($input, null, $this->options); + + $result = $r->parse(); + $rootElementName = $result['name']; + + return $result['value']; + } + + /** + * Parses a document in full, and specify what the expected root element + * name is. + * + * This function works similar to parse, but the difference is that the + * user can specify what the expected name of the root element should be, + * in clark notation. + * + * This is useful in cases where you expected a specific document to be + * passed, and reduces the amount of if statements. + * + * It's also possible to pass an array of expected rootElements if your + * code may expect more than one document type. + * + * @param string|string[] $rootElementName + * @param string|resource $input + * + * @return array|object|string + * + * @throws ParseException + */ + public function expect($rootElementName, $input, ?string $contextUri = null) + { + if (!is_string($input)) { + // Unfortunately the XMLReader doesn't support streams. When it + // does, we can optimize this. + if (is_resource($input)) { + $input = (string) stream_get_contents($input); + } else { + // Input is not a string and not a resource. + // Therefore, it has to be a closed resource. + // Effectively empty input has been passed in. + $input = ''; + } + } + + // If input is empty, then it's safe to throw an exception + if (empty($input)) { + throw new ParseException('The input element to parse is empty. Do not attempt to parse'); + } + + $r = $this->getReader(); + $r->contextUri = $contextUri; + $r->XML($input, null, $this->options); + + $rootElementName = (array) $rootElementName; + + foreach ($rootElementName as &$rEl) { + if ('{' !== $rEl[0]) { + $rEl = '{}'.$rEl; + } + } + + $result = $r->parse(); + if (!in_array($result['name'], $rootElementName, true)) { + throw new ParseException('Expected '.implode(' or ', $rootElementName).' but received '.$result['name'].' as the root element'); + } + + return $result['value']; + } + + /** + * Generates an XML document in one go. + * + * The $rootElement must be specified in clark notation. + * The value must be a string, an array or an object implementing + * XmlSerializable. Basically, anything that's supported by the Writer + * object. + * + * $contextUri can be used to specify a sort of 'root' of the PHP application, + * in case the xml document is used as a http response. + * + * This allows an implementor to easily create URI's relative to the root + * of the domain. + * + * @param string|array|object|XmlSerializable $value + * + * @return string + */ + public function write(string $rootElementName, $value, ?string $contextUri = null) + { + $w = $this->getWriter(); + $w->openMemory(); + $w->contextUri = $contextUri; + $w->setIndent(true); + $w->startDocument(); + $w->writeElement($rootElementName, $value); + + return $w->outputMemory(); + } + + /** + * Map an XML element to a PHP class. + * + * Calling this function will automatically set up the Reader and Writer + * classes to turn a specific XML element to a PHP class. + * + * For example, given a class such as : + * + * class Author { + * public $firstName; + * public $lastName; + * } + * + * and an XML element such as: + * + * + * ... + * ... + * + * + * These can easily be mapped by calling: + * + * $service->mapValueObject('{http://example.org}author', 'Author'); + */ + public function mapValueObject(string $elementName, string $className) + { + list($namespace) = self::parseClarkNotation($elementName); + + $this->elementMap[$elementName] = function (Reader $reader) use ($className, $namespace) { + return \Sabre\Xml\Deserializer\valueObject($reader, $className, $namespace); + }; + $this->classMap[$className] = function (Writer $writer, $valueObject) use ($namespace) { + return \Sabre\Xml\Serializer\valueObject($writer, $valueObject, $namespace); + }; + $this->valueObjectMap[$className] = $elementName; + } + + /** + * Writes a value object. + * + * This function largely behaves similar to write(), except that it's + * intended specifically to serialize a Value Object into an XML document. + * + * The ValueObject must have been previously registered using + * mapValueObject(). + * + * @param object $object + * + * @throws \InvalidArgumentException + */ + public function writeValueObject($object, ?string $contextUri = null) + { + if (!isset($this->valueObjectMap[get_class($object)])) { + throw new \InvalidArgumentException('"'.get_class($object).'" is not a registered value object class. Register your class with mapValueObject.'); + } + + return $this->write( + $this->valueObjectMap[get_class($object)], + $object, + $contextUri + ); + } + + /** + * Parses a clark-notation string, and returns the namespace and element + * name components. + * + * If the string was invalid, it will throw an InvalidArgumentException. + * + * @throws \InvalidArgumentException + */ + public static function parseClarkNotation(string $str): array + { + static $cache = []; + + if (!isset($cache[$str])) { + if (!preg_match('/^{([^}]*)}(.*)$/', $str, $matches)) { + throw new \InvalidArgumentException('\''.$str.'\' is not a valid clark-notation formatted string'); + } + + $cache[$str] = [ + $matches[1], + $matches[2], + ]; + } + + return $cache[$str]; + } + + /** + * A list of classes and which XML elements they map to. + */ + protected $valueObjectMap = []; +} diff --git a/lib/vendor/sabre/xml/lib/Version.php b/lib/vendor/sabre/xml/lib/Version.php new file mode 100644 index 0000000..c2da842 --- /dev/null +++ b/lib/vendor/sabre/xml/lib/Version.php @@ -0,0 +1,20 @@ + "..", + * "{namespace}name2" => "..", + * ] + * + * One element will be created for each key in this array. The values of + * this array support any format this method supports (this method is + * called recursively). + * + * Array format 2: + * + * [ + * [ + * "name" => "{namespace}name1" + * "value" => "..", + * "attributes" => [ + * "attr" => "attribute value", + * ] + * ], + * [ + * "name" => "{namespace}name1" + * "value" => "..", + * "attributes" => [ + * "attr" => "attribute value", + * ] + * ] + * ] + */ + public function write($value) + { + Serializer\standardSerializer($this, $value); + } + + /** + * Opens a new element. + * + * You can either just use a local elementname, or you can use clark- + * notation to start a new element. + * + * Example: + * + * $writer->startElement('{http://www.w3.org/2005/Atom}entry'); + * + * Would result in something like: + * + * + * + * Note: this function doesn't have the string typehint, because PHP's + * XMLWriter::startElement doesn't either. + * + * @param string $name + */ + public function startElement($name): bool + { + if ('{' === $name[0]) { + list($namespace, $localName) = + Service::parseClarkNotation($name); + + if (array_key_exists($namespace, $this->namespaceMap)) { + $result = $this->startElementNS( + '' === $this->namespaceMap[$namespace] ? null : $this->namespaceMap[$namespace], + $localName, + null + ); + } else { + // An empty namespace means it's the global namespace. This is + // allowed, but it mustn't get a prefix. + if ('' === $namespace || null === $namespace) { + $result = $this->startElement($localName); + $this->writeAttribute('xmlns', ''); + } else { + if (!isset($this->adhocNamespaces[$namespace])) { + $this->adhocNamespaces[$namespace] = 'x'.(count($this->adhocNamespaces) + 1); + } + $result = $this->startElementNS($this->adhocNamespaces[$namespace], $localName, $namespace); + } + } + } else { + $result = parent::startElement($name); + } + + if (!$this->namespacesWritten) { + foreach ($this->namespaceMap as $namespace => $prefix) { + $this->writeAttribute($prefix ? 'xmlns:'.$prefix : 'xmlns', $namespace); + } + $this->namespacesWritten = true; + } + + return $result; + } + + /** + * Write a full element tag and it's contents. + * + * This method automatically closes the element as well. + * + * The element name may be specified in clark-notation. + * + * Examples: + * + * $writer->writeElement('{http://www.w3.org/2005/Atom}author',null); + * becomes: + * + * + * $writer->writeElement('{http://www.w3.org/2005/Atom}author', [ + * '{http://www.w3.org/2005/Atom}name' => 'Evert Pot', + * ]); + * becomes: + * Evert Pot + * + * Note: this function doesn't have the string typehint, because PHP's + * XMLWriter::startElement doesn't either. + * + * @param array|string|object|null $content + */ + public function writeElement($name, $content = null): bool + { + $this->startElement($name); + if (!is_null($content)) { + $this->write($content); + } + $this->endElement(); + + return true; + } + + /** + * Writes a list of attributes. + * + * Attributes are specified as a key->value array. + * + * The key is an attribute name. If the key is a 'localName', the current + * xml namespace is assumed. If it's a 'clark notation key', this namespace + * will be used instead. + */ + public function writeAttributes(array $attributes) + { + foreach ($attributes as $name => $value) { + $this->writeAttribute($name, $value); + } + } + + /** + * Writes a new attribute. + * + * The name may be specified in clark-notation. + * + * Returns true when successful. + * + * Note: this function doesn't have typehints, because for some reason + * PHP's XMLWriter::writeAttribute doesn't either. + * + * @param string $name + * @param string $value + */ + public function writeAttribute($name, $value): bool + { + if ('{' !== $name[0]) { + return parent::writeAttribute($name, $value); + } + + list( + $namespace, + $localName + ) = Service::parseClarkNotation($name); + + if (array_key_exists($namespace, $this->namespaceMap)) { + // It's an attribute with a namespace we know + return $this->writeAttribute( + $this->namespaceMap[$namespace].':'.$localName, + $value + ); + } + + // We don't know the namespace, we must add it in-line + if (!isset($this->adhocNamespaces[$namespace])) { + $this->adhocNamespaces[$namespace] = 'x'.(count($this->adhocNamespaces) + 1); + } + + return $this->writeAttributeNS( + $this->adhocNamespaces[$namespace], + $localName, + $namespace, + $value + ); + } +} diff --git a/lib/vendor/sabre/xml/lib/XmlDeserializable.php b/lib/vendor/sabre/xml/lib/XmlDeserializable.php new file mode 100644 index 0000000..0a57203 --- /dev/null +++ b/lib/vendor/sabre/xml/lib/XmlDeserializable.php @@ -0,0 +1,36 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + */ + public static function xmlDeserialize(Reader $reader); +} diff --git a/lib/vendor/sabre/xml/lib/XmlSerializable.php b/lib/vendor/sabre/xml/lib/XmlSerializable.php new file mode 100644 index 0000000..2affc33 --- /dev/null +++ b/lib/vendor/sabre/xml/lib/XmlSerializable.php @@ -0,0 +1,34 @@ +