From 6033cea60ee302fc115f9ce1795a65248ad734df Mon Sep 17 00:00:00 2001 From: Fenris Wolf Date: Tue, 9 Sep 2025 10:07:53 +0000 Subject: [PATCH] [ini] --- .gitignore | 2 + conf/example.json | 13 + lib/composer/composer.json | 6 + lib/composer/composer.lock | 578 ++++ lib/composer/vendor/autoload.php | 25 + lib/composer/vendor/bin/generate_vcards | 119 + lib/composer/vendor/bin/naturalselection | 37 + lib/composer/vendor/bin/sabredav | 37 + lib/composer/vendor/bin/vobject | 119 + lib/composer/vendor/composer/ClassLoader.php | 585 ++++ .../vendor/composer/InstalledVersions.php | 359 +++ lib/composer/vendor/composer/LICENSE | 19 + .../vendor/composer/autoload_classmap.php | 10 + .../vendor/composer/autoload_files.php | 16 + .../vendor/composer/autoload_namespaces.php | 10 + .../vendor/composer/autoload_psr4.php | 16 + .../vendor/composer/autoload_real.php | 50 + .../vendor/composer/autoload_static.php | 90 + lib/composer/vendor/composer/installed.json | 589 ++++ lib/composer/vendor/composer/installed.php | 95 + .../vendor/composer/platform_check.php | 26 + .../vendor/johngrogg/ics-parser/.editorconfig | 11 + .../.github/ISSUE_TEMPLATE/bug_report.yml | 53 + .../pull_request_template.md | 12 + .../ics-parser/.github/dependabot.yml | 13 + .../ics-parser/.github/release_template.md | 9 + .../.github/workflows/coding-standards.yml | 67 + .../vendor/johngrogg/ics-parser/.gitignore | 59 + .../johngrogg/ics-parser/CONTRIBUTING.md | 18 + .../vendor/johngrogg/ics-parser/FUNDING.yml | 1 + .../vendor/johngrogg/ics-parser/LICENSE | 15 + .../vendor/johngrogg/ics-parser/README.md | 256 ++ .../vendor/johngrogg/ics-parser/composer.json | 49 + .../vendor/johngrogg/ics-parser/composer.lock | 1763 +++++++++++ .../vendor/johngrogg/ics-parser/ecs.php | 205 ++ .../johngrogg/ics-parser/examples/ICal.ics | 338 ++ .../johngrogg/ics-parser/examples/index.php | 175 ++ .../johngrogg/ics-parser/phpstan.neon.dist | 9 + .../vendor/johngrogg/ics-parser/phpunit.xml | 7 + .../vendor/johngrogg/ics-parser/rector.php | 79 + .../johngrogg/ics-parser/src/ICal/Event.php | 264 ++ .../johngrogg/ics-parser/src/ICal/ICal.php | 2731 +++++++++++++++++ .../ics-parser/tests/CleanCharacterTest.php | 53 + .../tests/DynamicPropertiesTest.php | 24 + .../ics-parser/tests/KeyValueTest.php | 86 + .../ics-parser/tests/RecurrencesTest.php | 580 ++++ .../tests/Rfc5545RecurrenceTest.php | 1059 +++++++ .../ics-parser/tests/SingleEventsTest.php | 509 +++ .../ics-parser/tests/ical/ical-monthly.ics | 18 + .../ics-parser/tests/ical/issue-196.ics | 64 + lib/composer/vendor/psr/log/LICENSE | 19 + lib/composer/vendor/psr/log/README.md | 58 + lib/composer/vendor/psr/log/composer.json | 26 + .../vendor/psr/log/src/AbstractLogger.php | 15 + .../psr/log/src/InvalidArgumentException.php | 7 + lib/composer/vendor/psr/log/src/LogLevel.php | 18 + .../psr/log/src/LoggerAwareInterface.php | 14 + .../vendor/psr/log/src/LoggerAwareTrait.php | 22 + .../vendor/psr/log/src/LoggerInterface.php | 98 + .../vendor/psr/log/src/LoggerTrait.php | 98 + .../vendor/psr/log/src/NullLogger.php | 26 + lib/composer/vendor/sabre/dav/LICENSE | 27 + lib/composer/vendor/sabre/dav/README.md | 39 + lib/composer/vendor/sabre/dav/bin/build.php | 169 + .../vendor/sabre/dav/bin/migrateto20.php | 414 +++ .../vendor/sabre/dav/bin/migrateto21.php | 166 + .../vendor/sabre/dav/bin/migrateto30.php | 161 + .../vendor/sabre/dav/bin/migrateto32.php | 258 ++ .../vendor/sabre/dav/bin/naturalselection | 140 + lib/composer/vendor/sabre/dav/bin/sabredav | 2 + .../vendor/sabre/dav/bin/sabredav.php | 51 + lib/composer/vendor/sabre/dav/composer.json | 81 + .../lib/CalDAV/Backend/AbstractBackend.php | 216 ++ .../lib/CalDAV/Backend/BackendInterface.php | 273 ++ .../CalDAV/Backend/NotificationSupport.php | 62 + .../sabre/dav/lib/CalDAV/Backend/PDO.php | 1487 +++++++++ .../lib/CalDAV/Backend/SchedulingSupport.php | 66 + .../dav/lib/CalDAV/Backend/SharingSupport.php | 60 + .../dav/lib/CalDAV/Backend/SimplePDO.php | 289 ++ .../CalDAV/Backend/SubscriptionSupport.php | 89 + .../dav/lib/CalDAV/Backend/SyncSupport.php | 83 + .../vendor/sabre/dav/lib/CalDAV/Calendar.php | 460 +++ .../sabre/dav/lib/CalDAV/CalendarHome.php | 356 +++ .../sabre/dav/lib/CalDAV/CalendarObject.php | 223 ++ .../dav/lib/CalDAV/CalendarQueryValidator.php | 354 +++ .../sabre/dav/lib/CalDAV/CalendarRoot.php | 75 + .../CalDAV/Exception/InvalidComponentType.php | 31 + .../sabre/dav/lib/CalDAV/ICSExportPlugin.php | 377 +++ .../vendor/sabre/dav/lib/CalDAV/ICalendar.php | 20 + .../sabre/dav/lib/CalDAV/ICalendarObject.php | 23 + .../lib/CalDAV/ICalendarObjectContainer.php | 39 + .../sabre/dav/lib/CalDAV/ISharedCalendar.php | 27 + .../lib/CalDAV/Notifications/Collection.php | 96 + .../lib/CalDAV/Notifications/ICollection.php | 25 + .../dav/lib/CalDAV/Notifications/INode.php | 41 + .../dav/lib/CalDAV/Notifications/Node.php | 112 + .../dav/lib/CalDAV/Notifications/Plugin.php | 161 + .../vendor/sabre/dav/lib/CalDAV/Plugin.php | 1011 ++++++ .../dav/lib/CalDAV/Principal/Collection.php | 32 + .../dav/lib/CalDAV/Principal/IProxyRead.php | 21 + .../dav/lib/CalDAV/Principal/IProxyWrite.php | 21 + .../dav/lib/CalDAV/Principal/ProxyRead.php | 161 + .../dav/lib/CalDAV/Principal/ProxyWrite.php | 161 + .../sabre/dav/lib/CalDAV/Principal/User.php | 136 + .../sabre/dav/lib/CalDAV/Schedule/IInbox.php | 17 + .../dav/lib/CalDAV/Schedule/IMipPlugin.php | 178 ++ .../sabre/dav/lib/CalDAV/Schedule/IOutbox.php | 17 + .../lib/CalDAV/Schedule/ISchedulingObject.php | 15 + .../sabre/dav/lib/CalDAV/Schedule/Inbox.php | 198 ++ .../sabre/dav/lib/CalDAV/Schedule/Outbox.php | 119 + .../sabre/dav/lib/CalDAV/Schedule/Plugin.php | 1006 ++++++ .../lib/CalDAV/Schedule/SchedulingObject.php | 130 + .../sabre/dav/lib/CalDAV/SharedCalendar.php | 219 ++ .../sabre/dav/lib/CalDAV/SharingPlugin.php | 346 +++ .../CalDAV/Subscriptions/ISubscription.php | 41 + .../dav/lib/CalDAV/Subscriptions/Plugin.php | 108 + .../lib/CalDAV/Subscriptions/Subscription.php | 204 ++ .../lib/CalDAV/Xml/Filter/CalendarData.php | 80 + .../dav/lib/CalDAV/Xml/Filter/CompFilter.php | 94 + .../dav/lib/CalDAV/Xml/Filter/ParamFilter.php | 79 + .../dav/lib/CalDAV/Xml/Filter/PropFilter.php | 95 + .../lib/CalDAV/Xml/Notification/Invite.php | 290 ++ .../CalDAV/Xml/Notification/InviteReply.php | 199 ++ .../Notification/NotificationInterface.php | 43 + .../CalDAV/Xml/Notification/SystemStatus.php | 171 ++ .../Xml/Property/AllowedSharingModes.php | 81 + .../CalDAV/Xml/Property/EmailAddressSet.php | 71 + .../dav/lib/CalDAV/Xml/Property/Invite.php | 120 + .../Xml/Property/ScheduleCalendarTransp.php | 124 + .../SupportedCalendarComponentSet.php | 118 + .../Xml/Property/SupportedCalendarData.php | 57 + .../Xml/Property/SupportedCollationSet.php | 54 + .../Xml/Request/CalendarMultiGetReport.php | 119 + .../Xml/Request/CalendarQueryReport.php | 137 + .../Xml/Request/FreeBusyQueryReport.php | 90 + .../lib/CalDAV/Xml/Request/InviteReply.php | 145 + .../dav/lib/CalDAV/Xml/Request/MkCalendar.php | 77 + .../dav/lib/CalDAV/Xml/Request/Share.php | 107 + .../sabre/dav/lib/CardDAV/AddressBook.php | 335 ++ .../sabre/dav/lib/CardDAV/AddressBookHome.php | 178 ++ .../sabre/dav/lib/CardDAV/AddressBookRoot.php | 75 + .../lib/CardDAV/Backend/AbstractBackend.php | 38 + .../lib/CardDAV/Backend/BackendInterface.php | 194 ++ .../sabre/dav/lib/CardDAV/Backend/PDO.php | 539 ++++ .../dav/lib/CardDAV/Backend/SyncSupport.php | 83 + .../vendor/sabre/dav/lib/CardDAV/Card.php | 202 ++ .../sabre/dav/lib/CardDAV/IAddressBook.php | 20 + .../vendor/sabre/dav/lib/CardDAV/ICard.php | 21 + .../sabre/dav/lib/CardDAV/IDirectory.php | 22 + .../vendor/sabre/dav/lib/CardDAV/Plugin.php | 886 ++++++ .../sabre/dav/lib/CardDAV/VCFExportPlugin.php | 165 + .../lib/CardDAV/Xml/Filter/AddressData.php | 66 + .../lib/CardDAV/Xml/Filter/ParamFilter.php | 86 + .../dav/lib/CardDAV/Xml/Filter/PropFilter.php | 95 + .../Xml/Property/SupportedAddressData.php | 77 + .../Xml/Property/SupportedCollationSet.php | 44 + .../Xml/Request/AddressBookMultiGetReport.php | 116 + .../Xml/Request/AddressBookQueryReport.php | 193 ++ .../lib/DAV/Auth/Backend/AbstractBasic.php | 136 + .../lib/DAV/Auth/Backend/AbstractBearer.php | 130 + .../lib/DAV/Auth/Backend/AbstractDigest.php | 160 + .../sabre/dav/lib/DAV/Auth/Backend/Apache.php | 93 + .../lib/DAV/Auth/Backend/BackendInterface.php | 65 + .../lib/DAV/Auth/Backend/BasicCallBack.php | 56 + .../sabre/dav/lib/DAV/Auth/Backend/File.php | 74 + .../sabre/dav/lib/DAV/Auth/Backend/IMAP.php | 82 + .../sabre/dav/lib/DAV/Auth/Backend/PDO.php | 55 + .../dav/lib/DAV/Auth/Backend/PDOBasicAuth.php | 114 + .../vendor/sabre/dav/lib/DAV/Auth/Plugin.php | 255 ++ .../dav/lib/DAV/Browser/GuessContentType.php | 93 + .../sabre/dav/lib/DAV/Browser/HtmlOutput.php | 34 + .../dav/lib/DAV/Browser/HtmlOutputHelper.php | 118 + .../dav/lib/DAV/Browser/MapGetToPropFind.php | 58 + .../sabre/dav/lib/DAV/Browser/Plugin.php | 789 +++++ .../sabre/dav/lib/DAV/Browser/PropFindAll.php | 128 + .../dav/lib/DAV/Browser/assets/favicon.ico | Bin 0 -> 4286 bytes .../Browser/assets/openiconic/ICON-LICENSE | 21 + .../Browser/assets/openiconic/open-iconic.css | 510 +++ .../Browser/assets/openiconic/open-iconic.eot | Bin 0 -> 23144 bytes .../Browser/assets/openiconic/open-iconic.otf | Bin 0 -> 21048 bytes .../Browser/assets/openiconic/open-iconic.svg | 543 ++++ .../Browser/assets/openiconic/open-iconic.ttf | Bin 0 -> 25568 bytes .../assets/openiconic/open-iconic.woff | Bin 0 -> 12404 bytes .../dav/lib/DAV/Browser/assets/sabredav.css | 228 ++ .../dav/lib/DAV/Browser/assets/sabredav.png | Bin 0 -> 2825 bytes .../vendor/sabre/dav/lib/DAV/Client.php | 485 +++ .../vendor/sabre/dav/lib/DAV/Collection.php | 106 + .../vendor/sabre/dav/lib/DAV/CorePlugin.php | 907 ++++++ .../vendor/sabre/dav/lib/DAV/Exception.php | 50 + .../dav/lib/DAV/Exception/BadRequest.php | 30 + .../sabre/dav/lib/DAV/Exception/Conflict.php | 30 + .../dav/lib/DAV/Exception/ConflictingLock.php | 32 + .../sabre/dav/lib/DAV/Exception/Forbidden.php | 29 + .../lib/DAV/Exception/InsufficientStorage.php | 29 + .../lib/DAV/Exception/InvalidResourceType.php | 29 + .../lib/DAV/Exception/InvalidSyncToken.php | 34 + .../dav/lib/DAV/Exception/LengthRequired.php | 30 + .../Exception/LockTokenMatchesRequestUri.php | 36 + .../sabre/dav/lib/DAV/Exception/Locked.php | 68 + .../lib/DAV/Exception/MethodNotAllowed.php | 46 + .../lib/DAV/Exception/NotAuthenticated.php | 30 + .../sabre/dav/lib/DAV/Exception/NotFound.php | 29 + .../dav/lib/DAV/Exception/NotImplemented.php | 29 + .../dav/lib/DAV/Exception/PaymentRequired.php | 30 + .../lib/DAV/Exception/PreconditionFailed.php | 65 + .../lib/DAV/Exception/ReportNotSupported.php | 28 + .../RequestedRangeNotSatisfiable.php | 30 + .../lib/DAV/Exception/ServiceUnavailable.php | 30 + .../dav/lib/DAV/Exception/TooManyMatches.php | 34 + .../DAV/Exception/UnsupportedMediaType.php | 30 + .../vendor/sabre/dav/lib/DAV/FS/Directory.php | 147 + .../vendor/sabre/dav/lib/DAV/FS/File.php | 87 + .../vendor/sabre/dav/lib/DAV/FS/Node.php | 96 + .../sabre/dav/lib/DAV/FSExt/Directory.php | 212 ++ .../vendor/sabre/dav/lib/DAV/FSExt/File.php | 153 + .../vendor/sabre/dav/lib/DAV/File.php | 93 + .../vendor/sabre/dav/lib/DAV/ICollection.php | 79 + .../vendor/sabre/dav/lib/DAV/ICopyTarget.php | 38 + .../sabre/dav/lib/DAV/IExtendedCollection.php | 43 + .../vendor/sabre/dav/lib/DAV/IFile.php | 83 + .../vendor/sabre/dav/lib/DAV/IMoveTarget.php | 46 + .../vendor/sabre/dav/lib/DAV/IMultiGet.php | 38 + .../vendor/sabre/dav/lib/DAV/INode.php | 44 + .../vendor/sabre/dav/lib/DAV/INodeByPath.php | 36 + .../vendor/sabre/dav/lib/DAV/IProperties.php | 46 + .../vendor/sabre/dav/lib/DAV/IQuota.php | 27 + .../lib/DAV/Locks/Backend/AbstractBackend.php | 20 + .../DAV/Locks/Backend/BackendInterface.php | 52 + .../sabre/dav/lib/DAV/Locks/Backend/File.php | 182 ++ .../sabre/dav/lib/DAV/Locks/Backend/PDO.php | 172 ++ .../sabre/dav/lib/DAV/Locks/LockInfo.php | 82 + .../vendor/sabre/dav/lib/DAV/Locks/Plugin.php | 550 ++++ .../vendor/sabre/dav/lib/DAV/MkCol.php | 71 + .../vendor/sabre/dav/lib/DAV/Mount/Plugin.php | 78 + .../vendor/sabre/dav/lib/DAV/Node.php | 51 + .../lib/DAV/PartialUpdate/IPatchSupport.php | 49 + .../dav/lib/DAV/PartialUpdate/Plugin.php | 212 ++ .../vendor/sabre/dav/lib/DAV/PropFind.php | 335 ++ .../vendor/sabre/dav/lib/DAV/PropPatch.php | 337 ++ .../Backend/BackendInterface.php | 75 + .../lib/DAV/PropertyStorage/Backend/PDO.php | 224 ++ .../dav/lib/DAV/PropertyStorage/Plugin.php | 176 ++ .../vendor/sabre/dav/lib/DAV/Server.php | 1682 ++++++++++ .../vendor/sabre/dav/lib/DAV/ServerPlugin.php | 105 + .../sabre/dav/lib/DAV/Sharing/ISharedNode.php | 69 + .../sabre/dav/lib/DAV/Sharing/Plugin.php | 312 ++ .../sabre/dav/lib/DAV/SimpleCollection.php | 109 + .../vendor/sabre/dav/lib/DAV/SimpleFile.php | 118 + .../vendor/sabre/dav/lib/DAV/StringUtil.php | 86 + .../dav/lib/DAV/Sync/ISyncCollection.php | 90 + .../vendor/sabre/dav/lib/DAV/Sync/Plugin.php | 249 ++ .../dav/lib/DAV/TemporaryFileFilterPlugin.php | 298 ++ .../vendor/sabre/dav/lib/DAV/Tree.php | 342 +++ .../vendor/sabre/dav/lib/DAV/UUIDUtil.php | 66 + .../vendor/sabre/dav/lib/DAV/Version.php | 20 + .../sabre/dav/lib/DAV/Xml/Element/Prop.php | 110 + .../dav/lib/DAV/Xml/Element/Response.php | 267 ++ .../sabre/dav/lib/DAV/Xml/Element/Sharee.php | 189 ++ .../dav/lib/DAV/Xml/Property/Complex.php | 87 + .../lib/DAV/Xml/Property/GetLastModified.php | 103 + .../sabre/dav/lib/DAV/Xml/Property/Href.php | 166 + .../sabre/dav/lib/DAV/Xml/Property/Invite.php | 66 + .../dav/lib/DAV/Xml/Property/LocalHref.php | 48 + .../lib/DAV/Xml/Property/LockDiscovery.php | 105 + .../dav/lib/DAV/Xml/Property/ResourceType.php | 120 + .../dav/lib/DAV/Xml/Property/ShareAccess.php | 135 + .../lib/DAV/Xml/Property/SupportedLock.php | 52 + .../DAV/Xml/Property/SupportedMethodSet.php | 114 + .../DAV/Xml/Property/SupportedReportSet.php | 144 + .../sabre/dav/lib/DAV/Xml/Request/Lock.php | 84 + .../sabre/dav/lib/DAV/Xml/Request/MkCol.php | 80 + .../dav/lib/DAV/Xml/Request/PropFind.php | 79 + .../dav/lib/DAV/Xml/Request/PropPatch.php | 109 + .../dav/lib/DAV/Xml/Request/ShareResource.php | 80 + .../DAV/Xml/Request/SyncCollectionReport.php | 118 + .../dav/lib/DAV/Xml/Response/MultiStatus.php | 136 + .../vendor/sabre/dav/lib/DAV/Xml/Service.php | 47 + .../vendor/sabre/dav/lib/DAVACL/ACLTrait.php | 94 + .../DAVACL/AbstractPrincipalCollection.php | 178 ++ .../dav/lib/DAVACL/Exception/AceConflict.php | 31 + .../lib/DAVACL/Exception/NeedPrivileges.php | 73 + .../dav/lib/DAVACL/Exception/NoAbstract.php | 31 + .../Exception/NotRecognizedPrincipal.php | 31 + .../Exception/NotSupportedPrivilege.php | 31 + .../sabre/dav/lib/DAVACL/FS/Collection.php | 109 + .../vendor/sabre/dav/lib/DAVACL/FS/File.php | 78 + .../dav/lib/DAVACL/FS/HomeCollection.php | 123 + .../vendor/sabre/dav/lib/DAVACL/IACL.php | 72 + .../sabre/dav/lib/DAVACL/IPrincipal.php | 75 + .../dav/lib/DAVACL/IPrincipalCollection.php | 64 + .../vendor/sabre/dav/lib/DAVACL/Plugin.php | 1545 ++++++++++ .../vendor/sabre/dav/lib/DAVACL/Principal.php | 199 ++ .../PrincipalBackend/AbstractBackend.php | 54 + .../PrincipalBackend/BackendInterface.php | 143 + .../CreatePrincipalSupport.php | 29 + .../dav/lib/DAVACL/PrincipalBackend/PDO.php | 443 +++ .../dav/lib/DAVACL/PrincipalCollection.php | 96 + .../sabre/dav/lib/DAVACL/Xml/Property/Acl.php | 257 ++ .../DAVACL/Xml/Property/AclRestrictions.php | 42 + .../Xml/Property/CurrentUserPrivilegeSet.php | 145 + .../dav/lib/DAVACL/Xml/Property/Principal.php | 186 ++ .../Xml/Property/SupportedPrivilegeSet.php | 146 + .../Xml/Request/AclPrincipalPropSetReport.php | 66 + .../Xml/Request/ExpandPropertyReport.php | 100 + .../Xml/Request/PrincipalMatchReport.php | 106 + .../Request/PrincipalPropertySearchReport.php | 122 + .../PrincipalSearchPropertySetReport.php | 58 + .../vendor/sabre/event/.php-cs-fixer.dist.php | 18 + lib/composer/vendor/sabre/event/LICENSE | 27 + lib/composer/vendor/sabre/event/composer.json | 69 + .../vendor/sabre/event/lib/Emitter.php | 19 + .../sabre/event/lib/EmitterInterface.php | 78 + .../vendor/sabre/event/lib/EmitterTrait.php | 178 ++ .../vendor/sabre/event/lib/EventEmitter.php | 20 + .../vendor/sabre/event/lib/Loop/Loop.php | 343 +++ .../vendor/sabre/event/lib/Loop/functions.php | 143 + .../vendor/sabre/event/lib/Promise.php | 253 ++ .../sabre/event/lib/Promise/functions.php | 125 + .../lib/PromiseAlreadyResolvedException.php | 17 + .../vendor/sabre/event/lib/Version.php | 20 + .../sabre/event/lib/WildcardEmitter.php | 36 + .../sabre/event/lib/WildcardEmitterTrait.php | 233 ++ .../vendor/sabre/event/lib/coroutine.php | 122 + .../sabre/http/.github/workflows/ci.yml | 68 + lib/composer/vendor/sabre/http/.gitignore | 9 + .../vendor/sabre/http/.php-cs-fixer.dist.php | 17 + lib/composer/vendor/sabre/http/.php_cs.dist | 12 + lib/composer/vendor/sabre/http/CHANGELOG.md | 398 +++ lib/composer/vendor/sabre/http/LICENSE | 27 + lib/composer/vendor/sabre/http/README.md | 747 +++++ lib/composer/vendor/sabre/http/bin/.empty | 0 lib/composer/vendor/sabre/http/composer.json | 64 + .../sabre/http/examples/asyncclient.php | 62 + .../vendor/sabre/http/examples/basicauth.php | 50 + .../vendor/sabre/http/examples/client.php | 37 + .../vendor/sabre/http/examples/digestauth.php | 51 + .../sabre/http/examples/reverseproxy.php | 48 + .../vendor/sabre/http/examples/stringify.php | 50 + .../vendor/sabre/http/lib/Auth/AWS.php | 220 ++ .../sabre/http/lib/Auth/AbstractAuth.php | 65 + .../vendor/sabre/http/lib/Auth/Basic.php | 60 + .../vendor/sabre/http/lib/Auth/Bearer.php | 53 + .../vendor/sabre/http/lib/Auth/Digest.php | 208 ++ lib/composer/vendor/sabre/http/lib/Client.php | 620 ++++ .../vendor/sabre/http/lib/ClientException.php | 17 + .../sabre/http/lib/ClientHttpException.php | 50 + .../vendor/sabre/http/lib/HttpException.php | 31 + .../vendor/sabre/http/lib/Message.php | 291 ++ .../sabre/http/lib/MessageDecoratorTrait.php | 206 ++ .../sabre/http/lib/MessageInterface.php | 151 + .../vendor/sabre/http/lib/Request.php | 266 ++ .../sabre/http/lib/RequestDecorator.php | 179 ++ .../sabre/http/lib/RequestInterface.php | 114 + .../vendor/sabre/http/lib/Response.php | 187 ++ .../sabre/http/lib/ResponseDecorator.php | 72 + .../sabre/http/lib/ResponseInterface.php | 42 + lib/composer/vendor/sabre/http/lib/Sapi.php | 240 ++ .../vendor/sabre/http/lib/Version.php | 20 + .../vendor/sabre/http/lib/functions.php | 410 +++ lib/composer/vendor/sabre/http/phpstan.neon | 2 + .../sabre/http/tests/HTTP/Auth/AWSTest.php | 236 ++ .../sabre/http/tests/HTTP/Auth/BasicTest.php | 67 + .../sabre/http/tests/HTTP/Auth/BearerTest.php | 55 + .../sabre/http/tests/HTTP/Auth/DigestTest.php | 185 ++ .../sabre/http/tests/HTTP/ClientTest.php | 605 ++++ .../sabre/http/tests/HTTP/FunctionsTest.php | 212 ++ .../http/tests/HTTP/MessageDecoratorTest.php | 91 + .../sabre/http/tests/HTTP/MessageTest.php | 281 ++ .../sabre/http/tests/HTTP/NegotiateTest.php | 135 + .../http/tests/HTTP/RequestDecoratorTest.php | 103 + .../sabre/http/tests/HTTP/RequestTest.php | 137 + .../http/tests/HTTP/ResponseDecoratorTest.php | 35 + .../sabre/http/tests/HTTP/ResponseTest.php | 41 + .../vendor/sabre/http/tests/HTTP/SapiTest.php | 326 ++ .../sabre/http/tests/HTTP/URLUtilTest.php | 114 + .../vendor/sabre/http/tests/bootstrap.php | 17 + .../vendor/sabre/http/tests/phpunit.xml | 27 + .../vendor/sabre/http/tests/www/bar.php | 5 + .../http/tests/www/connection_aborted.php | 69 + lib/composer/vendor/sabre/http/tests/www/foo | 1 + .../vendor/sabre/http/tests/www/large.php | 7 + .../vendor/sabre/uri/.php-cs-fixer.dist.php | 17 + lib/composer/vendor/sabre/uri/LICENSE | 27 + lib/composer/vendor/sabre/uri/composer.json | 68 + .../sabre/uri/lib/InvalidUriException.php | 19 + lib/composer/vendor/sabre/uri/lib/Version.php | 20 + .../vendor/sabre/uri/lib/functions.php | 412 +++ lib/composer/vendor/sabre/vobject/LICENSE | 27 + lib/composer/vendor/sabre/vobject/README.md | 55 + .../vendor/sabre/vobject/bin/bench.php | 12 + .../vobject/bin/bench_freebusygenerator.php | 53 + .../vobject/bin/bench_manipulatevcard.php | 64 + .../sabre/vobject/bin/fetch_windows_zones.php | 48 + .../vendor/sabre/vobject/bin/generate_vcards | 241 ++ .../vobject/bin/generateicalendardata.php | 87 + .../sabre/vobject/bin/mergeduplicates.php | 160 + .../vendor/sabre/vobject/bin/rrulebench.php | 32 + lib/composer/vendor/sabre/vobject/bin/vobject | 27 + .../vendor/sabre/vobject/composer.json | 107 + .../vobject/lib/BirthdayCalendarGenerator.php | 172 ++ lib/composer/vendor/sabre/vobject/lib/Cli.php | 705 +++++ .../vendor/sabre/vobject/lib/Component.php | 672 ++++ .../sabre/vobject/lib/Component/Available.php | 123 + .../sabre/vobject/lib/Component/VAlarm.php | 138 + .../vobject/lib/Component/VAvailability.php | 149 + .../sabre/vobject/lib/Component/VCalendar.php | 528 ++++ .../sabre/vobject/lib/Component/VCard.php | 541 ++++ .../sabre/vobject/lib/Component/VEvent.php | 140 + .../sabre/vobject/lib/Component/VFreeBusy.php | 93 + .../sabre/vobject/lib/Component/VJournal.php | 101 + .../sabre/vobject/lib/Component/VTimeZone.php | 63 + .../sabre/vobject/lib/Component/VTodo.php | 181 ++ .../sabre/vobject/lib/DateTimeParser.php | 560 ++++ .../vendor/sabre/vobject/lib/Document.php | 269 ++ .../vendor/sabre/vobject/lib/ElementList.php | 52 + .../vendor/sabre/vobject/lib/EofException.php | 15 + .../vendor/sabre/vobject/lib/FreeBusyData.php | 185 ++ .../sabre/vobject/lib/FreeBusyGenerator.php | 549 ++++ .../vendor/sabre/vobject/lib/ITip/Broker.php | 986 ++++++ .../sabre/vobject/lib/ITip/ITipException.php | 16 + .../vendor/sabre/vobject/lib/ITip/Message.php | 136 + ...SameOrganizerForAllComponentsException.php | 18 + .../vobject/lib/InvalidDataException.php | 15 + .../vendor/sabre/vobject/lib/Node.php | 256 ++ .../sabre/vobject/lib/PHPUnitAssertions.php | 75 + .../vendor/sabre/vobject/lib/Parameter.php | 368 +++ .../sabre/vobject/lib/ParseException.php | 14 + .../vendor/sabre/vobject/lib/Parser/Json.php | 190 ++ .../sabre/vobject/lib/Parser/MimeDir.php | 689 +++++ .../sabre/vobject/lib/Parser/Parser.php | 75 + .../vendor/sabre/vobject/lib/Parser/XML.php | 377 +++ .../lib/Parser/XML/Element/KeyValue.php | 63 + .../vendor/sabre/vobject/lib/Property.php | 646 ++++ .../sabre/vobject/lib/Property/Binary.php | 109 + .../sabre/vobject/lib/Property/Boolean.php | 72 + .../sabre/vobject/lib/Property/FlatText.php | 46 + .../sabre/vobject/lib/Property/FloatValue.php | 124 + .../lib/Property/ICalendar/CalAddress.php | 63 + .../vobject/lib/Property/ICalendar/Date.php | 18 + .../lib/Property/ICalendar/DateTime.php | 366 +++ .../lib/Property/ICalendar/Duration.php | 79 + .../vobject/lib/Property/ICalendar/Period.php | 135 + .../vobject/lib/Property/ICalendar/Recur.php | 344 +++ .../vobject/lib/Property/IntegerValue.php | 76 + .../sabre/vobject/lib/Property/Text.php | 392 +++ .../sabre/vobject/lib/Property/Time.php | 131 + .../sabre/vobject/lib/Property/Unknown.php | 41 + .../vendor/sabre/vobject/lib/Property/Uri.php | 116 + .../sabre/vobject/lib/Property/UtcOffset.php | 70 + .../sabre/vobject/lib/Property/VCard/Date.php | 36 + .../lib/Property/VCard/DateAndOrTime.php | 367 +++ .../vobject/lib/Property/VCard/DateTime.php | 28 + .../lib/Property/VCard/LanguageTag.php | 53 + .../lib/Property/VCard/PhoneNumber.php | 30 + .../vobject/lib/Property/VCard/TimeStamp.php | 81 + .../vendor/sabre/vobject/lib/Reader.php | 95 + .../sabre/vobject/lib/Recur/EventIterator.php | 497 +++ .../Recur/MaxInstancesExceededException.php | 17 + .../lib/Recur/NoInstancesException.php | 18 + .../sabre/vobject/lib/Recur/RDateIterator.php | 175 ++ .../sabre/vobject/lib/Recur/RRuleIterator.php | 1079 +++++++ .../vendor/sabre/vobject/lib/Settings.php | 55 + .../sabre/vobject/lib/Splitter/ICalendar.php | 106 + .../lib/Splitter/SplitterInterface.php | 38 + .../sabre/vobject/lib/Splitter/VCard.php | 74 + .../vendor/sabre/vobject/lib/StringUtil.php | 50 + .../vendor/sabre/vobject/lib/TimeZoneUtil.php | 272 ++ .../lib/TimezoneGuesser/FindFromOffset.php | 31 + .../FindFromTimezoneIdentifier.php | 71 + .../TimezoneGuesser/FindFromTimezoneMap.php | 78 + .../lib/TimezoneGuesser/GuessFromLicEntry.php | 33 + .../lib/TimezoneGuesser/GuessFromMsTzId.php | 119 + .../lib/TimezoneGuesser/TimezoneFinder.php | 10 + .../lib/TimezoneGuesser/TimezoneGuesser.php | 11 + .../vendor/sabre/vobject/lib/UUIDUtil.php | 66 + .../sabre/vobject/lib/VCardConverter.php | 421 +++ .../vendor/sabre/vobject/lib/Version.php | 18 + .../vendor/sabre/vobject/lib/Writer.php | 68 + .../lib/timezonedata/exchangezones.php | 95 + .../vobject/lib/timezonedata/lotuszones.php | 101 + .../sabre/vobject/lib/timezonedata/php-bc.php | 152 + .../lib/timezonedata/php-workaround.php | 46 + .../vobject/lib/timezonedata/windowszones.php | 152 + .../sabre/vobject/resources/schema/xcal.rng | 1192 +++++++ .../sabre/vobject/resources/schema/xcard.rng | 388 +++ .../vendor/sabre/xml/.github/workflows/ci.yml | 69 + .../vendor/sabre/xml/.php-cs-fixer.dist.php | 17 + lib/composer/vendor/sabre/xml/LICENSE | 27 + lib/composer/vendor/sabre/xml/README.md | 25 + lib/composer/vendor/sabre/xml/bin/.empty | 0 lib/composer/vendor/sabre/xml/composer.json | 67 + .../sabre/xml/lib/ContextStackTrait.php | 118 + .../sabre/xml/lib/Deserializer/functions.php | 360 +++ lib/composer/vendor/sabre/xml/lib/Element.php | 22 + .../vendor/sabre/xml/lib/Element/Base.php | 80 + .../vendor/sabre/xml/lib/Element/Cdata.php | 59 + .../vendor/sabre/xml/lib/Element/Elements.php | 98 + .../vendor/sabre/xml/lib/Element/KeyValue.php | 98 + .../vendor/sabre/xml/lib/Element/Uri.php | 97 + .../sabre/xml/lib/Element/XmlFragment.php | 146 + .../vendor/sabre/xml/lib/LibXMLException.php | 47 + .../vendor/sabre/xml/lib/ParseException.php | 18 + lib/composer/vendor/sabre/xml/lib/Reader.php | 307 ++ .../sabre/xml/lib/Serializer/functions.php | 207 ++ lib/composer/vendor/sabre/xml/lib/Service.php | 326 ++ lib/composer/vendor/sabre/xml/lib/Version.php | 20 + lib/composer/vendor/sabre/xml/lib/Writer.php | 255 ++ .../sabre/xml/lib/XmlDeserializable.php | 36 + .../vendor/sabre/xml/lib/XmlSerializable.php | 34 + readme.md | 34 + source/conf.php | 37 + source/helpers/cache.php | 370 +++ source/helpers/call.php | 21 + source/helpers/ics.php | 793 +++++ source/helpers/pit.php | 241 ++ source/helpers/string.php | 39 + source/main.php | 61 + source/overwrites/auths/_factory.php | 37 + source/overwrites/auths/basic.php | 51 + source/overwrites/auths/none.php | 39 + source/overwrites/caldav_backend.php | 295 ++ source/overwrites/principal_backend.php | 94 + source/sources/_factory.php | 36 + source/sources/_interface.php | 39 + source/sources/ics_feed.php | 115 + tools/build | 23 + tools/run | 4 + tools/update-libs | 8 + 528 files changed, 82278 insertions(+) create mode 100644 .gitignore create mode 100644 conf/example.json create mode 100644 lib/composer/composer.json create mode 100644 lib/composer/composer.lock create mode 100644 lib/composer/vendor/autoload.php create mode 100755 lib/composer/vendor/bin/generate_vcards create mode 100755 lib/composer/vendor/bin/naturalselection create mode 100755 lib/composer/vendor/bin/sabredav create mode 100755 lib/composer/vendor/bin/vobject create mode 100644 lib/composer/vendor/composer/ClassLoader.php create mode 100644 lib/composer/vendor/composer/InstalledVersions.php create mode 100644 lib/composer/vendor/composer/LICENSE create mode 100644 lib/composer/vendor/composer/autoload_classmap.php create mode 100644 lib/composer/vendor/composer/autoload_files.php create mode 100644 lib/composer/vendor/composer/autoload_namespaces.php create mode 100644 lib/composer/vendor/composer/autoload_psr4.php create mode 100644 lib/composer/vendor/composer/autoload_real.php create mode 100644 lib/composer/vendor/composer/autoload_static.php create mode 100644 lib/composer/vendor/composer/installed.json create mode 100644 lib/composer/vendor/composer/installed.php create mode 100644 lib/composer/vendor/composer/platform_check.php create mode 100644 lib/composer/vendor/johngrogg/ics-parser/.editorconfig create mode 100644 lib/composer/vendor/johngrogg/ics-parser/.github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 lib/composer/vendor/johngrogg/ics-parser/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md create mode 100644 lib/composer/vendor/johngrogg/ics-parser/.github/dependabot.yml create mode 100644 lib/composer/vendor/johngrogg/ics-parser/.github/release_template.md create mode 100644 lib/composer/vendor/johngrogg/ics-parser/.github/workflows/coding-standards.yml create mode 100644 lib/composer/vendor/johngrogg/ics-parser/.gitignore create mode 100644 lib/composer/vendor/johngrogg/ics-parser/CONTRIBUTING.md create mode 100644 lib/composer/vendor/johngrogg/ics-parser/FUNDING.yml create mode 100644 lib/composer/vendor/johngrogg/ics-parser/LICENSE create mode 100644 lib/composer/vendor/johngrogg/ics-parser/README.md create mode 100644 lib/composer/vendor/johngrogg/ics-parser/composer.json create mode 100644 lib/composer/vendor/johngrogg/ics-parser/composer.lock create mode 100644 lib/composer/vendor/johngrogg/ics-parser/ecs.php create mode 100644 lib/composer/vendor/johngrogg/ics-parser/examples/ICal.ics create mode 100644 lib/composer/vendor/johngrogg/ics-parser/examples/index.php create mode 100644 lib/composer/vendor/johngrogg/ics-parser/phpstan.neon.dist create mode 100644 lib/composer/vendor/johngrogg/ics-parser/phpunit.xml create mode 100644 lib/composer/vendor/johngrogg/ics-parser/rector.php create mode 100644 lib/composer/vendor/johngrogg/ics-parser/src/ICal/Event.php create mode 100644 lib/composer/vendor/johngrogg/ics-parser/src/ICal/ICal.php create mode 100644 lib/composer/vendor/johngrogg/ics-parser/tests/CleanCharacterTest.php create mode 100644 lib/composer/vendor/johngrogg/ics-parser/tests/DynamicPropertiesTest.php create mode 100644 lib/composer/vendor/johngrogg/ics-parser/tests/KeyValueTest.php create mode 100644 lib/composer/vendor/johngrogg/ics-parser/tests/RecurrencesTest.php create mode 100644 lib/composer/vendor/johngrogg/ics-parser/tests/Rfc5545RecurrenceTest.php create mode 100644 lib/composer/vendor/johngrogg/ics-parser/tests/SingleEventsTest.php create mode 100644 lib/composer/vendor/johngrogg/ics-parser/tests/ical/ical-monthly.ics create mode 100644 lib/composer/vendor/johngrogg/ics-parser/tests/ical/issue-196.ics create mode 100644 lib/composer/vendor/psr/log/LICENSE create mode 100644 lib/composer/vendor/psr/log/README.md create mode 100644 lib/composer/vendor/psr/log/composer.json create mode 100644 lib/composer/vendor/psr/log/src/AbstractLogger.php create mode 100644 lib/composer/vendor/psr/log/src/InvalidArgumentException.php create mode 100644 lib/composer/vendor/psr/log/src/LogLevel.php create mode 100644 lib/composer/vendor/psr/log/src/LoggerAwareInterface.php create mode 100644 lib/composer/vendor/psr/log/src/LoggerAwareTrait.php create mode 100644 lib/composer/vendor/psr/log/src/LoggerInterface.php create mode 100644 lib/composer/vendor/psr/log/src/LoggerTrait.php create mode 100644 lib/composer/vendor/psr/log/src/NullLogger.php create mode 100644 lib/composer/vendor/sabre/dav/LICENSE create mode 100644 lib/composer/vendor/sabre/dav/README.md create mode 100755 lib/composer/vendor/sabre/dav/bin/build.php create mode 100755 lib/composer/vendor/sabre/dav/bin/migrateto20.php create mode 100755 lib/composer/vendor/sabre/dav/bin/migrateto21.php create mode 100755 lib/composer/vendor/sabre/dav/bin/migrateto30.php create mode 100755 lib/composer/vendor/sabre/dav/bin/migrateto32.php create mode 100755 lib/composer/vendor/sabre/dav/bin/naturalselection create mode 100755 lib/composer/vendor/sabre/dav/bin/sabredav create mode 100755 lib/composer/vendor/sabre/dav/bin/sabredav.php create mode 100644 lib/composer/vendor/sabre/dav/composer.json create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/BackendInterface.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/NotificationSupport.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/PDO.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/SharingSupport.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/SimplePDO.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/SyncSupport.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Calendar.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/CalendarHome.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/CalendarObject.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/CalendarQueryValidator.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/CalendarRoot.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/ICSExportPlugin.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/ICalendar.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/ICalendarObject.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/ICalendarObjectContainer.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/ISharedCalendar.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Notifications/Collection.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Notifications/ICollection.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Notifications/INode.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Notifications/Node.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Notifications/Plugin.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Plugin.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/Collection.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/IProxyRead.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/IProxyWrite.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/ProxyRead.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/ProxyWrite.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/User.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/IInbox.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/IMipPlugin.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/IOutbox.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/ISchedulingObject.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/Inbox.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/Outbox.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/Plugin.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/SharedCalendar.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/SharingPlugin.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Subscriptions/ISubscription.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Subscriptions/Plugin.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Filter/CalendarData.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Filter/CompFilter.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Filter/ParamFilter.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Filter/PropFilter.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Notification/NotificationInterface.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Notification/SystemStatus.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/AllowedSharingModes.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/EmailAddressSet.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/Invite.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/ScheduleCalendarTransp.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarComponentSet.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarData.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCollationSet.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/CalendarMultiGetReport.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/CalendarQueryReport.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/FreeBusyQueryReport.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/MkCalendar.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/Share.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CardDAV/AddressBook.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CardDAV/AddressBookHome.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CardDAV/AddressBookRoot.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CardDAV/Backend/AbstractBackend.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CardDAV/Backend/BackendInterface.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CardDAV/Backend/PDO.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CardDAV/Backend/SyncSupport.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CardDAV/Card.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CardDAV/IAddressBook.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CardDAV/ICard.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CardDAV/IDirectory.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CardDAV/Plugin.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CardDAV/VCFExportPlugin.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Filter/AddressData.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Filter/ParamFilter.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Filter/PropFilter.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Property/SupportedAddressData.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Property/SupportedCollationSet.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Request/AddressBookMultiGetReport.php create mode 100644 lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Request/AddressBookQueryReport.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractBasic.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractBearer.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractDigest.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/Apache.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/BackendInterface.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/BasicCallBack.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/File.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/IMAP.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/PDO.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/PDOBasicAuth.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Auth/Plugin.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Browser/GuessContentType.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Browser/HtmlOutput.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Browser/HtmlOutputHelper.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Browser/MapGetToPropFind.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Browser/Plugin.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Browser/PropFindAll.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/favicon.ico create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/ICON-LICENSE create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.css create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.eot create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.otf create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.svg create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.ttf create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.woff create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/sabredav.css create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/sabredav.png create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Client.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Collection.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/CorePlugin.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Exception.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Exception/BadRequest.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Exception/Conflict.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Exception/ConflictingLock.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Exception/Forbidden.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Exception/InsufficientStorage.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Exception/InvalidResourceType.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Exception/InvalidSyncToken.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Exception/LengthRequired.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Exception/LockTokenMatchesRequestUri.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Exception/Locked.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Exception/NotAuthenticated.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Exception/NotFound.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Exception/NotImplemented.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Exception/PaymentRequired.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Exception/PreconditionFailed.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Exception/ReportNotSupported.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Exception/RequestedRangeNotSatisfiable.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Exception/ServiceUnavailable.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Exception/TooManyMatches.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Exception/UnsupportedMediaType.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/FS/Directory.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/FS/File.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/FS/Node.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/FSExt/Directory.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/FSExt/File.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/File.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/ICollection.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/ICopyTarget.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/IExtendedCollection.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/IFile.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/IMoveTarget.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/IMultiGet.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/INode.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/INodeByPath.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/IProperties.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/IQuota.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Locks/Backend/AbstractBackend.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Locks/Backend/BackendInterface.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Locks/Backend/File.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Locks/Backend/PDO.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Locks/LockInfo.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Locks/Plugin.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/MkCol.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Mount/Plugin.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Node.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/PartialUpdate/IPatchSupport.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/PartialUpdate/Plugin.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/PropFind.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/PropPatch.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/BackendInterface.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/PropertyStorage/Plugin.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Server.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/ServerPlugin.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Sharing/ISharedNode.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Sharing/Plugin.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/SimpleCollection.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/SimpleFile.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/StringUtil.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Sync/ISyncCollection.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Sync/Plugin.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/TemporaryFileFilterPlugin.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Tree.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/UUIDUtil.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Version.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Xml/Element/Prop.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Xml/Element/Response.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Xml/Element/Sharee.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/Complex.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/GetLastModified.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/Href.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/Invite.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/LocalHref.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/LockDiscovery.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/ResourceType.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/ShareAccess.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedLock.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedMethodSet.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedReportSet.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/Lock.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/MkCol.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/PropFind.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/PropPatch.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/ShareResource.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/SyncCollectionReport.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Xml/Response/MultiStatus.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAV/Xml/Service.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/ACLTrait.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/AbstractPrincipalCollection.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/AceConflict.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/NeedPrivileges.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/NoAbstract.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/NotRecognizedPrincipal.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/NotSupportedPrivilege.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/FS/Collection.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/FS/File.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/FS/HomeCollection.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/IACL.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/IPrincipal.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/IPrincipalCollection.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/Plugin.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/Principal.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/AbstractBackend.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/BackendInterface.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/CreatePrincipalSupport.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/PDO.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/PrincipalCollection.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/Acl.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/AclRestrictions.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/CurrentUserPrivilegeSet.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/Principal.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Request/AclPrincipalPropSetReport.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Request/ExpandPropertyReport.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalMatchReport.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalPropertySearchReport.php create mode 100644 lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalSearchPropertySetReport.php create mode 100644 lib/composer/vendor/sabre/event/.php-cs-fixer.dist.php create mode 100644 lib/composer/vendor/sabre/event/LICENSE create mode 100644 lib/composer/vendor/sabre/event/composer.json create mode 100644 lib/composer/vendor/sabre/event/lib/Emitter.php create mode 100644 lib/composer/vendor/sabre/event/lib/EmitterInterface.php create mode 100644 lib/composer/vendor/sabre/event/lib/EmitterTrait.php create mode 100644 lib/composer/vendor/sabre/event/lib/EventEmitter.php create mode 100644 lib/composer/vendor/sabre/event/lib/Loop/Loop.php create mode 100644 lib/composer/vendor/sabre/event/lib/Loop/functions.php create mode 100644 lib/composer/vendor/sabre/event/lib/Promise.php create mode 100644 lib/composer/vendor/sabre/event/lib/Promise/functions.php create mode 100644 lib/composer/vendor/sabre/event/lib/PromiseAlreadyResolvedException.php create mode 100644 lib/composer/vendor/sabre/event/lib/Version.php create mode 100644 lib/composer/vendor/sabre/event/lib/WildcardEmitter.php create mode 100644 lib/composer/vendor/sabre/event/lib/WildcardEmitterTrait.php create mode 100644 lib/composer/vendor/sabre/event/lib/coroutine.php create mode 100644 lib/composer/vendor/sabre/http/.github/workflows/ci.yml create mode 100644 lib/composer/vendor/sabre/http/.gitignore create mode 100644 lib/composer/vendor/sabre/http/.php-cs-fixer.dist.php create mode 100644 lib/composer/vendor/sabre/http/.php_cs.dist create mode 100644 lib/composer/vendor/sabre/http/CHANGELOG.md create mode 100644 lib/composer/vendor/sabre/http/LICENSE create mode 100644 lib/composer/vendor/sabre/http/README.md create mode 100644 lib/composer/vendor/sabre/http/bin/.empty create mode 100644 lib/composer/vendor/sabre/http/composer.json create mode 100644 lib/composer/vendor/sabre/http/examples/asyncclient.php create mode 100644 lib/composer/vendor/sabre/http/examples/basicauth.php create mode 100644 lib/composer/vendor/sabre/http/examples/client.php create mode 100644 lib/composer/vendor/sabre/http/examples/digestauth.php create mode 100644 lib/composer/vendor/sabre/http/examples/reverseproxy.php create mode 100644 lib/composer/vendor/sabre/http/examples/stringify.php create mode 100644 lib/composer/vendor/sabre/http/lib/Auth/AWS.php create mode 100644 lib/composer/vendor/sabre/http/lib/Auth/AbstractAuth.php create mode 100644 lib/composer/vendor/sabre/http/lib/Auth/Basic.php create mode 100644 lib/composer/vendor/sabre/http/lib/Auth/Bearer.php create mode 100644 lib/composer/vendor/sabre/http/lib/Auth/Digest.php create mode 100644 lib/composer/vendor/sabre/http/lib/Client.php create mode 100644 lib/composer/vendor/sabre/http/lib/ClientException.php create mode 100644 lib/composer/vendor/sabre/http/lib/ClientHttpException.php create mode 100644 lib/composer/vendor/sabre/http/lib/HttpException.php create mode 100644 lib/composer/vendor/sabre/http/lib/Message.php create mode 100644 lib/composer/vendor/sabre/http/lib/MessageDecoratorTrait.php create mode 100644 lib/composer/vendor/sabre/http/lib/MessageInterface.php create mode 100644 lib/composer/vendor/sabre/http/lib/Request.php create mode 100644 lib/composer/vendor/sabre/http/lib/RequestDecorator.php create mode 100644 lib/composer/vendor/sabre/http/lib/RequestInterface.php create mode 100644 lib/composer/vendor/sabre/http/lib/Response.php create mode 100644 lib/composer/vendor/sabre/http/lib/ResponseDecorator.php create mode 100644 lib/composer/vendor/sabre/http/lib/ResponseInterface.php create mode 100644 lib/composer/vendor/sabre/http/lib/Sapi.php create mode 100644 lib/composer/vendor/sabre/http/lib/Version.php create mode 100644 lib/composer/vendor/sabre/http/lib/functions.php create mode 100644 lib/composer/vendor/sabre/http/phpstan.neon create mode 100644 lib/composer/vendor/sabre/http/tests/HTTP/Auth/AWSTest.php create mode 100644 lib/composer/vendor/sabre/http/tests/HTTP/Auth/BasicTest.php create mode 100644 lib/composer/vendor/sabre/http/tests/HTTP/Auth/BearerTest.php create mode 100644 lib/composer/vendor/sabre/http/tests/HTTP/Auth/DigestTest.php create mode 100644 lib/composer/vendor/sabre/http/tests/HTTP/ClientTest.php create mode 100644 lib/composer/vendor/sabre/http/tests/HTTP/FunctionsTest.php create mode 100644 lib/composer/vendor/sabre/http/tests/HTTP/MessageDecoratorTest.php create mode 100644 lib/composer/vendor/sabre/http/tests/HTTP/MessageTest.php create mode 100644 lib/composer/vendor/sabre/http/tests/HTTP/NegotiateTest.php create mode 100644 lib/composer/vendor/sabre/http/tests/HTTP/RequestDecoratorTest.php create mode 100644 lib/composer/vendor/sabre/http/tests/HTTP/RequestTest.php create mode 100644 lib/composer/vendor/sabre/http/tests/HTTP/ResponseDecoratorTest.php create mode 100644 lib/composer/vendor/sabre/http/tests/HTTP/ResponseTest.php create mode 100644 lib/composer/vendor/sabre/http/tests/HTTP/SapiTest.php create mode 100644 lib/composer/vendor/sabre/http/tests/HTTP/URLUtilTest.php create mode 100644 lib/composer/vendor/sabre/http/tests/bootstrap.php create mode 100644 lib/composer/vendor/sabre/http/tests/phpunit.xml create mode 100644 lib/composer/vendor/sabre/http/tests/www/bar.php create mode 100644 lib/composer/vendor/sabre/http/tests/www/connection_aborted.php create mode 100644 lib/composer/vendor/sabre/http/tests/www/foo create mode 100644 lib/composer/vendor/sabre/http/tests/www/large.php create mode 100644 lib/composer/vendor/sabre/uri/.php-cs-fixer.dist.php create mode 100644 lib/composer/vendor/sabre/uri/LICENSE create mode 100644 lib/composer/vendor/sabre/uri/composer.json create mode 100644 lib/composer/vendor/sabre/uri/lib/InvalidUriException.php create mode 100644 lib/composer/vendor/sabre/uri/lib/Version.php create mode 100644 lib/composer/vendor/sabre/uri/lib/functions.php create mode 100644 lib/composer/vendor/sabre/vobject/LICENSE create mode 100644 lib/composer/vendor/sabre/vobject/README.md create mode 100755 lib/composer/vendor/sabre/vobject/bin/bench.php create mode 100644 lib/composer/vendor/sabre/vobject/bin/bench_freebusygenerator.php create mode 100644 lib/composer/vendor/sabre/vobject/bin/bench_manipulatevcard.php create mode 100755 lib/composer/vendor/sabre/vobject/bin/fetch_windows_zones.php create mode 100755 lib/composer/vendor/sabre/vobject/bin/generate_vcards create mode 100755 lib/composer/vendor/sabre/vobject/bin/generateicalendardata.php create mode 100755 lib/composer/vendor/sabre/vobject/bin/mergeduplicates.php create mode 100644 lib/composer/vendor/sabre/vobject/bin/rrulebench.php create mode 100755 lib/composer/vendor/sabre/vobject/bin/vobject create mode 100644 lib/composer/vendor/sabre/vobject/composer.json create mode 100644 lib/composer/vendor/sabre/vobject/lib/BirthdayCalendarGenerator.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Cli.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Component.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Component/Available.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Component/VAlarm.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Component/VAvailability.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Component/VCalendar.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Component/VCard.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Component/VEvent.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Component/VFreeBusy.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Component/VJournal.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Component/VTimeZone.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Component/VTodo.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/DateTimeParser.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Document.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/ElementList.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/EofException.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/FreeBusyData.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/FreeBusyGenerator.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/ITip/Broker.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/ITip/ITipException.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/ITip/Message.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/ITip/SameOrganizerForAllComponentsException.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/InvalidDataException.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Node.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/PHPUnitAssertions.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Parameter.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/ParseException.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Parser/Json.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Parser/MimeDir.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Parser/Parser.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Parser/XML.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Parser/XML/Element/KeyValue.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Property.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Property/Binary.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Property/Boolean.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Property/FlatText.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Property/FloatValue.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Property/ICalendar/CalAddress.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Property/ICalendar/Date.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Property/ICalendar/DateTime.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Property/ICalendar/Duration.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Property/ICalendar/Period.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Property/ICalendar/Recur.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Property/IntegerValue.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Property/Text.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Property/Time.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Property/Unknown.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Property/Uri.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Property/UtcOffset.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Property/VCard/Date.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Property/VCard/DateAndOrTime.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Property/VCard/DateTime.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Property/VCard/LanguageTag.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Property/VCard/PhoneNumber.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Property/VCard/TimeStamp.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Reader.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Recur/EventIterator.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Recur/MaxInstancesExceededException.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Recur/NoInstancesException.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Recur/RDateIterator.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Recur/RRuleIterator.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Settings.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Splitter/ICalendar.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Splitter/SplitterInterface.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Splitter/VCard.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/StringUtil.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/TimeZoneUtil.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/TimezoneGuesser/FindFromOffset.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneIdentifier.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneMap.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/TimezoneGuesser/GuessFromLicEntry.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/TimezoneGuesser/GuessFromMsTzId.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/TimezoneGuesser/TimezoneFinder.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/TimezoneGuesser/TimezoneGuesser.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/UUIDUtil.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/VCardConverter.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Version.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/Writer.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/timezonedata/exchangezones.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/timezonedata/lotuszones.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/timezonedata/php-bc.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/timezonedata/php-workaround.php create mode 100644 lib/composer/vendor/sabre/vobject/lib/timezonedata/windowszones.php create mode 100644 lib/composer/vendor/sabre/vobject/resources/schema/xcal.rng create mode 100644 lib/composer/vendor/sabre/vobject/resources/schema/xcard.rng create mode 100644 lib/composer/vendor/sabre/xml/.github/workflows/ci.yml create mode 100644 lib/composer/vendor/sabre/xml/.php-cs-fixer.dist.php create mode 100644 lib/composer/vendor/sabre/xml/LICENSE create mode 100644 lib/composer/vendor/sabre/xml/README.md create mode 100644 lib/composer/vendor/sabre/xml/bin/.empty create mode 100644 lib/composer/vendor/sabre/xml/composer.json create mode 100644 lib/composer/vendor/sabre/xml/lib/ContextStackTrait.php create mode 100644 lib/composer/vendor/sabre/xml/lib/Deserializer/functions.php create mode 100644 lib/composer/vendor/sabre/xml/lib/Element.php create mode 100644 lib/composer/vendor/sabre/xml/lib/Element/Base.php create mode 100644 lib/composer/vendor/sabre/xml/lib/Element/Cdata.php create mode 100644 lib/composer/vendor/sabre/xml/lib/Element/Elements.php create mode 100644 lib/composer/vendor/sabre/xml/lib/Element/KeyValue.php create mode 100644 lib/composer/vendor/sabre/xml/lib/Element/Uri.php create mode 100644 lib/composer/vendor/sabre/xml/lib/Element/XmlFragment.php create mode 100644 lib/composer/vendor/sabre/xml/lib/LibXMLException.php create mode 100644 lib/composer/vendor/sabre/xml/lib/ParseException.php create mode 100644 lib/composer/vendor/sabre/xml/lib/Reader.php create mode 100644 lib/composer/vendor/sabre/xml/lib/Serializer/functions.php create mode 100644 lib/composer/vendor/sabre/xml/lib/Service.php create mode 100644 lib/composer/vendor/sabre/xml/lib/Version.php create mode 100644 lib/composer/vendor/sabre/xml/lib/Writer.php create mode 100644 lib/composer/vendor/sabre/xml/lib/XmlDeserializable.php create mode 100644 lib/composer/vendor/sabre/xml/lib/XmlSerializable.php create mode 100644 readme.md create mode 100644 source/conf.php create mode 100644 source/helpers/cache.php create mode 100644 source/helpers/call.php create mode 100644 source/helpers/ics.php create mode 100644 source/helpers/pit.php create mode 100644 source/helpers/string.php create mode 100644 source/main.php create mode 100644 source/overwrites/auths/_factory.php create mode 100644 source/overwrites/auths/basic.php create mode 100644 source/overwrites/auths/none.php create mode 100644 source/overwrites/caldav_backend.php create mode 100644 source/overwrites/principal_backend.php create mode 100644 source/sources/_factory.php create mode 100644 source/sources/_interface.php create mode 100644 source/sources/ics_feed.php create mode 100755 tools/build create mode 100755 tools/run create mode 100755 tools/update-libs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba98a06 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.geany +/build/ diff --git a/conf/example.json b/conf/example.json new file mode 100644 index 0000000..0ce304e --- /dev/null +++ b/conf/example.json @@ -0,0 +1,13 @@ +{ + "auth": { + "kind": "none", + "data": null + }, + "source": { + "kind": "ics_feed", + "data": { + "url": "https://export.kalender.digital/ics/0/3e10dae66950379d4cc8/gesamterkalender.ics?past_months=1&future_months=2" + } + } +} + diff --git a/lib/composer/composer.json b/lib/composer/composer.json new file mode 100644 index 0000000..6096ef7 --- /dev/null +++ b/lib/composer/composer.json @@ -0,0 +1,6 @@ +{ + "require": { + "sabre/dav": "^4.7", + "johngrogg/ics-parser": "^3.4" + } +} diff --git a/lib/composer/composer.lock b/lib/composer/composer.lock new file mode 100644 index 0000000..11c0bec --- /dev/null +++ b/lib/composer/composer.lock @@ -0,0 +1,578 @@ +{ + "_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": "4bccd22e60e62b03ff088076eeaf58ba", + "packages": [ + { + "name": "johngrogg/ics-parser", + "version": "v3.4.1", + "source": { + "type": "git", + "url": "https://github.com/u01jmg3/ics-parser.git", + "reference": "abb41a4a46256389aa4e6f582bad76f0d4cb3ebc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/u01jmg3/ics-parser/zipball/abb41a4a46256389aa4e6f582bad76f0d4cb3ebc", + "reference": "abb41a4a46256389aa4e6f582bad76f0d4cb3ebc", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.6.40" + }, + "require-dev": { + "phpunit/phpunit": "^5|^9|^10" + }, + "type": "library", + "autoload": { + "psr-0": { + "ICal": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jonathan Goode", + "role": "Developer/Owner" + }, + { + "name": "John Grogg", + "email": "john.grogg@gmail.com", + "role": "Developer/Prior Owner" + } + ], + "description": "ICS Parser", + "homepage": "https://github.com/u01jmg3/ics-parser", + "keywords": [ + "iCalendar", + "ical", + "ical-parser", + "ics", + "ics-parser", + "ifb" + ], + "support": { + "issues": "https://github.com/u01jmg3/ics-parser/issues", + "source": "https://github.com/u01jmg3/ics-parser/tree/v3.4.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/u01jmg3", + "type": "github" + } + ], + "time": "2024-06-26T08:18:40+00:00" + }, + { + "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.12", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/http.git", + "reference": "dedff73f3995578bc942fa4c8484190cac14f139" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/http/zipball/dedff73f3995578bc942fa4c8484190cac14f139", + "reference": "dedff73f3995578bc942fa4c8484190cac14f139", + "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", + "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": "2024-08-27T16:07:41+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.7", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/vobject.git", + "reference": "ff22611a53782e90c97be0d0bc4a5f98a5c0a12c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/vobject/zipball/ff22611a53782e90c97be0d0bc4a5f98a5c0a12c", + "reference": "ff22611a53782e90c97be0d0bc4a5f98a5c0a12c", + "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": "2025-04-17T09:22:48+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": [], + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/lib/composer/vendor/autoload.php b/lib/composer/vendor/autoload.php new file mode 100644 index 0000000..e59d762 --- /dev/null +++ b/lib/composer/vendor/autoload.php @@ -0,0 +1,25 @@ +realpath = realpath($opened_path) ?: $opened_path; + $opened_path = $this->realpath; + $this->handle = fopen($this->realpath, $mode); + $this->position = 0; + + return (bool) $this->handle; + } + + public function stream_read($count) + { + $data = fread($this->handle, $count); + + if ($this->position === 0) { + $data = preg_replace('{^#!.*\r?\n}', '', $data); + } + + $this->position += strlen($data); + + return $data; + } + + public function stream_cast($castAs) + { + return $this->handle; + } + + public function stream_close() + { + fclose($this->handle); + } + + public function stream_lock($operation) + { + return $operation ? flock($this->handle, $operation) : true; + } + + public function stream_seek($offset, $whence) + { + if (0 === fseek($this->handle, $offset, $whence)) { + $this->position = ftell($this->handle); + return true; + } + + return false; + } + + public function stream_tell() + { + return $this->position; + } + + public function stream_eof() + { + return feof($this->handle); + } + + public function stream_stat() + { + return array(); + } + + public function stream_set_option($option, $arg1, $arg2) + { + return true; + } + + public function url_stat($path, $flags) + { + $path = substr($path, 17); + if (file_exists($path)) { + return stat($path); + } + + return false; + } + } + } + + if ( + (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) + || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) + ) { + return include("phpvfscomposer://" . __DIR__ . '/..'.'/sabre/vobject/bin/generate_vcards'); + } +} + +return include __DIR__ . '/..'.'/sabre/vobject/bin/generate_vcards'; diff --git a/lib/composer/vendor/bin/naturalselection b/lib/composer/vendor/bin/naturalselection new file mode 100755 index 0000000..d13c00b --- /dev/null +++ b/lib/composer/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/composer/vendor/bin/sabredav b/lib/composer/vendor/bin/sabredav new file mode 100755 index 0000000..6e915f6 --- /dev/null +++ b/lib/composer/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/composer/vendor/bin/vobject b/lib/composer/vendor/bin/vobject new file mode 100755 index 0000000..2a50071 --- /dev/null +++ b/lib/composer/vendor/bin/vobject @@ -0,0 +1,119 @@ +#!/usr/bin/env php +realpath = realpath($opened_path) ?: $opened_path; + $opened_path = $this->realpath; + $this->handle = fopen($this->realpath, $mode); + $this->position = 0; + + return (bool) $this->handle; + } + + public function stream_read($count) + { + $data = fread($this->handle, $count); + + if ($this->position === 0) { + $data = preg_replace('{^#!.*\r?\n}', '', $data); + } + + $this->position += strlen($data); + + return $data; + } + + public function stream_cast($castAs) + { + return $this->handle; + } + + public function stream_close() + { + fclose($this->handle); + } + + public function stream_lock($operation) + { + return $operation ? flock($this->handle, $operation) : true; + } + + public function stream_seek($offset, $whence) + { + if (0 === fseek($this->handle, $offset, $whence)) { + $this->position = ftell($this->handle); + return true; + } + + return false; + } + + public function stream_tell() + { + return $this->position; + } + + public function stream_eof() + { + return feof($this->handle); + } + + public function stream_stat() + { + return array(); + } + + public function stream_set_option($option, $arg1, $arg2) + { + return true; + } + + public function url_stat($path, $flags) + { + $path = substr($path, 17); + if (file_exists($path)) { + return stat($path); + } + + return false; + } + } + } + + if ( + (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) + || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) + ) { + return include("phpvfscomposer://" . __DIR__ . '/..'.'/sabre/vobject/bin/vobject'); + } +} + +return include __DIR__ . '/..'.'/sabre/vobject/bin/vobject'; diff --git a/lib/composer/vendor/composer/ClassLoader.php b/lib/composer/vendor/composer/ClassLoader.php new file mode 100644 index 0000000..a72151c --- /dev/null +++ b/lib/composer/vendor/composer/ClassLoader.php @@ -0,0 +1,585 @@ + + * 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 */ + private $vendorDir; + + // PSR-4 + /** + * @var array[] + * @psalm-var array> + */ + private $prefixLengthsPsr4 = array(); + /** + * @var array[] + * @psalm-var array> + */ + private $prefixDirsPsr4 = array(); + /** + * @var array[] + * @psalm-var array + */ + private $fallbackDirsPsr4 = array(); + + // PSR-0 + /** + * @var array[] + * @psalm-var array> + */ + private $prefixesPsr0 = array(); + /** + * @var array[] + * @psalm-var array + */ + private $fallbackDirsPsr0 = array(); + + /** @var bool */ + private $useIncludePath = false; + + /** + * @var string[] + * @psalm-var array + */ + private $classMap = array(); + + /** @var bool */ + private $classMapAuthoritative = false; + + /** + * @var bool[] + * @psalm-var array + */ + private $missingClasses = array(); + + /** @var ?string */ + private $apcuPrefix; + + /** + * @var self[] + */ + private static $registeredLoaders = array(); + + /** + * @param ?string $vendorDir + */ + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + self::initializeIncludeClosure(); + } + + /** + * @return string[] + */ + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); + } + + return array(); + } + + /** + * @return array[] + * @psalm-return array> + */ + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + /** + * @return array[] + * @psalm-return array + */ + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + /** + * @return array[] + * @psalm-return array + */ + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + /** + * @return string[] Array of classname => path + * @psalm-return array + */ + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param string[] $classMap Class to filename map + * @psalm-param array $classMap + * + * @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 string[]|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + * + * @return void + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $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 string[]|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) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $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] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $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 string[]|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 string[]|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 indexed by their corresponding vendor directories. + * + * @return self[] + */ + 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/composer/vendor/composer/InstalledVersions.php b/lib/composer/vendor/composer/InstalledVersions.php new file mode 100644 index 0000000..51e734a --- /dev/null +++ b/lib/composer/vendor/composer/InstalledVersions.php @@ -0,0 +1,359 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer; + +use Composer\Autoload\ClassLoader; +use Composer\Semver\VersionParser; + +/** + * This class is copied in every Composer installed project and available to all + * + * See also https://getcomposer.org/doc/07-runtime.md#installed-versions + * + * To require its presence, you can require `composer-runtime-api ^2.0` + * + * @final + */ +class InstalledVersions +{ + /** + * @var mixed[]|null + * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null + */ + private static $installed; + + /** + * @var bool|null + */ + private static $canGetVendors; + + /** + * @var array[] + * @psalm-var array}> + */ + private static $installedByVendor = array(); + + /** + * Returns a list of all package names which are present, either by being installed, replaced or provided + * + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackages() + { + $packages = array(); + foreach (self::getInstalled() as $installed) { + $packages[] = array_keys($installed['versions']); + } + + if (1 === \count($packages)) { + return $packages[0]; + } + + return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); + } + + /** + * Returns a list of all package names with a specific type e.g. 'library' + * + * @param string $type + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackagesByType($type) + { + $packagesByType = array(); + + foreach (self::getInstalled() as $installed) { + foreach ($installed['versions'] as $name => $package) { + if (isset($package['type']) && $package['type'] === $type) { + $packagesByType[] = $name; + } + } + } + + return $packagesByType; + } + + /** + * Checks whether the given package is installed + * + * This also returns true if the package name is provided or replaced by another package + * + * @param string $packageName + * @param bool $includeDevRequirements + * @return bool + */ + public static function isInstalled($packageName, $includeDevRequirements = true) + { + foreach (self::getInstalled() as $installed) { + if (isset($installed['versions'][$packageName])) { + return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; + } + } + + return false; + } + + /** + * Checks whether the given package satisfies a version constraint + * + * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: + * + * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') + * + * @param VersionParser $parser Install composer/semver to have access to this class and functionality + * @param string $packageName + * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package + * @return bool + */ + public static function satisfies(VersionParser $parser, $packageName, $constraint) + { + $constraint = $parser->parseConstraints((string) $constraint); + $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + + return $provided->matches($constraint); + } + + /** + * Returns a version constraint representing all the range(s) which are installed for a given package + * + * It is easier to use this via isInstalled() with the $constraint argument if you need to check + * whether a given version of a package is installed, and not just whether it exists + * + * @param string $packageName + * @return string Version constraint usable with composer/semver + */ + public static function getVersionRanges($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + $ranges = array(); + if (isset($installed['versions'][$packageName]['pretty_version'])) { + $ranges[] = $installed['versions'][$packageName]['pretty_version']; + } + if (array_key_exists('aliases', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); + } + if (array_key_exists('replaced', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); + } + if (array_key_exists('provided', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); + } + + return implode(' || ', $ranges); + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['version'])) { + return null; + } + + return $installed['versions'][$packageName]['version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getPrettyVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['pretty_version'])) { + return null; + } + + return $installed['versions'][$packageName]['pretty_version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference + */ + public static function getReference($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['reference'])) { + return null; + } + + return $installed['versions'][$packageName]['reference']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. + */ + public static function getInstallPath($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @return array + * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} + */ + public static function getRootPackage() + { + $installed = self::getInstalled(); + + return $installed[0]['root']; + } + + /** + * Returns the raw installed.php data for custom implementations + * + * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. + * @return array[] + * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} + */ + public static function getRawData() + { + @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = include __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + + return self::$installed; + } + + /** + * Returns the raw data of all installed.php which are currently loaded for custom implementations + * + * @return array[] + * @psalm-return list}> + */ + public static function getAllRawData() + { + return self::getInstalled(); + } + + /** + * Lets you reload the static array from another file + * + * This is only useful for complex integrations in which a project needs to use + * this class but then also needs to execute another project's autoloader in process, + * and wants to ensure both projects have access to their version of installed.php. + * + * A typical case would be PHPUnit, where it would need to make sure it reads all + * the data it needs from this class, then call reload() with + * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure + * the project in which it runs can then also use this class safely, without + * interference between PHPUnit's dependencies and the project's dependencies. + * + * @param array[] $data A vendor/composer/installed.php data set + * @return void + * + * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data + */ + public static function reload($data) + { + self::$installed = $data; + self::$installedByVendor = array(); + } + + /** + * @return array[] + * @psalm-return list}> + */ + private static function getInstalled() + { + if (null === self::$canGetVendors) { + self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); + } + + $installed = array(); + + if (self::$canGetVendors) { + foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + if (isset(self::$installedByVendor[$vendorDir])) { + $installed[] = self::$installedByVendor[$vendorDir]; + } elseif (is_file($vendorDir.'/composer/installed.php')) { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require $vendorDir.'/composer/installed.php'; + $installed[] = self::$installedByVendor[$vendorDir] = $required; + if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { + self::$installed = $installed[count($installed) - 1]; + } + } + } + } + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require __DIR__ . '/installed.php'; + self::$installed = $required; + } else { + self::$installed = array(); + } + } + + if (self::$installed !== array()) { + $installed[] = self::$installed; + } + + return $installed; + } +} diff --git a/lib/composer/vendor/composer/LICENSE b/lib/composer/vendor/composer/LICENSE new file mode 100644 index 0000000..62ecfd8 --- /dev/null +++ b/lib/composer/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/composer/vendor/composer/autoload_classmap.php b/lib/composer/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000..0fb0a2c --- /dev/null +++ b/lib/composer/vendor/composer/autoload_classmap.php @@ -0,0 +1,10 @@ + $vendorDir . '/composer/InstalledVersions.php', +); diff --git a/lib/composer/vendor/composer/autoload_files.php b/lib/composer/vendor/composer/autoload_files.php new file mode 100644 index 0000000..610e2d4 --- /dev/null +++ b/lib/composer/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/composer/vendor/composer/autoload_namespaces.php b/lib/composer/vendor/composer/autoload_namespaces.php new file mode 100644 index 0000000..0cb9ddf --- /dev/null +++ b/lib/composer/vendor/composer/autoload_namespaces.php @@ -0,0 +1,10 @@ + array($vendorDir . '/johngrogg/ics-parser/src'), +); diff --git a/lib/composer/vendor/composer/autoload_psr4.php b/lib/composer/vendor/composer/autoload_psr4.php new file mode 100644 index 0000000..e7c6320 --- /dev/null +++ b/lib/composer/vendor/composer/autoload_psr4.php @@ -0,0 +1,16 @@ + 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/composer/vendor/composer/autoload_real.php b/lib/composer/vendor/composer/autoload_real.php new file mode 100644 index 0000000..faa1009 --- /dev/null +++ b/lib/composer/vendor/composer/autoload_real.php @@ -0,0 +1,50 @@ +register(true); + + $filesToLoad = \Composer\Autoload\ComposerStaticInite6a6e754980b2e4ee0099ab3b538a42e::$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/composer/vendor/composer/autoload_static.php b/lib/composer/vendor/composer/autoload_static.php new file mode 100644 index 0000000..756a894 --- /dev/null +++ b/lib/composer/vendor/composer/autoload_static.php @@ -0,0 +1,90 @@ + __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 $prefixesPsr0 = array ( + 'I' => + array ( + 'ICal' => + array ( + 0 => __DIR__ . '/..' . '/johngrogg/ics-parser/src', + ), + ), + ); + + public static $classMap = array ( + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInite6a6e754980b2e4ee0099ab3b538a42e::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInite6a6e754980b2e4ee0099ab3b538a42e::$prefixDirsPsr4; + $loader->prefixesPsr0 = ComposerStaticInite6a6e754980b2e4ee0099ab3b538a42e::$prefixesPsr0; + $loader->classMap = ComposerStaticInite6a6e754980b2e4ee0099ab3b538a42e::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/lib/composer/vendor/composer/installed.json b/lib/composer/vendor/composer/installed.json new file mode 100644 index 0000000..d048070 --- /dev/null +++ b/lib/composer/vendor/composer/installed.json @@ -0,0 +1,589 @@ +{ + "packages": [ + { + "name": "johngrogg/ics-parser", + "version": "v3.4.1", + "version_normalized": "3.4.1.0", + "source": { + "type": "git", + "url": "https://github.com/u01jmg3/ics-parser.git", + "reference": "abb41a4a46256389aa4e6f582bad76f0d4cb3ebc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/u01jmg3/ics-parser/zipball/abb41a4a46256389aa4e6f582bad76f0d4cb3ebc", + "reference": "abb41a4a46256389aa4e6f582bad76f0d4cb3ebc", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.6.40" + }, + "require-dev": { + "phpunit/phpunit": "^5|^9|^10" + }, + "time": "2024-06-26T08:18:40+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-0": { + "ICal": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jonathan Goode", + "role": "Developer/Owner" + }, + { + "name": "John Grogg", + "email": "john.grogg@gmail.com", + "role": "Developer/Prior Owner" + } + ], + "description": "ICS Parser", + "homepage": "https://github.com/u01jmg3/ics-parser", + "keywords": [ + "iCalendar", + "ical", + "ical-parser", + "ics", + "ics-parser", + "ifb" + ], + "support": { + "issues": "https://github.com/u01jmg3/ics-parser/issues", + "source": "https://github.com/u01jmg3/ics-parser/tree/v3.4.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/u01jmg3", + "type": "github" + } + ], + "install-path": "../johngrogg/ics-parser" + }, + { + "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.12", + "version_normalized": "5.1.12.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/http.git", + "reference": "dedff73f3995578bc942fa4c8484190cac14f139" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/http/zipball/dedff73f3995578bc942fa4c8484190cac14f139", + "reference": "dedff73f3995578bc942fa4c8484190cac14f139", + "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", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "suggest": { + "ext-curl": " to make http requests with the Client class" + }, + "time": "2024-08-27T16:07:41+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.7", + "version_normalized": "4.5.7.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/vobject.git", + "reference": "ff22611a53782e90c97be0d0bc4a5f98a5c0a12c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/vobject/zipball/ff22611a53782e90c97be0d0bc4a5f98a5c0a12c", + "reference": "ff22611a53782e90c97be0d0bc4a5f98a5c0a12c", + "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": "2025-04-17T09:22:48+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/composer/vendor/composer/installed.php b/lib/composer/vendor/composer/installed.php new file mode 100644 index 0000000..75c78a9 --- /dev/null +++ b/lib/composer/vendor/composer/installed.php @@ -0,0 +1,95 @@ + array( + 'name' => '__root__', + 'pretty_version' => '1.0.0+no-version-set', + 'version' => '1.0.0.0', + 'reference' => null, + 'type' => 'library', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev' => true, + ), + 'versions' => array( + '__root__' => array( + 'pretty_version' => '1.0.0+no-version-set', + 'version' => '1.0.0.0', + 'reference' => null, + 'type' => 'library', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'johngrogg/ics-parser' => array( + 'pretty_version' => 'v3.4.1', + 'version' => '3.4.1.0', + 'reference' => 'abb41a4a46256389aa4e6f582bad76f0d4cb3ebc', + 'type' => 'library', + 'install_path' => __DIR__ . '/../johngrogg/ics-parser', + '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.12', + 'version' => '5.1.12.0', + 'reference' => 'dedff73f3995578bc942fa4c8484190cac14f139', + '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.7', + 'version' => '4.5.7.0', + 'reference' => 'ff22611a53782e90c97be0d0bc4a5f98a5c0a12c', + '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/composer/vendor/composer/platform_check.php b/lib/composer/vendor/composer/platform_check.php new file mode 100644 index 0000000..adfb472 --- /dev/null +++ b/lib/composer/vendor/composer/platform_check.php @@ -0,0 +1,26 @@ += 80000)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 8.0.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/composer/vendor/johngrogg/ics-parser/.editorconfig b/lib/composer/vendor/johngrogg/ics-parser/.editorconfig new file mode 100644 index 0000000..9c1f764 --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/.editorconfig @@ -0,0 +1,11 @@ +# https://editorconfig.org/ + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/lib/composer/vendor/johngrogg/ics-parser/.github/ISSUE_TEMPLATE/bug_report.yml b/lib/composer/vendor/johngrogg/ics-parser/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..5e3515f --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,53 @@ +name: Bug Report +description: "Report something that's broken." +labels: ["bug-normal"] +body: + - type: markdown + attributes: + value: "Before raising an issue, please check the issue has not already been fixed in `dev-master`. You can also search through our [closed issues](../issues?q=is%3Aissue+is%3Aclosed+)." + - type: input + id: php-version + attributes: + label: PHP Version + description: Provide the PHP version that you are using. + placeholder: 8.1.4 + validations: + required: true + - type: input + id: php-date-timezone + attributes: + label: PHP date.timezone + description: Provide the PHP date.timezone that you are using. + placeholder: "[Country] / [City]" + validations: + required: true + - type: input + id: ics-parser-version + attributes: + label: ICS Parser Version + description: Provide the `ics-parser` library version that you are using. + placeholder: 3.2.1 + validations: + required: true + - type: input + id: operating-system + attributes: + label: Operating System + description: Provide the operating system that you are using. + placeholder: "Windows / Mac / Linux" + validations: + required: true + - type: textarea + id: description + attributes: + label: Description + description: Provide a detailed description of the issue that you are facing. + validations: + required: true + - type: textarea + id: steps-to-reproduce + attributes: + label: Steps to Reproduce + description: Provide detailed steps to reproduce your issue. It is **essential** that you supply a copy of the iCal file that is causing the parser to behave incorrectly to allow us to investigate. Prior to uploading the iCal file, please remove any personal or identifying information. + validations: + required: true diff --git a/lib/composer/vendor/johngrogg/ics-parser/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/lib/composer/vendor/johngrogg/ics-parser/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 0000000..5a92ac1 --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,12 @@ +> :information_source: +> - File a bug on our [issue tracker](https://github.com/u01jmg3/ics-parser/issues) (if there isn't one already). +> - If your patch is going to be large it might be a good idea to get the discussion started early. We are happy to discuss it in a new issue beforehand. +> - Please follow the coding standards already adhered to in the file you're editing before committing +> - This includes the use of *4 spaces* over tabs for indentation +> - Trim all trailing whitespace +> - Using single quotes (`'`) where possible +> - Use `PHP_EOL` where possible or default to `\n` +> - Using the [1TBS](https://en.wikipedia.org/wiki/Indent_style#Variant:_1TBS_.28OTBS.29) indent style +> - If a function is added or changed, please remember to update the [API documentation in the README](https://github.com/u01jmg3/ics-parser/blob/master/README.md#api) +> - Please include unit tests to verify any new functionality +> - Also check that existing tests still pass: `composer test` diff --git a/lib/composer/vendor/johngrogg/ics-parser/.github/dependabot.yml b/lib/composer/vendor/johngrogg/ics-parser/.github/dependabot.yml new file mode 100644 index 0000000..3f0c15e --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: +- package-ecosystem: composer + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 + reviewers: + - u01jmg3 +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: weekly diff --git a/lib/composer/vendor/johngrogg/ics-parser/.github/release_template.md b/lib/composer/vendor/johngrogg/ics-parser/.github/release_template.md new file mode 100644 index 0000000..483f50b --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/.github/release_template.md @@ -0,0 +1,9 @@ +# Release Checklist + +- [ ] Update docblock in `src/ICal/ICal.php` +- [ ] Ensure the documentation is up to date +- [ ] Push the code changes to GitHub (`git push`) +- [ ] Tag the release (`git tag v1.2.3`) +- [ ] Push the tag (`git push --tag`) +- [ ] Check [Packagist](https://packagist.org/packages/johngrogg/ics-parser) is updated +- [ ] Notify anyone who opened [an issue or PR](https://github.com/u01jmg3/ics-parser/issues?q=is%3Aopen) of the fix diff --git a/lib/composer/vendor/johngrogg/ics-parser/.github/workflows/coding-standards.yml b/lib/composer/vendor/johngrogg/ics-parser/.github/workflows/coding-standards.yml new file mode 100644 index 0000000..3d196ed --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/.github/workflows/coding-standards.yml @@ -0,0 +1,67 @@ +name: Coding Standards + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +permissions: + contents: read + +jobs: + Scan: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + php: [5.6, 7.4, '8.0', 8.1, 8.2, 8.3, 8.4] + + name: PHP ${{ matrix.php }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer:2.2 + coverage: none + + - name: Install dependencies for PHP 5.6 + run: composer update --quiet --no-scripts + if: matrix.php == 5.6 + + - name: Install dependencies for PHP 7.4+ + run: composer install --quiet --no-scripts + if: matrix.php >= 7.4 + + - name: Execute tests + run: vendor/bin/phpunit --verbose + + - name: Install additional dependencies + run: | + composer config allow-plugins.bamarni/composer-bin-plugin true --no-plugins + composer require bamarni/composer-bin-plugin rector/rector squizlabs/php_codesniffer --dev --quiet --no-scripts + composer bin easy-coding-standard config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true + composer bin easy-coding-standard require symplify/easy-coding-standard slevomat/coding-standard --dev --quiet --no-scripts + if: matrix.php == 8.3 + + - name: Execute PHPCodeSniffer + run: vendor/bin/phpcs -n -s --standard=psr12 src + if: matrix.php == 8.3 + + - name: Execute Rector + run: vendor/bin/rector process src --dry-run + if: matrix.php == 8.3 + + - name: Execute ECS + run: vendor/bin/ecs check src + if: matrix.php == 8.3 + + - name: Execute PHPStan + run: vendor/bin/phpstan analyse src + if: matrix.php == 8.3 diff --git a/lib/composer/vendor/johngrogg/ics-parser/.gitignore b/lib/composer/vendor/johngrogg/ics-parser/.gitignore new file mode 100644 index 0000000..80c161d --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/.gitignore @@ -0,0 +1,59 @@ +################### +# Compiled Source # +################### +*.com +*.class +*.dll +*.exe +*.o +*.so + +############ +# Packages # +############ +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip + +###################### +# Logs and Databases # +###################### +*.log +*.sqlite + +###################### +# OS Generated Files # +###################### +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +.phpunit.result.cache +ehthumbs.db +Thumbs.db +workbench + +#################### +# Package Managers # +#################### +auth.json +node_modules +vendor + +########## +# Custom # +########## +*.git +*-report.* + +######## +# IDEs # +######## +.idea +*.iml diff --git a/lib/composer/vendor/johngrogg/ics-parser/CONTRIBUTING.md b/lib/composer/vendor/johngrogg/ics-parser/CONTRIBUTING.md new file mode 100644 index 0000000..9e7ed85 --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/CONTRIBUTING.md @@ -0,0 +1,18 @@ +## Contributing + +ICS Parser is an open source project. It is licensed under the [MIT license](https://opensource.org/licenses/MIT). +We appreciate pull requests, here are our guidelines: + +1. Firstly, check if your issue is present within the latest version (`dev-master`) as the problem may already have been fixed. +1. Log a bug in our [issue tracker](https://github.com/u01jmg3/ics-parser/issues) (if there isn't one already). + - If your patch is going to be large it might be a good idea to get the discussion started early. + - We are happy to discuss it in an issue beforehand. + - If you could provide an iCal snippet causing the parser to behave incorrectly it is extremely useful for debugging + - Please remove all irrelevant events +1. Please follow the coding standard already present in the file you are editing _before_ committing + - Adhere to the [PSR-2](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md) coding standard + - Use *4 spaces* instead of tabs for indentation + - Trim all trailing whitespace and blank lines + - Use single quotes (`'`) where possible instead of double + - Use `PHP_EOL` where possible or default to `\n` + - Abide by the [1TBS](https://en.wikipedia.org/wiki/Indent_style#Variant:_1TBS_.28OTBS.29) indentation style diff --git a/lib/composer/vendor/johngrogg/ics-parser/FUNDING.yml b/lib/composer/vendor/johngrogg/ics-parser/FUNDING.yml new file mode 100644 index 0000000..e6e77b3 --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/FUNDING.yml @@ -0,0 +1 @@ +github: u01jmg3 diff --git a/lib/composer/vendor/johngrogg/ics-parser/LICENSE b/lib/composer/vendor/johngrogg/ics-parser/LICENSE new file mode 100644 index 0000000..0708674 --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/LICENSE @@ -0,0 +1,15 @@ +The MIT License (MIT) +Copyright (c) 2018 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, +modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/composer/vendor/johngrogg/ics-parser/README.md b/lib/composer/vendor/johngrogg/ics-parser/README.md new file mode 100644 index 0000000..2c1235a --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/README.md @@ -0,0 +1,256 @@ +# PHP ICS Parser + +[![Latest Stable Release](https://poser.pugx.org/johngrogg/ics-parser/v/stable.png "Latest Stable Release")](https://packagist.org/packages/johngrogg/ics-parser) +[![Total Downloads](https://poser.pugx.org/johngrogg/ics-parser/downloads.png "Total Downloads")](https://packagist.org/packages/johngrogg/ics-parser) + +--- + +## Installation + +### Requirements + - PHP 5 (≥ 5.6.40) + - [Valid ICS](https://icalendar.org/validator.html) (`.ics`, `.ical`, `.ifb`) file + - [IANA](https://www.iana.org/time-zones), [Unicode CLDR](http://cldr.unicode.org/translation/timezones) or [Windows](https://support.microsoft.com/en-ca/help/973627/microsoft-time-zone-index-values) Time Zones + +### Setup + + - Install [Composer](https://getcomposer.org/) + - Add the following dependency to `composer.json` + - :warning: **Note with Composer the owner is `johngrogg` and not `u01jmg3`** + - To access the latest stable branch (`v3`) use the following + - To access new features you can require [`dev-master`](https://getcomposer.org/doc/articles/aliases.md#branch-alias) + + ```yaml + { + "require": { + "johngrogg/ics-parser": "^3" + } + } + ``` + +## Running tests + +```sh +composer test +``` + +## How to use + +### How to instantiate the Parser + + - Using the example script as a guide, [refer to this code](https://github.com/u01jmg3/ics-parser/blob/master/examples/index.php#L1-L22) + +#### What will the parser return? + + - Each key/value pair from the iCal file will be parsed creating an associative array for both the calendar and every event it contains. + - Also injected will be content under `dtstart_tz` and `dtend_tz` for accessing start and end dates with time zone data applied. + - Where possible [`DateTime`](https://secure.php.net/manual/en/class.datetime.php) objects are used and returned. + - :information_source: **Note the parser is limited to [relative date formats](https://www.php.net/manual/en/datetime.formats.relative.php) which can inhibit how complex recurrence rule parts are processed (e.g. `BYDAY` combined with `BYSETPOS`)** + + ```php + // Dump the whole calendar + var_dump($ical->cal); + + // Dump every event + var_dump($ical->events()); + ``` + + - Also included are special `{property}_array` arrays which further resolve the contents of a key/value pair. + + ```php + // Dump a parsed event's start date + var_dump($event->dtstart_array); + + // array (size=4) + // 0 => + // array (size=1) + // 'TZID' => string 'America/Detroit' (length=15) + // 1 => string '20160409T090000' (length=15) + // 2 => int 1460192400 + // 3 => string 'TZID=America/Detroit:20160409T090000' (length=36) + ``` + +### Are you using Outlook? + +Outlook has a quirk where it requires the User Agent string to be set in your request headers. + +We have done this for you by injecting a default User Agent string, if one has not been specified. + +If you wish to provide your own User agent string you can do so by using the `httpUserAgent` argument when creating your ICal object. + +```php +$ical = new ICal($url, array('httpUserAgent' => 'A Different User Agent')); +``` + +--- + +## When Parsing an iCal Feed + +Parsing [iCal/iCalendar/ICS](https://en.wikipedia.org/wiki/ICalendar) resources can pose several challenges. One challenge is that +the specification is a moving target; the original RFC has only been updated four times in ten years. The other challenge is that vendors +were both liberal (read: creative) in interpreting the specification and productive implementing proprietary extensions. + +However, what impedes efficient parsing most directly are recurrence rules for events. This library parses the original +calendar into an easy to work with memory model. This requires that each recurring event is expanded or exploded. Hence, +a single event that occurs daily will generate a new event instance for each day as this parser processes the +calendar ([`$defaultSpan`](#variables) limits this). To get an idea how this is done take a look at the +[call graph](https://user-images.githubusercontent.com/624195/45904641-f3cd0a80-bded-11e8-925f-7bcee04b8575.png). + +As a consequence the _entire_ calendar is parsed line-by-line, and thus loaded into memory, first. As you can imagine +large calendars tend to get huge when exploded i.e. with all their recurrence rules evaluated. This is exacerbated when +old calendars do not remove past events as they get fatter and fatter every year. + +This limitation is particularly painful if you only need a window into the original calendar. It seems wasteful to parse +the entire fully exploded calendar into memory if you later are going to call the +[`eventsFromInterval()` or `eventsFromRange()`](#methods) on it. + +In late 2018 [#190](https://github.com/u01jmg3/ics-parser/pull/190) added the option to drop all events outside a given +range very early in the parsing process at the cost of some precision (time zone calculations are not calculated at that point). This +massively reduces the total time for parsing a calendar. The same goes for memory consumption. The precondition is that +you know upfront that you don't care about events outside a given range. + +Let's say you are only interested in events from yesterday, today and tomorrow. To compensate for the fact that the +tricky time zone transformations and calculations have not been executed yet by the time the parser has to decide whether +to keep or drop an event you can set it to filter for **+-2d** instead of +-1d. Once it is done you would then call +`eventsFromRange()` with +-1d to get precisely the events in the window you are interested in. That is what the variables +[`$filterDaysBefore` and `$filterDaysAfter`](#variables) are for. + +In Q1 2019 [#213](https://github.com/u01jmg3/ics-parser/pull/213) further improved the performance by immediately +dropping _non-recurring_ events once parsed if they are outside that fuzzy window. This greatly reduces the maximum +memory consumption for large calendars. PHP by default does not allocate more than 128MB heap and would otherwise crash +with `Fatal error: Allowed memory size of 134217728 bytes exhausted`. It goes without saying that recurring events first +need to be evaluated before non-fitting events can be dropped. + +--- + +## API + +### `ICal` API + +#### Variables + +| Name | Configurable | Default Value | Description | +|--------------------------------|:------------------------:|-------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `$alarmCount` | :heavy_multiplication_x: | N/A | Tracks the number of alarms in the current iCal feed | +| `$cal` | :heavy_multiplication_x: | N/A | The parsed calendar | +| `$defaultSpan` | :ballot_box_with_check: | `2` | The value in years to use for indefinite, recurring events | +| `$defaultTimeZone` | :ballot_box_with_check: | [System default](https://secure.php.net/manual/en/function.date-default-timezone-get.php) | Enables customisation of the default time zone | +| `$defaultWeekStart` | :ballot_box_with_check: | `MO` | The two letter representation of the first day of the week | +| `$disableCharacterReplacement` | :ballot_box_with_check: | `false` | Toggles whether to disable all character replacement. Will replace curly quotes and other special characters with their standard equivalents if `false`. Can be a costly operation! | +| `$eventCount` | :heavy_multiplication_x: | N/A | Tracks the number of events in the current iCal feed | +| `$filterDaysAfter` | :ballot_box_with_check: | `null` | When set the parser will ignore all events more than roughly this many days _after_ now. To be on the safe side it is advised that you make the filter window `+/- 1` day larger than necessary. For performance reasons this filter is applied before any date and time zone calculations are done. Hence, depending the time zone settings of the parser and the calendar the cut-off date is not "calibrated". You can then use `$ical->eventsFromRange()` to precisely shrink the window. | +| `$filterDaysBefore` | :ballot_box_with_check: | `null` | When set the parser will ignore all events more than roughly this many days _before_ now. See `$filterDaysAfter` above for more details. | +| `$freeBusyCount` | :heavy_multiplication_x: | N/A | Tracks the free/busy count in the current iCal feed | +| `$httpBasicAuth` | :heavy_multiplication_x: | `array()` | Holds the username and password for HTTP basic authentication | +| `$httpUserAgent` | :ballot_box_with_check: | `null` | Holds the custom User Agent string header | +| `$httpAcceptLanguage` | :heavy_multiplication_x: | `null` | Holds the custom Accept Language request header, e.g. "en" or "de" | +| `$httpProtocolVersion` | :heavy_multiplication_x: | `null` | Holds the custom HTTP Protocol version, e.g. "1.0" or "1.1" | +| `$shouldFilterByWindow` | :heavy_multiplication_x: | `false` | `true` if either `$filterDaysBefore` or `$filterDaysAfter` are set | +| `$skipRecurrence` | :ballot_box_with_check: | `false` | Toggles whether to skip the parsing of recurrence rules | +| `$todoCount` | :heavy_multiplication_x: | N/A | Tracks the number of todos in the current iCal feed | +| `$windowMaxTimestamp` | :heavy_multiplication_x: | `null` | If `$filterDaysBefore` or `$filterDaysAfter` are set then the events are filtered according to the window defined by this field and `$windowMinTimestamp` | +| `$windowMinTimestamp` | :heavy_multiplication_x: | `null` | If `$filterDaysBefore` or `$filterDaysAfter` are set then the events are filtered according to the window defined by this field and `$windowMaxTimestamp` | + +#### Methods + +| Method | Parameter(s) | Visibility | Description | +|-------------------------------------------------|-----------------------------------------------------------------------------------------------|-------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `__construct` | `$files = false`, `$options = array()` | `public` | Creates the ICal object | +| `initFile` | `$file` | `protected` | Initialises lines from a file | +| `initLines` | `$lines` | `protected` | Initialises the parser using an array containing each line of iCal content | +| `initString` | `$string` | `protected` | Initialises lines from a string | +| `initUrl` | `$url`, `$username = null`, `$password = null`, `$userAgent = null`, `$acceptLanguage = null` | `protected` | Initialises lines from a URL. Accepts a username/password combination for HTTP basic authentication, a custom User Agent string and the accepted client language | +| `addCalendarComponentWithKeyAndValue` | `$component`, `$keyword`, `$value` | `protected` | Add one key and value pair to the `$this->cal` array | +| `calendarDescription` | - | `public` | Returns the calendar description | +| `calendarName` | - | `public` | Returns the calendar name | +| `calendarTimeZone` | `$ignoreUtc` | `public` | Returns the calendar time zone | +| `cleanCharacters` | `$data` | `protected` | Replaces curly quotes and other special characters with their standard equivalents | +| `eventsFromInterval` | `$interval` | `public` | Returns a sorted array of events following a given string | +| `eventsFromRange` | `$rangeStart = false`, `$rangeEnd = false` | `public` | Returns a sorted array of events in a given range, or an empty array if no events exist in the range | +| `events` | - | `public` | Returns an array of Events | +| `fileOrUrl` | `$filename` | `protected` | Reads an entire file or URL into an array | +| `filterValuesUsingBySetPosRRule` | `$bysetpos`, `$valueslist` | `protected` | Filters a provided values-list by applying a BYSETPOS RRule | +| `freeBusyEvents` | - | `public` | Returns an array of arrays with all free/busy events | +| `getDaysOfMonthMatchingByDayRRule` | `$bydays`, `$initialDateTime` | `protected` | Find all days of a month that match the BYDAY stanza of an RRULE | +| `getDaysOfMonthMatchingByMonthDayRRule` | `$byMonthDays`, `$initialDateTime` | `protected` | Find all days of a month that match the BYMONTHDAY stanza of an RRULE | +| `getDaysOfYearMatchingByDayRRule` | `$byDays`, `$initialDateTime` | `protected` | Find all days of a year that match the BYDAY stanza of an RRULE | +| `getDaysOfYearMatchingByMonthDayRRule` | `$byMonthDays`, `$initialDateTime` | `protected` | Find all days of a year that match the BYMONTHDAY stanza of an RRULE | +| `getDaysOfYearMatchingByWeekNoRRule` | `$byWeekNums`, `$initialDateTime` | `protected` | Find all days of a year that match the BYWEEKNO stanza of an RRULE | +| `getDaysOfYearMatchingByYearDayRRule` | `$byYearDays`, `$initialDateTime` | `protected` | Find all days of a year that match the BYYEARDAY stanza of an RRULE | +| `getDefaultTimeZone` | `$forceReturnSystemDefault` | `private` | Returns the default time zone if set or falls back to the system default if not set | +| `hasEvents` | - | `public` | Returns a boolean value whether the current calendar has events or not | +| `iCalDateToDateTime` | `$icalDate` | `public` | Returns a `DateTime` object from an iCal date time format | +| `iCalDateToUnixTimestamp` | `$icalDate` | `public` | Returns a Unix timestamp from an iCal date time format | +| `iCalDateWithTimeZone` | `$event`, `$key`, `$format = DATE_TIME_FORMAT` | `public` | Returns a date adapted to the calendar time zone depending on the event `TZID` | +| `doesEventStartOutsideWindow` | `$event` | `protected` | Determines whether the event start date is outside `$windowMinTimestamp` / `$windowMaxTimestamp` | +| `isFileOrUrl` | `$filename` | `protected` | Checks if a filename exists as a file or URL | +| `isOutOfRange` | `$calendarDate`, `$minTimestamp`, `$maxTimestamp` | `protected` | Determines whether a valid iCalendar date is within a given range | +| `isValidCldrTimeZoneId` | `$timeZone` | `protected` | Checks if a time zone is a valid CLDR time zone | +| `isValidDate` | `$value` | `public` | Checks if a date string is a valid date | +| `isValidIanaTimeZoneId` | `$timeZone` | `protected` | Checks if a time zone is a valid IANA time zone | +| `isValidWindowsTimeZoneId` | `$timeZone` | `protected` | Checks if a time zone is a recognised Windows (non-CLDR) time zone | +| `isValidTimeZoneId` | `$timeZone` | `protected` | Checks if a time zone is valid (IANA, CLDR, or Windows) | +| `keyValueFromString` | `$text` | `public` | Gets the key value pair from an iCal string | +| `parseLine` | `$line` | `protected` | Parses a line from an iCal file into an array of tokens | +| `mb_chr` | `$code` | `protected` | Provides a polyfill for PHP 7.2's `mb_chr()`, which is a multibyte safe version of `chr()` | +| `escapeParamText` | `$candidateText` | `protected` | Places double-quotes around texts that have characters not permitted in parameter-texts, but are permitted in quoted-texts. | +| `parseDuration` | `$date`, `$duration` | `protected` | Parses a duration and applies it to a date | +| `parseExdates` | `$event` | `public` | Parses a list of excluded dates to be applied to an Event | +| `processDateConversions` | - | `protected` | Processes date conversions using the time zone | +| `processEvents` | - | `protected` | Performs admin tasks on all events as read from the iCal file | +| `processRecurrences` | - | `protected` | Processes recurrence rules | +| `reduceEventsToMinMaxRange` | | `protected` | Reduces the number of events to the defined minimum and maximum range | +| `removeLastEventIfOutsideWindowAndNonRecurring` | | `protected` | Removes the last event (i.e. most recently parsed) if its start date is outside the window spanned by `$windowMinTimestamp` / `$windowMaxTimestamp` | +| `removeUnprintableChars` | `$data` | `protected` | Removes unprintable ASCII and UTF-8 characters | +| `resolveIndicesOfRange` | `$indexes`, `$limit` | `protected` | Resolves values from indices of the range 1 -> `$limit` | +| `sortEventsWithOrder` | `$events`, `$sortOrder = SORT_ASC` | `public` | Sorts events based on a given sort order | +| `timeZoneStringToDateTimeZone` | `$timeZoneString` | `public` | Returns a `DateTimeZone` object based on a string containing a time zone name. | +| `unfold` | `$lines` | `protected` | Unfolds an iCal file in preparation for parsing | + +#### Constants + +| Name | Description | +|---------------------------|-----------------------------------------------| +| `DATE_TIME_FORMAT_PRETTY` | Default pretty date time format to use | +| `DATE_TIME_FORMAT` | Default date time format to use | +| `ICAL_DATE_TIME_TEMPLATE` | String template to generate an iCal date time | +| `ISO_8601_WEEK_START` | First day of the week, as defined by ISO-8601 | +| `RECURRENCE_EVENT` | Used to isolate generated recurrence events | +| `SECONDS_IN_A_WEEK` | The number of seconds in a week | +| `TIME_FORMAT` | Default time format to use | +| `TIME_ZONE_UTC` | UTC time zone string | +| `UNIX_FORMAT` | Unix timestamp date format | +| `UNIX_MIN_YEAR` | The year Unix time began | + +--- + +### `Event` API (extends `ICal` API) + +#### Methods + +| Method | Parameter(s) | Visibility | Description | +|---------------|---------------------------------------------|-------------|---------------------------------------------------------------------| +| `__construct` | `$data = array()` | `public` | Creates the Event object | +| `prepareData` | `$value` | `protected` | Prepares the data for output | +| `printData` | `$html = HTML_TEMPLATE` | `public` | Returns Event data excluding anything blank within an HTML template | +| `snakeCase` | `$input`, `$glue = '_'`, `$separator = '-'` | `protected` | Converts the given input to snake_case | + +#### Constants + +| Name | Description | +|-----------------|-----------------------------------------------------| +| `HTML_TEMPLATE` | String template to use when pretty printing content | + +--- + +## Credits + - [Jonathan Goode](https://github.com/u01jmg3) (programming, bug fixing, codebase enhancement, coding standard adoption) + - [s0600204](https://github.com/s0600204) (major enhancements to RRULE support, many bug fixes and other contributions) + +--- + +## Tools for Testing + + - [iCal Validator](https://icalendar.org/validator.html) + - [Recurrence Rule Tester](https://jkbrzt.github.io/rrule/) + - [Unix Timestamp Converter](https://www.unixtimestamp.com) diff --git a/lib/composer/vendor/johngrogg/ics-parser/composer.json b/lib/composer/vendor/johngrogg/ics-parser/composer.json new file mode 100644 index 0000000..a9d348e --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/composer.json @@ -0,0 +1,49 @@ +{ + "name": "johngrogg/ics-parser", + "description": "ICS Parser", + "homepage": "https://github.com/u01jmg3/ics-parser", + "keywords": [ + "ical", + "ical-parser", + "icalendar", + "ics", + "ics-parser", + "ifb" + ], + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Jonathan Goode", + "role": "Developer/Owner" + }, + { + "name": "John Grogg", + "email": "john.grogg@gmail.com", + "role": "Developer/Prior Owner" + } + ], + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/u01jmg3" + } + ], + "require": { + "php": ">=5.6.40", + "ext-mbstring": "*" + }, + "require-dev": { + "phpunit/phpunit": "^5|^9|^10" + }, + "autoload": { + "psr-0": { + "ICal": "src/" + } + }, + "scripts": { + "test": [ + "phpunit --colors=always" + ] + } +} diff --git a/lib/composer/vendor/johngrogg/ics-parser/composer.lock b/lib/composer/vendor/johngrogg/ics-parser/composer.lock new file mode 100644 index 0000000..2ccb010 --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/composer.lock @@ -0,0 +1,1763 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "6e5a6da49b03030889a52c62bd2e7eab", + "packages": [], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.30 || ^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.5.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:15:36+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.12.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2024-06-12T14:39:25+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.0.2", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13", + "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.2" + }, + "time": "2024-03-05T20:51:40+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.31", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.3", + "phpunit/php-text-template": "^2.0.2", + "sebastian/code-unit-reverse-lookup": "^2.0.2", + "sebastian/complexity": "^2.0", + "sebastian/environment": "^5.1.2", + "sebastian/lines-of-code": "^1.0.3", + "sebastian/version": "^3.0.1", + "theseer/tokenizer": "^1.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:37:42+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.19", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a1a54a473501ef4cdeaae4e06891674114d79db8", + "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.3.1 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.1", + "phar-io/manifest": "^2.0.3", + "phar-io/version": "^3.0.2", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.28", + "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.3", + "phpunit/php-timer": "^5.0.2", + "sebastian/cli-parser": "^1.0.1", + "sebastian/code-unit": "^1.0.6", + "sebastian/comparator": "^4.0.8", + "sebastian/diff": "^4.0.3", + "sebastian/environment": "^5.1.3", + "sebastian/exporter": "^4.0.5", + "sebastian/global-state": "^5.0.1", + "sebastian/object-enumerator": "^4.0.3", + "sebastian/resource-operations": "^3.0.3", + "sebastian/type": "^3.2", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.19" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2024-04-05T04:35:58+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:27:43+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T12:41:17+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:19:30+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:30:58+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:33:00+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:35:11+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:20:34+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:07:39+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-14T16:00:52+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:13:03+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.6.40", + "ext-mbstring": "*" + }, + "platform-dev": [], + "plugin-api-version": "2.2.0" +} diff --git a/lib/composer/vendor/johngrogg/ics-parser/ecs.php b/lib/composer/vendor/johngrogg/ics-parser/ecs.php new file mode 100644 index 0000000..025c69c --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/ecs.php @@ -0,0 +1,205 @@ +disableParallel(); + + // https://github.com/easy-coding-standard/easy-coding-standard/blob/main/config/set/psr12.php + $ecsConfig->import(SetList::PSR_12); + + $ecsConfig->lineEnding("\n"); + + $ecsConfig->skip(array( + // Fixers + 'PhpCsFixer\Fixer\Whitespace\StatementIndentationFixer' => array('examples/index.php'), + 'PhpCsFixer\Fixer\Basic\BracesFixer' => null, + 'PhpCsFixer\Fixer\Operator\BinaryOperatorSpacesFixer' => null, + 'PhpCsFixer\Fixer\Operator\NotOperatorWithSuccessorSpaceFixer' => null, + 'PhpCsFixer\Fixer\Phpdoc\PhpdocScalarFixer' => null, + 'PhpCsFixer\Fixer\Phpdoc\PhpdocSummaryFixer' => null, + 'PhpCsFixer\Fixer\Phpdoc\PhpdocVarWithoutNameFixer' => null, + 'PhpCsFixer\Fixer\ReturnNotation\SimplifiedNullReturnFixer' => null, + // Requires PHP 7.1 and above + 'PhpCsFixer\Fixer\ClassNotation\VisibilityRequiredFixer' => null, + )); + + $ecsConfig->ruleWithConfiguration(SpaceAfterNotSniff::class, array('spacing' => 0)); + + $ecsConfig->ruleWithConfiguration(ArraySyntaxFixer::class, array('syntax' => 'long')); + + $ecsConfig->ruleWithConfiguration( + YodaStyleFixer::class, + array( + 'equal' => false, + 'identical' => false, + 'less_and_greater' => false, + ) + ); + + $ecsConfig->ruleWithConfiguration(ListSyntaxFixer::class, array('syntax' => 'long')); // PHP 5.6 + + $ecsConfig->ruleWithConfiguration( + BlankLineBeforeStatementFixer::class, + array( + 'statements' => array( + 'continue', + 'declare', + 'return', + 'throw', + 'try', + ), + ) + ); + + $ecsConfig->rules( + array( + AlphabeticallySortedUsesSniff::class, + UnusedVariableSniff::class, + SelfMemberReferenceSniff::class, + BlankLinesBeforeNamespaceFixer::class, + CastSpacesFixer::class, + ClassDefinitionFixer::class, + CompactNullableTypehintFixer::class, + ConstantCaseFixer::class, + ElseifFixer::class, + EncodingFixer::class, + FullOpeningTagFixer::class, + FunctionDeclarationFixer::class, + HeredocToNowdocFixer::class, + IncludeFixer::class, + LambdaNotUsedImportFixer::class, + LineEndingFixer::class, + LowercaseKeywordsFixer::class, + LowercaseStaticReferenceFixer::class, + MagicConstantCasingFixer::class, + MagicMethodCasingFixer::class, + MethodArgumentSpaceFixer::class, + MultilineWhitespaceBeforeSemicolonsFixer::class, + NativeFunctionCasingFixer::class, + NativeFunctionTypeDeclarationCasingFixer::class, + NoAliasFunctionsFixer::class, + NoClosingTagFixer::class, + NoEmptyPhpdocFixer::class, + NoEmptyStatementFixer::class, + NoExtraBlankLinesFixer::class, + NoLeadingNamespaceWhitespaceFixer::class, + NoMixedEchoPrintFixer::class, + NoMultilineWhitespaceAroundDoubleArrowFixer::class, + NoShortBoolCastFixer::class, + NoSpacesAfterFunctionNameFixer::class, + NoSpacesInsideParenthesisFixer::class, + NoTrailingCommaInSinglelineFixer::class, + NoTrailingWhitespaceInCommentFixer::class, + NoUnneededControlParenthesesFixer::class, + NoUnneededCurlyBracesFixer::class, + NoUnreachableDefaultArgumentValueFixer::class, + NoUnusedImportsFixer::class, + NoUselessReturnFixer::class, + NoWhitespaceInBlankLineFixer::class, + NormalizeIndexBraceFixer::class, + ObjectOperatorWithoutWhitespaceFixer::class, + PhpdocIndentFixer::class, + PhpdocInlineTagNormalizerFixer::class, + PhpdocNoAccessFixer::class, + PhpdocNoPackageFixer::class, + PhpdocNoUselessInheritdocFixer::class, + PhpdocParamOrderFixer::class, + PhpdocSingleLineVarSpacingFixer::class, + PhpdocToCommentFixer::class, + PhpdocTrimFixer::class, + PhpdocTypesFixer::class, + SingleBlankLineAtEofFixer::class, + SingleClassElementPerStatementFixer::class, + SingleImportPerStatementFixer::class, + SingleLineAfterImportsFixer::class, + SingleLineCommentStyleFixer::class, + SingleQuoteFixer::class, + SpaceAfterSemicolonFixer::class, + StandardizeNotEqualsFixer::class, + SwitchCaseSemicolonToColonFixer::class, + SwitchCaseSpaceFixer::class, + TrailingCommaInMultilineFixer::class, + TrimArraySpacesFixer::class, + TypeDeclarationSpacesFixer::class, + ) + ); +}; diff --git a/lib/composer/vendor/johngrogg/ics-parser/examples/ICal.ics b/lib/composer/vendor/johngrogg/ics-parser/examples/ICal.ics new file mode 100644 index 0000000..9c1346c --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/examples/ICal.ics @@ -0,0 +1,338 @@ +BEGIN:VCALENDAR +PRODID:-//Google Inc//Google Calendar 70.9054//EN +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:Testkalender +X-WR-TIMEZONE:UTC +X-WR-CALDESC:Nur zum testen vom Google Kalender +BEGIN:VFREEBUSY +UID:f06ff6b3564b2f696bf42d393f8dea59 +ORGANIZER:MAILTO:jane_smith@host1.com +DTSTAMP:20170316T204607Z +DTSTART:20170213T204607Z +DTEND:20180517T204607Z +URL:https://www.host.com/calendar/busytime/jsmith.ifb +FREEBUSY;FBTYPE=BUSY:20170623T070000Z/20170223T110000Z +FREEBUSY;FBTYPE=BUSY:20170624T131500Z/20170316T151500Z +FREEBUSY;FBTYPE=BUSY:20170715T131500Z/20170416T150000Z +FREEBUSY;FBTYPE=BUSY:20170716T131500Z/20170516T100500Z +END:VFREEBUSY +BEGIN:VEVENT +DTSTART:20171032T000000 +DTEND:20171101T2300 +DESCRIPTION:Invalid date - parser will skip the event +SUMMARY:Invalid date - parser will skip the event +DTSTAMP:20170406T063924 +LOCATION: +UID:f81b0b41a2e138ae0903daee0a966e1e +SEQUENCE:0 +END:VEVENT +BEGIN:VEVENT +DTSTART;VALUE=DATE;TZID=America/Los_Angeles:19410512 +DTEND;VALUE=DATE;TZID=America/Los_Angeles:19410512 +DTSTAMP;TZID=America/Los_Angeles:19410512T195741Z +UID:dh3fki5du0opa7cs5n5s87ca02@google.com +CREATED:20380101T141901Z +DESCRIPTION;LANGUAGE=en-gb: +LAST-MODIFIED:20380101T141901Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY;LANGUAGE=en-gb:Before 1970-Test: Konrad Zuse invents the Z3, the "first + digital Computer" +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +DTSTART;VALUE=DATE:20380201 +DTEND;VALUE=DATE:20380202 +DTSTAMP;TZID="GMT Standard Time":20380101T195741Z +UID:dh3fki5du0opa7cs5n5s87ca01@google.com +CREATED:20380101T141901Z +DESCRIPTION;LANGUAGE=en-gb: +LAST-MODIFIED:20380101T141901Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY;LANGUAGE=en-gb:Year 2038 problem test +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +DTSTART:20160105T090000Z +DTEND:20160107T173000Z +DTSTAMP;TZID="Greenwich Mean Time:Dublin; Edinburgh; Lisbon; London":20110121T195741Z +UID:15lc1nvupht8dtfiptenljoiv4@google.com +CREATED:20110121T195616Z +DESCRIPTION;LANGUAGE=en-gb:This is a short description\nwith a new line. Some "special" 's + igns' may be interesting\, too. +  And a non-breaking space. +LAST-MODIFIED:20150409T150000Z +LOCATION:Kansas +SEQUENCE:2 +STATUS:CONFIRMED +SUMMARY;LANGUAGE=en-gb:My Holidays +TRANSP:TRANSPARENT +ORGANIZER;CN="My Name":mailto:my.name@mydomain.com +END:VEVENT +BEGIN:VEVENT +ATTENDEE;CN="Page, Larry (l.page@google.com)";ROLE=REQ-PARTICIPANT;RSVP=FALSE:mailto:l.page@google.com +ATTENDEE;CN="Brin, Sergey (s.brin@google.com)";ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:s.brin@google.com +DTSTART;VALUE=DATE:20160112 +DTEND;VALUE=DATE:20160116 +DTSTAMP;TZID="GMT Standard Time":20110121T195741Z +UID:1koigufm110c5hnq6ln57murd4@google.com +CREATED:20110119T142901Z +DESCRIPTION;LANGUAGE=en-gb:Project xyz Review Meeting Minutes\n + Agenda\n1. Review of project version 1.0 requirements.\n2. + Definition + of project processes.\n3. Review of project schedule.\n + Participants: John Smith, Jane Doe, Jim Dandy\n-It was + decided that the requirements need to be signed off by + product marketing.\n-Project processes were accepted.\n + -Project schedule needs to account for scheduled holidays + and employee vacation time. Check with HR for specific + dates.\n-New schedule will be distributed by Friday.\n- + Next weeks meeting is cancelled. No meeting until 3/23. +LAST-MODIFIED:20150409T150000Z +LOCATION: +SEQUENCE:2 +STATUS:CONFIRMED +SUMMARY;LANGUAGE=en-gb:Test 2 +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +DTSTART;VALUE=DATE:20160119 +DTEND;VALUE=DATE:20160120 +DTSTAMP;TZID="GMT Standard Time":20110121T195741Z +UID:rq8jng4jgq0m1lvpj8486fttu0@google.com +CREATED:20110119T141904Z +DESCRIPTION;LANGUAGE=en-gb: +LAST-MODIFIED:20150409T150000Z +LOCATION: +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY;LANGUAGE=en-gb:DST Change +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +DTSTART;VALUE=DATE:20160119 +DTEND;VALUE=DATE:20160120 +DTSTAMP;TZID="GMT Standard Time":20110121T195741Z +UID:dh3fki5du0opa7cs5n5s87ca00@google.com +CREATED:20110119T141901Z +DESCRIPTION;LANGUAGE=en-gb: +LAST-MODIFIED:20150409T150000Z +LOCATION: +RRULE:FREQ=WEEKLY;COUNT=5;INTERVAL=2;BYDAY=TU +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY;LANGUAGE=en-gb:Test 1 +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +SUMMARY:Duration Test +DTSTART:20160425T150000Z +DTSTAMP:20160424T150000Z +DURATION:PT1H15M5S +RRULE:FREQ=DAILY;COUNT=2 +UID:calendar-62-e7c39bf02382917349672271dd781c89 +END:VEVENT +BEGIN:VEVENT +SUMMARY:BYMONTHDAY Test +DTSTART:20160922T130000Z +DTEND:20160922T150000Z +DTSTAMP:20160921T130000Z +RRULE:FREQ=MONTHLY;UNTIL=20170923T000000Z;INTERVAL=1;BYMONTHDAY=23 +UID:33844fe8df15fbfc13c97fc41c0c4b00392c6870@google.com +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=Europe/Paris:20160921T080000 +DTEND;TZID=Europe/Paris:20160921T090000 +RRULE:FREQ=WEEKLY;BYDAY=WE +DTSTAMP:20161117T165045Z +UID:884bc8350185031337d9ec49d2e7e101dd5ae5fb@google.com +CREATED:20160920T133918Z +DESCRIPTION: +LAST-MODIFIED:20160920T133923Z +LOCATION: +SEQUENCE:1 +STATUS:CONFIRMED +SUMMARY:Paris Timezone Test +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +DTSTART:20160215T080000Z +DTEND:20160515T090000Z +DTSTAMP:20161121T113027Z +CREATED:20161121T113027Z +UID:65323c541a30dd1f180e2bbfa2724995 +DESCRIPTION: +LAST-MODIFIED:20161121T113027Z +LOCATION: +SEQUENCE:1 +STATUS:CONFIRMED +SUMMARY:Long event covering the range from example with special chars: + ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÕÖÒÓÔØÙÚÛÜÝÞß + àáâãäåæçèéêėëìíîïðñòóôõöøùúûüūýþÿž + ‘ ’ ‚ ‛ “ ” „ ‟ – — … +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +CLASS:PUBLIC +CREATED:20160706T161104Z +DTEND;TZID="(UTC-05:00) Eastern Time (US & Canada)":20160409T110000 +DTSTAMP:20160706T150005Z +DTSTART;TZID="(UTC-05:00) Eastern Time (US & Canada)":20160409T090000 +EXDATE;TZID="(UTC-05:00) Eastern Time (US & Canada)": + 20160528T090000, + 20160625T090000 +LAST-MODIFIED:20160707T182011Z +EXDATE;TZID="(UTC-05:00) Eastern Time (US & Canada)":20160709T090000 +EXDATE;TZID="(UTC-05:00) Eastern Time (US & Canada)":20160723T090000 +LOCATION:Sanctuary +PRIORITY:5 +RRULE:FREQ=WEEKLY;COUNT=15;BYDAY=SA +SEQUENCE:0 +SUMMARY:Microsoft Unicode CLDR EXDATE Test +TRANSP:OPAQUE +UID:040000008200E00074C5B7101A82E0080000000020F6512D0B48CF0100000000000000001000000058BFB8CBB85D504CB99FBA637BCFD6BF +X-MICROSOFT-CDO-BUSYSTATUS:BUSY +X-MICROSOFT-CDO-IMPORTANCE:1 +X-MICROSOFT-DISALLOW-COUNTER:FALSE +END:VEVENT +BEGIN:VEVENT +DTSTART;VALUE=DATE:20170118 +DTEND;VALUE=DATE:20170118 +DTSTAMP;TZID="GMT Standard Time":20170121T195741Z +RRULE:FREQ=MONTHLY;BYSETPOS=3;BYDAY=WE;COUNT=5 +UID:4dnsuc3nknin15kv25cn7ridss@google.com +CREATED:20170119T142059Z +DESCRIPTION;LANGUAGE=en-gb:BYDAY Test 1 +LAST-MODIFIED:20170409T150000Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY;LANGUAGE=en-gb:BYDAY Test 1 +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +DTSTART;VALUE=DATE:20190101 +DTEND;VALUE=DATE:20190101 +DTSTAMP;TZID="GMT Standard Time":20190101T195741Z +RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=1 +UID:4dnsuc3nknin15kv25cn7ridssy@google.com +CREATED:20190101T142059Z +DESCRIPTION;LANGUAGE=en-gb:BYSETPOS First weekday of every month +LAST-MODIFIED:20190101T150000Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY;LANGUAGE=en-gb:BYSETPOS First weekday of every month +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +DTSTART;VALUE=DATE:20190131 +DTEND;VALUE=DATE:20190131 +DTSTAMP;TZID="GMT Standard Time":20190121T195741Z +RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=SU,MO,TU,WE,TH,FR,SA;BYSETPOS=-1 +UID:4dnsuc3nknin15kv25cn7ridssx@google.com +CREATED:20190119T142059Z +DESCRIPTION;LANGUAGE=en-gb:BYSETPOS Last day of every month +LAST-MODIFIED:20190409T150000Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY;LANGUAGE=en-gb:BYSETPOS Last day of every month +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +DTSTART;VALUE=DATE:20170301 +DTEND;VALUE=DATE:20170301 +DTSTAMP;TZID="GMT Standard Time":20170121T195741Z +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=WE +UID:h6f7sdjbpt47v3dkral8lnsgcc@google.com +CREATED:20170119T142040Z +DESCRIPTION;LANGUAGE=en-gb:BYDAY Test 2 +LAST-MODIFIED:20170409T150000Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY;LANGUAGE=en-gb:BYDAY Test 2 +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +DTSTART;VALUE=DATE:20170111 +DTEND;VALUE=DATE:20170111 +DTSTAMP;TZID="GMT Standard Time":20170121T195741Z +RRULE:FREQ=YEARLY;INTERVAL=2;COUNT=5;BYMONTH=1,2,3 +UID:f50e8b89a4a3b0070e0b687d03@google.com +CREATED:20170119T142040Z +DESCRIPTION;LANGUAGE=en-gb:BYMONTH Multiple Test 1 +LAST-MODIFIED:20170409T150000Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY;LANGUAGE=en-gb:BYMONTH Multiple Test 1 +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +DTSTART;VALUE=DATE:20170405 +DTEND;VALUE=DATE:20170405 +DTSTAMP;TZID="GMT Standard Time":20170121T195741Z +RRULE:FREQ=YEARLY;BYMONTH=4,5,6;BYDAY=WE;COUNT=5 +UID:675f06aa795665ae50904ebf0e@google.com +CREATED:20170119T142040Z +DESCRIPTION;LANGUAGE=en-gb:BYMONTH Multiple Test 2 +LAST-MODIFIED:20170409T150000Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY;LANGUAGE=en-gb:BYMONTH Multiple Test 2 +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +BEGIN:VALARM +TRIGGER;VALUE=DURATION:-PT30M +ACTION:DISPLAY +DESCRIPTION:Buzz buzz +END:VALARM +DTSTART;VALUE=DATE;TZID=Germany/Berlin:20170123 +DTEND;VALUE=DATE;TZID=Germany/Berlin:20170123 +DTSTAMP;TZID="GMT Standard Time":20170121T195741Z +RRULE:FREQ=MONTHLY;BYDAY=-2MO;COUNT=5 +EXDATE;VALUE=DATE:20171020 +UID:d287b7ec808fcf084983f10837@google.com +CREATED:20170119T142040Z +DESCRIPTION;LANGUAGE=en-gb:Negative BYDAY +LAST-MODIFIED:20170409T150000Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY;LANGUAGE=en-gb:Negative BYDAY +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=Australia/Sydney:20170813T190000 +DTEND;TZID=Australia/Sydney:20170813T213000 +RRULE:FREQ=MONTHLY;INTERVAL=2;BYDAY=2SU;COUNT=2 +DTSTAMP:20170809T114431Z +UID:testuid@google.com +CREATED:20170802T135539Z +DESCRIPTION: +LAST-MODIFIED:20170802T135935Z +LOCATION: +SEQUENCE:1 +STATUS:CONFIRMED +SUMMARY:Parent Recurrence Event +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=Australia/Sydney:20170813T190000 +DTEND;TZID=Australia/Sydney:20170813T213000 +DTSTAMP:20170809T114431Z +UID:testuid@google.com +RECURRENCE-ID;TZID=Australia/Sydney:20170813T190000 +CREATED:20170802T135539Z +DESCRIPTION: +LAST-MODIFIED:20170809T105604Z +LOCATION:Melbourne VIC\, Australia +SEQUENCE:1 +STATUS:CONFIRMED +SUMMARY:Override Parent Recurrence Event +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR diff --git a/lib/composer/vendor/johngrogg/ics-parser/examples/index.php b/lib/composer/vendor/johngrogg/ics-parser/examples/index.php new file mode 100644 index 0000000..d7e118a --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/examples/index.php @@ -0,0 +1,175 @@ + 2, // Default value + 'defaultTimeZone' => 'UTC', + 'defaultWeekStart' => 'MO', // Default value + 'disableCharacterReplacement' => false, // Default value + 'filterDaysAfter' => null, // Default value + 'filterDaysBefore' => null, // Default value + 'httpUserAgent' => null, // Default value + 'skipRecurrence' => false, // Default value + )); + // $ical->initFile('ICal.ics'); + // $ical->initUrl('https://raw.githubusercontent.com/u01jmg3/ics-parser/master/examples/ICal.ics', $username = null, $password = null, $userAgent = null); +} catch (\Exception $e) { + die($e); +} +?> + + + + + + + PHP ICS Parser example + + + +
+

PHP ICS Parser example

+
    +
  • + The number of events + eventCount ?> +
  • +
  • + The number of free/busy time slots + freeBusyCount ?> +
  • +
  • + The number of todos + todoCount ?> +
  • +
  • + The number of alarms + alarmCount ?> +
  • +
+ + true, + 'range' => true, + 'all' => true, + ); + ?> + + eventsFromInterval('1 week'); + + if ($events) { + echo '

Events in the next 7 days:

'; + } + + $count = 1; + ?> +
+ +
+
+
+

iCalDateToDateTime($event->dtstart_array[3]); + echo $event->summary . ' (' . $dtstart->format('d-m-Y H:i') . ')'; + ?>

+ printData() ?> +
+
+
+ 1 && $count % 3 === 0) { + echo '
'; + } + + $count++; + ?> + +
+ + + eventsFromRange('2017-03-01 12:00:00', '2017-04-31 17:00:00'); + + if ($events) { + echo '

Events March through April:

'; + } + + $count = 1; + ?> +
+ +
+
+
+

iCalDateToDateTime($event->dtstart_array[3]); + echo $event->summary . ' (' . $dtstart->format('d-m-Y H:i') . ')'; + ?>

+ printData() ?> +
+
+
+ 1 && $count % 3 === 0) { + echo '
'; + } + + $count++; + ?> + +
+ + + sortEventsWithOrder($ical->events()); + + if ($events) { + echo '

All Events:

'; + } + ?> +
+ +
+
+
+

iCalDateToDateTime($event->dtstart_array[3]); + echo $event->summary . ' (' . $dtstart->format('d-m-Y H:i') . ')'; + ?>

+ printData() ?> +
+
+
+ 1 && $count % 3 === 0) { + echo '
'; + } + + $count++; + ?> + +
+ +
+ + diff --git a/lib/composer/vendor/johngrogg/ics-parser/phpstan.neon.dist b/lib/composer/vendor/johngrogg/ics-parser/phpstan.neon.dist new file mode 100644 index 0000000..ca0c732 --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/phpstan.neon.dist @@ -0,0 +1,9 @@ +parameters: + paths: + - src + + level: max + + ignoreErrors: + - + identifier: missingType.iterableValue diff --git a/lib/composer/vendor/johngrogg/ics-parser/phpunit.xml b/lib/composer/vendor/johngrogg/ics-parser/phpunit.xml new file mode 100644 index 0000000..f40c752 --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/phpunit.xml @@ -0,0 +1,7 @@ + + + + tests + + + diff --git a/lib/composer/vendor/johngrogg/ics-parser/rector.php b/lib/composer/vendor/johngrogg/ics-parser/rector.php new file mode 100644 index 0000000..8e88aa1 --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/rector.php @@ -0,0 +1,79 @@ +disableParallel(); + + $rectorConfig->importShortClasses(false); + + $rectorConfig->phpVersion(PhpVersion::PHP_56); + + $rectorConfig->skip( + array( + Rector\CodeQuality\Rector\Class_\CompleteDynamicPropertiesRector::class, + Rector\CodeQuality\Rector\Concat\JoinStringConcatRector::class, + Rector\CodeQuality\Rector\FuncCall\ChangeArrayPushToArrayAssignRector::class, + Rector\CodeQuality\Rector\FuncCall\CompactToVariablesRector::class, + Rector\CodeQuality\Rector\FuncCall\InlineIsAInstanceOfRector::class, + Rector\CodeQuality\Rector\FunctionLike\SimplifyUselessVariableRector::class, + Rector\CodeQuality\Rector\Identical\BooleanNotIdenticalToNotIdenticalRector::class, + Rector\CodeQuality\Rector\Identical\SimplifyBoolIdenticalTrueRector::class, + Rector\CodeQuality\Rector\If_\CombineIfRector::class, + Rector\CodeQuality\Rector\If_\ExplicitBoolCompareRector::class, + Rector\CodeQuality\Rector\If_\SimplifyIfElseToTernaryRector::class, + Rector\CodeQuality\Rector\If_\SimplifyIfReturnBoolRector::class, + Rector\CodeQuality\Rector\Isset_\IssetOnPropertyObjectToPropertyExistsRector::class, + Rector\CodingStyle\Rector\Encapsed\EncapsedStringsToSprintfRector::class, + Rector\CodingStyle\Rector\Stmt\NewlineAfterStatementRector::class, + Rector\CodingStyle\Rector\String_\SymplifyQuoteEscapeRector::class, + Rector\DeadCode\Rector\Assign\RemoveUnusedVariableAssignRector::class, + Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPromotedPropertyRector::class, + Rector\DeadCode\Rector\ClassMethod\RemoveUselessParamTagRector::class, + Rector\DeadCode\Rector\ClassMethod\RemoveUselessReturnTagRector::class, + Rector\DeadCode\Rector\StaticCall\RemoveParentCallWithoutParentRector::class, + Rector\Php70\Rector\MethodCall\ThisCallOnStaticMethodToStaticCallRector::class, + Rector\Php70\Rector\StaticCall\StaticCallOnNonStaticToInstanceCallRector::class, + Rector\Php74\Rector\Closure\ClosureToArrowFunctionRector::class, + // PHP 5.6 incompatible + Rector\CodeQuality\Rector\Ternary\ArrayKeyExistsTernaryThenValueToCoalescingRector::class, // PHP 7 + Rector\Php70\Rector\If_\IfToSpaceshipRector::class, + Rector\Php70\Rector\Ternary\TernaryToSpaceshipRector::class, + Rector\Php71\Rector\BooleanOr\IsIterableRector::class, + Rector\Php71\Rector\List_\ListToArrayDestructRector::class, + Rector\Php71\Rector\TryCatch\MultiExceptionCatchRector::class, + Rector\Php73\Rector\FuncCall\ArrayKeyFirstLastRector::class, + Rector\Php73\Rector\BooleanOr\IsCountableRector::class, + Rector\Php74\Rector\Assign\NullCoalescingOperatorRector::class, + Rector\Php74\Rector\StaticCall\ExportToReflectionFunctionRector::class, + Rector\CodingStyle\Rector\ClassConst\RemoveFinalFromConstRector::class, // PHP 8 + ) + ); + + $rectorConfig->sets( + array( + SetList::CODE_QUALITY, + SetList::CODING_STYLE, + SetList::DEAD_CODE, + SetList::PHP_70, + SetList::PHP_71, + SetList::PHP_72, + SetList::PHP_73, + SetList::PHP_74, + SetList::PHP_80, + SetList::PHP_81, + SetList::PHP_82, + ) + ); + + $rectorConfig->rule(TernaryToElvisRector::class); +}; diff --git a/lib/composer/vendor/johngrogg/ics-parser/src/ICal/Event.php b/lib/composer/vendor/johngrogg/ics-parser/src/ICal/Event.php new file mode 100644 index 0000000..8845a66 --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/src/ICal/Event.php @@ -0,0 +1,264 @@ +%s: %s

'; + + /** + * https://www.kanzaki.com/docs/ical/summary.html + * + * @var string + */ + public $summary; + + /** + * https://www.kanzaki.com/docs/ical/dtstart.html + * + * @var string + */ + public $dtstart; + + /** + * https://www.kanzaki.com/docs/ical/dtend.html + * + * @var string + */ + public $dtend; + + /** + * https://www.kanzaki.com/docs/ical/duration.html + * + * @var string|null + */ + public $duration; + + /** + * https://www.kanzaki.com/docs/ical/dtstamp.html + * + * @var string + */ + public $dtstamp; + + /** + * When the event starts, represented as a timezone-adjusted string + * + * @var string + */ + public $dtstart_tz; + + /** + * When the event ends, represented as a timezone-adjusted string + * + * @var string + */ + public $dtend_tz; + + /** + * https://www.kanzaki.com/docs/ical/uid.html + * + * @var string + */ + public $uid; + + /** + * https://www.kanzaki.com/docs/ical/created.html + * + * @var string + */ + public $created; + + /** + * https://www.kanzaki.com/docs/ical/lastModified.html + * + * @var string + */ + public $last_modified; + + /** + * https://www.kanzaki.com/docs/ical/description.html + * + * @var string|null + */ + public $description; + + /** + * https://www.kanzaki.com/docs/ical/location.html + * + * @var string|null + */ + public $location; + + /** + * https://www.kanzaki.com/docs/ical/sequence.html + * + * @var string + */ + public $sequence; + + /** + * https://www.kanzaki.com/docs/ical/status.html + * + * @var string + */ + public $status; + + /** + * https://www.kanzaki.com/docs/ical/transp.html + * + * @var string + */ + public $transp; + + /** + * https://www.kanzaki.com/docs/ical/organizer.html + * + * @var string + */ + public $organizer; + + /** + * https://www.kanzaki.com/docs/ical/attendee.html + * + * @var string + */ + public $attendee; + + /** + * Manage additional properties + * + * @var array + */ + public $additionalProperties = array(); + + /** + * Creates the Event object + * + * @param array $data + * @return void + */ + public function __construct(array $data = array()) + { + foreach ($data as $key => $value) { + $variable = self::snakeCase($key); + if (property_exists($this, $variable)) { + $this->{$variable} = $this->prepareData($value); + } else { + $this->additionalProperties[$variable] = $this->prepareData($value); + } + } + } + + /** + * Magic getter method + * + * @param string $additionalPropertyName + * @return mixed + */ + public function __get($additionalPropertyName) + { + if (array_key_exists($additionalPropertyName, $this->additionalProperties)) { + return $this->additionalProperties[$additionalPropertyName]; + } + + return null; + } + + /** + * Magic isset method + * + * @param string $name + * @return boolean + */ + public function __isset($name) + { + return is_null($this->$name) === false; + } + + /** + * Prepares the data for output + * + * @param mixed $value + * @return mixed + */ + protected function prepareData($value) + { + if (is_string($value)) { + return stripslashes(trim(str_replace('\n', "\n", $value))); + } + + if (is_array($value)) { + return array_map(function ($value) { + return $this->prepareData($value); + }, $value); + } + + return $value; + } + + /** + * Returns Event data excluding anything blank + * within an HTML template + * + * @param string $html HTML template to use + * @return string + */ + public function printData($html = self::HTML_TEMPLATE) + { + $data = array( + 'SUMMARY' => $this->summary, + 'DTSTART' => $this->dtstart, + 'DTEND' => $this->dtend, + 'DTSTART_TZ' => $this->dtstart_tz, + 'DTEND_TZ' => $this->dtend_tz, + 'DURATION' => $this->duration, + 'DTSTAMP' => $this->dtstamp, + 'UID' => $this->uid, + 'CREATED' => $this->created, + 'LAST-MODIFIED' => $this->last_modified, + 'DESCRIPTION' => $this->description, + 'LOCATION' => $this->location, + 'SEQUENCE' => $this->sequence, + 'STATUS' => $this->status, + 'TRANSP' => $this->transp, + 'ORGANISER' => $this->organizer, + 'ATTENDEE(S)' => $this->attendee, + ); + + // Remove any blank values + $data = array_filter($data); + + $output = ''; + + foreach ($data as $key => $value) { + $output .= sprintf($html, $key, $value); + } + + return $output; + } + + /** + * Converts the given input to snake_case + * + * @param string $input + * @param string $glue + * @param string $separator + * @return string + */ + protected static function snakeCase($input, $glue = '_', $separator = '-') + { + $inputSplit = preg_split('/(?<=[a-z])(?=[A-Z])/x', $input); + + if ($inputSplit === false) { + return $input; + } + + $inputSplit = implode($glue, $inputSplit); + $inputSplit = str_replace($separator, $glue, $inputSplit); + + return strtolower($inputSplit); + } +} diff --git a/lib/composer/vendor/johngrogg/ics-parser/src/ICal/ICal.php b/lib/composer/vendor/johngrogg/ics-parser/src/ICal/ICal.php new file mode 100644 index 0000000..f4aab16 --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/src/ICal/ICal.php @@ -0,0 +1,2731 @@ + + * @license https://opensource.org/licenses/mit-license.php MIT License + * @version 3.2.0 + */ + +namespace ICal; + +class ICal +{ + // phpcs:disable Generic.Arrays.DisallowLongArraySyntax + + const DATE_TIME_FORMAT = 'Ymd\THis'; + const DATE_TIME_FORMAT_PRETTY = 'F Y H:i:s'; + const ICAL_DATE_TIME_TEMPLATE = 'TZID=%s:'; + const ISO_8601_WEEK_START = 'MO'; + const RECURRENCE_EVENT = 'Generated recurrence event'; + const SECONDS_IN_A_WEEK = 604800; + const TIME_FORMAT = 'His'; + const TIME_ZONE_UTC = 'UTC'; + const UNIX_FORMAT = 'U'; + const UNIX_MIN_YEAR = 1970; + + /** + * Tracks the number of alarms in the current iCal feed + * + * @var integer + */ + public $alarmCount = 0; + + /** + * Tracks the number of events in the current iCal feed + * + * @var integer + */ + public $eventCount = 0; + + /** + * Tracks the free/busy count in the current iCal feed + * + * @var integer + */ + public $freeBusyCount = 0; + + /** + * Tracks the number of todos in the current iCal feed + * + * @var integer + */ + public $todoCount = 0; + + /** + * The value in years to use for indefinite, recurring events + * + * @var integer + */ + public $defaultSpan = 2; + + /** + * Enables customisation of the default time zone + * + * @var string|null + */ + public $defaultTimeZone; + + /** + * The two letter representation of the first day of the week + * + * @var string + */ + public $defaultWeekStart = self::ISO_8601_WEEK_START; + + /** + * Toggles whether to skip the parsing of recurrence rules + * + * @var boolean + */ + public $skipRecurrence = false; + + /** + * Toggles whether to disable all character replacement. + * + * @var boolean + */ + public $disableCharacterReplacement = false; + + /** + * If this value is an integer, the parser will ignore all events more than roughly this many days before now. + * If this value is a date, the parser will ignore all events occurring before this date. + * + * @var \DateTimeInterface|integer|null + */ + public $filterDaysBefore; + + /** + * If this value is an integer, the parser will ignore all events more than roughly this many days after now. + * If this value is a date, the parser will ignore all events occurring after this date. + * + * @var \DateTimeInterface|integer|null + */ + public $filterDaysAfter; + + /** + * The parsed calendar + * + * @var array + */ + public $cal = array(); + + /** + * Tracks the VFREEBUSY component + * + * @var integer + */ + protected $freeBusyIndex = 0; + + /** + * Variable to track the previous keyword + * + * @var string + */ + protected $lastKeyword; + + /** + * Cache valid IANA time zone IDs to avoid unnecessary lookups + * + * @var array + */ + protected $validIanaTimeZones = array(); + + /** + * Event recurrence instances that have been altered + * + * @var array + */ + protected $alteredRecurrenceInstances = array(); + + /** + * An associative array containing weekday conversion data + * + * The order of the days in the array follow the ISO-8601 specification of a week. + * + * @var array + */ + protected $weekdays = array( + 'MO' => 'monday', + 'TU' => 'tuesday', + 'WE' => 'wednesday', + 'TH' => 'thursday', + 'FR' => 'friday', + 'SA' => 'saturday', + 'SU' => 'sunday', + ); + + /** + * An associative array containing frequency conversion terms + * + * @var array + */ + protected $frequencyConversion = array( + 'DAILY' => 'day', + 'WEEKLY' => 'week', + 'MONTHLY' => 'month', + 'YEARLY' => 'year', + ); + + /** + * Holds the username and password for HTTP basic authentication + * + * @var array + */ + protected $httpBasicAuth = array(); + + /** + * Holds the custom User Agent string header + * + * @var string + */ + protected $httpUserAgent; + + /** + * Holds the custom Accept Language string header + * + * @var string + */ + protected $httpAcceptLanguage; + + /** + * Holds the custom HTTP Protocol version + * + * @var string + */ + protected $httpProtocolVersion; + + /** + * Define which variables can be configured + * + * @var array + */ + private static $configurableOptions = array( + 'defaultSpan', + 'defaultTimeZone', + 'defaultWeekStart', + 'disableCharacterReplacement', + 'filterDaysAfter', + 'filterDaysBefore', + 'httpUserAgent', + 'skipRecurrence', + ); + + /** + * CLDR time zones mapped to IANA time zones. + * + * @var array + */ + private static $cldrTimeZonesMap = array( + '(UTC-12:00) International Date Line West' => 'Etc/GMT+12', + '(UTC-11:00) Coordinated Universal Time-11' => 'Etc/GMT+11', + '(UTC-10:00) Hawaii' => 'Pacific/Honolulu', + '(UTC-09:00) Alaska' => 'America/Anchorage', + '(UTC-08:00) Pacific Time (US & Canada)' => 'America/Los_Angeles', + '(UTC-07:00) Arizona' => 'America/Phoenix', + '(UTC-07:00) Chihuahua, La Paz, Mazatlan' => 'America/Chihuahua', + '(UTC-07:00) Mountain Time (US & Canada)' => 'America/Denver', + '(UTC-06:00) Central America' => 'America/Guatemala', + '(UTC-06:00) Central Time (US & Canada)' => 'America/Chicago', + '(UTC-06:00) Guadalajara, Mexico City, Monterrey' => 'America/Mexico_City', + '(UTC-06:00) Saskatchewan' => 'America/Regina', + '(UTC-05:00) Bogota, Lima, Quito, Rio Branco' => 'America/Bogota', + '(UTC-05:00) Chetumal' => 'America/Cancun', + '(UTC-05:00) Eastern Time (US & Canada)' => 'America/New_York', + '(UTC-05:00) Indiana (East)' => 'America/Indianapolis', + '(UTC-04:00) Asuncion' => 'America/Asuncion', + '(UTC-04:00) Atlantic Time (Canada)' => 'America/Halifax', + '(UTC-04:00) Caracas' => 'America/Caracas', + '(UTC-04:00) Cuiaba' => 'America/Cuiaba', + '(UTC-04:00) Georgetown, La Paz, Manaus, San Juan' => 'America/La_Paz', + '(UTC-04:00) Santiago' => 'America/Santiago', + '(UTC-03:30) Newfoundland' => 'America/St_Johns', + '(UTC-03:00) Brasilia' => 'America/Sao_Paulo', + '(UTC-03:00) Cayenne, Fortaleza' => 'America/Cayenne', + '(UTC-03:00) City of Buenos Aires' => 'America/Buenos_Aires', + '(UTC-03:00) Greenland' => 'America/Godthab', + '(UTC-03:00) Montevideo' => 'America/Montevideo', + '(UTC-03:00) Salvador' => 'America/Bahia', + '(UTC-02:00) Coordinated Universal Time-02' => 'Etc/GMT+2', + '(UTC-01:00) Azores' => 'Atlantic/Azores', + '(UTC-01:00) Cabo Verde Is.' => 'Atlantic/Cape_Verde', + '(UTC) Coordinated Universal Time' => 'Etc/GMT', + '(UTC+00:00) Casablanca' => 'Africa/Casablanca', + '(UTC+00:00) Dublin, Edinburgh, Lisbon, London' => 'Europe/London', + '(UTC+00:00) Monrovia, Reykjavik' => 'Atlantic/Reykjavik', + '(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna' => 'Europe/Berlin', + '(UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague' => 'Europe/Budapest', + '(UTC+01:00) Brussels, Copenhagen, Madrid, Paris' => 'Europe/Paris', + '(UTC+01:00) Sarajevo, Skopje, Warsaw, Zagreb' => 'Europe/Warsaw', + '(UTC+01:00) West Central Africa' => 'Africa/Lagos', + '(UTC+02:00) Amman' => 'Asia/Amman', + '(UTC+02:00) Athens, Bucharest' => 'Europe/Bucharest', + '(UTC+02:00) Beirut' => 'Asia/Beirut', + '(UTC+02:00) Cairo' => 'Africa/Cairo', + '(UTC+02:00) Chisinau' => 'Europe/Chisinau', + '(UTC+02:00) Damascus' => 'Asia/Damascus', + '(UTC+02:00) Harare, Pretoria' => 'Africa/Johannesburg', + '(UTC+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius' => 'Europe/Kiev', + '(UTC+02:00) Jerusalem' => 'Asia/Jerusalem', + '(UTC+02:00) Kaliningrad' => 'Europe/Kaliningrad', + '(UTC+02:00) Tripoli' => 'Africa/Tripoli', + '(UTC+02:00) Windhoek' => 'Africa/Windhoek', + '(UTC+03:00) Baghdad' => 'Asia/Baghdad', + '(UTC+03:00) Istanbul' => 'Europe/Istanbul', + '(UTC+03:00) Kuwait, Riyadh' => 'Asia/Riyadh', + '(UTC+03:00) Minsk' => 'Europe/Minsk', + '(UTC+03:00) Moscow, St. Petersburg, Volgograd' => 'Europe/Moscow', + '(UTC+03:00) Nairobi' => 'Africa/Nairobi', + '(UTC+03:30) Tehran' => 'Asia/Tehran', + '(UTC+04:00) Abu Dhabi, Muscat' => 'Asia/Dubai', + '(UTC+04:00) Baku' => 'Asia/Baku', + '(UTC+04:00) Izhevsk, Samara' => 'Europe/Samara', + '(UTC+04:00) Port Louis' => 'Indian/Mauritius', + '(UTC+04:00) Tbilisi' => 'Asia/Tbilisi', + '(UTC+04:00) Yerevan' => 'Asia/Yerevan', + '(UTC+04:30) Kabul' => 'Asia/Kabul', + '(UTC+05:00) Ashgabat, Tashkent' => 'Asia/Tashkent', + '(UTC+05:00) Ekaterinburg' => 'Asia/Yekaterinburg', + '(UTC+05:00) Islamabad, Karachi' => 'Asia/Karachi', + '(UTC+05:30) Chennai, Kolkata, Mumbai, New Delhi' => 'Asia/Calcutta', + '(UTC+05:30) Sri Jayawardenepura' => 'Asia/Colombo', + '(UTC+05:45) Kathmandu' => 'Asia/Katmandu', + '(UTC+06:00) Astana' => 'Asia/Almaty', + '(UTC+06:00) Dhaka' => 'Asia/Dhaka', + '(UTC+06:30) Yangon (Rangoon)' => 'Asia/Rangoon', + '(UTC+07:00) Bangkok, Hanoi, Jakarta' => 'Asia/Bangkok', + '(UTC+07:00) Krasnoyarsk' => 'Asia/Krasnoyarsk', + '(UTC+07:00) Novosibirsk' => 'Asia/Novosibirsk', + '(UTC+08:00) Beijing, Chongqing, Hong Kong, Urumqi' => 'Asia/Shanghai', + '(UTC+08:00) Irkutsk' => 'Asia/Irkutsk', + '(UTC+08:00) Kuala Lumpur, Singapore' => 'Asia/Singapore', + '(UTC+08:00) Perth' => 'Australia/Perth', + '(UTC+08:00) Taipei' => 'Asia/Taipei', + '(UTC+08:00) Ulaanbaatar' => 'Asia/Ulaanbaatar', + '(UTC+09:00) Osaka, Sapporo, Tokyo' => 'Asia/Tokyo', + '(UTC+09:00) Pyongyang' => 'Asia/Pyongyang', + '(UTC+09:00) Seoul' => 'Asia/Seoul', + '(UTC+09:00) Yakutsk' => 'Asia/Yakutsk', + '(UTC+09:30) Adelaide' => 'Australia/Adelaide', + '(UTC+09:30) Darwin' => 'Australia/Darwin', + '(UTC+10:00) Brisbane' => 'Australia/Brisbane', + '(UTC+10:00) Canberra, Melbourne, Sydney' => 'Australia/Sydney', + '(UTC+10:00) Guam, Port Moresby' => 'Pacific/Port_Moresby', + '(UTC+10:00) Hobart' => 'Australia/Hobart', + '(UTC+10:00) Vladivostok' => 'Asia/Vladivostok', + '(UTC+11:00) Chokurdakh' => 'Asia/Srednekolymsk', + '(UTC+11:00) Magadan' => 'Asia/Magadan', + '(UTC+11:00) Solomon Is., New Caledonia' => 'Pacific/Guadalcanal', + '(UTC+12:00) Anadyr, Petropavlovsk-Kamchatsky' => 'Asia/Kamchatka', + '(UTC+12:00) Auckland, Wellington' => 'Pacific/Auckland', + '(UTC+12:00) Coordinated Universal Time+12' => 'Etc/GMT-12', + '(UTC+12:00) Fiji' => 'Pacific/Fiji', + "(UTC+13:00) Nuku'alofa" => 'Pacific/Tongatapu', + '(UTC+13:00) Samoa' => 'Pacific/Apia', + '(UTC+14:00) Kiritimati Island' => 'Pacific/Kiritimati', + ); + + /** + * Maps Windows (non-CLDR) time zone ID to IANA ID. This is pragmatic but not 100% precise as one Windows zone ID + * maps to multiple IANA IDs (one for each territory). For all practical purposes this should be good enough, though. + * + * Source: http://unicode.org/repos/cldr/trunk/common/supplemental/windowsZones.xml + * + * @var array + */ + private static $windowsTimeZonesMap = array( + 'AUS Central Standard Time' => 'Australia/Darwin', + 'AUS Eastern Standard Time' => 'Australia/Sydney', + 'Afghanistan Standard Time' => 'Asia/Kabul', + 'Alaskan Standard Time' => 'America/Anchorage', + 'Aleutian Standard Time' => 'America/Adak', + 'Altai Standard Time' => 'Asia/Barnaul', + 'Arab Standard Time' => 'Asia/Riyadh', + 'Arabian Standard Time' => 'Asia/Dubai', + 'Arabic Standard Time' => 'Asia/Baghdad', + 'Argentina Standard Time' => 'America/Buenos_Aires', + 'Astrakhan Standard Time' => 'Europe/Astrakhan', + 'Atlantic Standard Time' => 'America/Halifax', + 'Aus Central W. Standard Time' => 'Australia/Eucla', + 'Azerbaijan Standard Time' => 'Asia/Baku', + 'Azores Standard Time' => 'Atlantic/Azores', + 'Bahia Standard Time' => 'America/Bahia', + 'Bangladesh Standard Time' => 'Asia/Dhaka', + 'Belarus Standard Time' => 'Europe/Minsk', + 'Bougainville Standard Time' => 'Pacific/Bougainville', + 'Canada Central Standard Time' => 'America/Regina', + 'Cape Verde Standard Time' => 'Atlantic/Cape_Verde', + 'Caucasus Standard Time' => 'Asia/Yerevan', + 'Cen. Australia Standard Time' => 'Australia/Adelaide', + 'Central America Standard Time' => 'America/Guatemala', + 'Central Asia Standard Time' => 'Asia/Almaty', + 'Central Brazilian Standard Time' => 'America/Cuiaba', + 'Central Europe Standard Time' => 'Europe/Budapest', + 'Central European Standard Time' => 'Europe/Warsaw', + 'Central Pacific Standard Time' => 'Pacific/Guadalcanal', + 'Central Standard Time (Mexico)' => 'America/Mexico_City', + 'Central Standard Time' => 'America/Chicago', + 'Chatham Islands Standard Time' => 'Pacific/Chatham', + 'China Standard Time' => 'Asia/Shanghai', + 'Cuba Standard Time' => 'America/Havana', + 'Dateline Standard Time' => 'Etc/GMT+12', + 'E. Africa Standard Time' => 'Africa/Nairobi', + 'E. Australia Standard Time' => 'Australia/Brisbane', + 'E. Europe Standard Time' => 'Europe/Chisinau', + 'E. South America Standard Time' => 'America/Sao_Paulo', + 'Easter Island Standard Time' => 'Pacific/Easter', + 'Eastern Standard Time (Mexico)' => 'America/Cancun', + 'Eastern Standard Time' => 'America/New_York', + 'Egypt Standard Time' => 'Africa/Cairo', + 'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg', + 'FLE Standard Time' => 'Europe/Kiev', + 'Fiji Standard Time' => 'Pacific/Fiji', + 'GMT Standard Time' => 'Europe/London', + 'GTB Standard Time' => 'Europe/Bucharest', + 'Georgian Standard Time' => 'Asia/Tbilisi', + 'Greenland Standard Time' => 'America/Godthab', + 'Greenwich Standard Time' => 'Atlantic/Reykjavik', + 'Haiti Standard Time' => 'America/Port-au-Prince', + 'Hawaiian Standard Time' => 'Pacific/Honolulu', + 'India Standard Time' => 'Asia/Calcutta', + 'Iran Standard Time' => 'Asia/Tehran', + 'Israel Standard Time' => 'Asia/Jerusalem', + 'Jordan Standard Time' => 'Asia/Amman', + 'Kaliningrad Standard Time' => 'Europe/Kaliningrad', + 'Korea Standard Time' => 'Asia/Seoul', + 'Libya Standard Time' => 'Africa/Tripoli', + 'Line Islands Standard Time' => 'Pacific/Kiritimati', + 'Lord Howe Standard Time' => 'Australia/Lord_Howe', + 'Magadan Standard Time' => 'Asia/Magadan', + 'Magallanes Standard Time' => 'America/Punta_Arenas', + 'Marquesas Standard Time' => 'Pacific/Marquesas', + 'Mauritius Standard Time' => 'Indian/Mauritius', + 'Middle East Standard Time' => 'Asia/Beirut', + 'Montevideo Standard Time' => 'America/Montevideo', + 'Morocco Standard Time' => 'Africa/Casablanca', + 'Mountain Standard Time (Mexico)' => 'America/Chihuahua', + 'Mountain Standard Time' => 'America/Denver', + 'Myanmar Standard Time' => 'Asia/Rangoon', + 'N. Central Asia Standard Time' => 'Asia/Novosibirsk', + 'Namibia Standard Time' => 'Africa/Windhoek', + 'Nepal Standard Time' => 'Asia/Katmandu', + 'New Zealand Standard Time' => 'Pacific/Auckland', + 'Newfoundland Standard Time' => 'America/St_Johns', + 'Norfolk Standard Time' => 'Pacific/Norfolk', + 'North Asia East Standard Time' => 'Asia/Irkutsk', + 'North Asia Standard Time' => 'Asia/Krasnoyarsk', + 'North Korea Standard Time' => 'Asia/Pyongyang', + 'Omsk Standard Time' => 'Asia/Omsk', + 'Pacific SA Standard Time' => 'America/Santiago', + 'Pacific Standard Time (Mexico)' => 'America/Tijuana', + 'Pacific Standard Time' => 'America/Los_Angeles', + 'Pakistan Standard Time' => 'Asia/Karachi', + 'Paraguay Standard Time' => 'America/Asuncion', + 'Romance Standard Time' => 'Europe/Paris', + 'Russia Time Zone 10' => 'Asia/Srednekolymsk', + 'Russia Time Zone 11' => 'Asia/Kamchatka', + 'Russia Time Zone 3' => 'Europe/Samara', + 'Russian Standard Time' => 'Europe/Moscow', + 'SA Eastern Standard Time' => 'America/Cayenne', + 'SA Pacific Standard Time' => 'America/Bogota', + 'SA Western Standard Time' => 'America/La_Paz', + 'SE Asia Standard Time' => 'Asia/Bangkok', + 'Saint Pierre Standard Time' => 'America/Miquelon', + 'Sakhalin Standard Time' => 'Asia/Sakhalin', + 'Samoa Standard Time' => 'Pacific/Apia', + 'Sao Tome Standard Time' => 'Africa/Sao_Tome', + 'Saratov Standard Time' => 'Europe/Saratov', + 'Singapore Standard Time' => 'Asia/Singapore', + 'South Africa Standard Time' => 'Africa/Johannesburg', + 'Sri Lanka Standard Time' => 'Asia/Colombo', + 'Sudan Standard Time' => 'Africa/Tripoli', + 'Syria Standard Time' => 'Asia/Damascus', + 'Taipei Standard Time' => 'Asia/Taipei', + 'Tasmania Standard Time' => 'Australia/Hobart', + 'Tocantins Standard Time' => 'America/Araguaina', + 'Tokyo Standard Time' => 'Asia/Tokyo', + 'Tomsk Standard Time' => 'Asia/Tomsk', + 'Tonga Standard Time' => 'Pacific/Tongatapu', + 'Transbaikal Standard Time' => 'Asia/Chita', + 'Turkey Standard Time' => 'Europe/Istanbul', + 'Turks And Caicos Standard Time' => 'America/Grand_Turk', + 'US Eastern Standard Time' => 'America/Indianapolis', + 'US Mountain Standard Time' => 'America/Phoenix', + 'UTC' => 'Etc/GMT', + 'UTC+12' => 'Etc/GMT-12', + 'UTC+13' => 'Etc/GMT-13', + 'UTC-02' => 'Etc/GMT+2', + 'UTC-08' => 'Etc/GMT+8', + 'UTC-09' => 'Etc/GMT+9', + 'UTC-11' => 'Etc/GMT+11', + 'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar', + 'Venezuela Standard Time' => 'America/Caracas', + 'Vladivostok Standard Time' => 'Asia/Vladivostok', + 'W. Australia Standard Time' => 'Australia/Perth', + 'W. Central Africa Standard Time' => 'Africa/Lagos', + 'W. Europe Standard Time' => 'Europe/Berlin', + 'W. Mongolia Standard Time' => 'Asia/Hovd', + 'West Asia Standard Time' => 'Asia/Tashkent', + 'West Bank Standard Time' => 'Asia/Hebron', + 'West Pacific Standard Time' => 'Pacific/Port_Moresby', + 'Yakutsk Standard Time' => 'Asia/Yakutsk', + ); + + /** + * If `$filterDaysBefore` or `$filterDaysAfter` are set then the events are filtered according to the window defined + * by this field and `$windowMaxTimestamp`. + * + * @var integer + */ + private $windowMinTimestamp; + + /** + * If `$filterDaysBefore` or `$filterDaysAfter` are set then the events are filtered according to the window defined + * by this field and `$windowMinTimestamp`. + * + * @var integer + */ + private $windowMaxTimestamp; + + /** + * `true` if either `$filterDaysBefore` or `$filterDaysAfter` are set. + * + * @var boolean + */ + private $shouldFilterByWindow = false; + + /** + * Creates the ICal object + * + * @param mixed $files + * @param array $options + * @return void + */ + public function __construct($files = false, array $options = array()) + { + if (\PHP_VERSION_ID < 80100) { + ini_set('auto_detect_line_endings', '1'); + } + + foreach ($options as $option => $value) { + if (in_array($option, self::$configurableOptions)) { + $this->{$option} = $value; + } + } + + // Fallback to use the system default time zone + if (!isset($this->defaultTimeZone) || !$this->isValidTimeZoneId($this->defaultTimeZone)) { + $this->defaultTimeZone = $this->getDefaultTimeZone(true); + } + + // Ideally you would use `PHP_INT_MIN` from PHP 7 + $php_int_min = -2147483648; + + $this->windowMinTimestamp = $php_int_min; + + if (!is_null($this->filterDaysBefore)) { + if (is_int($this->filterDaysBefore)) { + $this->windowMinTimestamp = (new \DateTime('now')) + ->sub(new \DateInterval('P' . $this->filterDaysBefore . 'D')) + ->getTimestamp(); + } + + if ($this->filterDaysBefore instanceof \DateTimeInterface) { + $this->windowMinTimestamp = $this->filterDaysBefore->getTimestamp(); + } + } + + $this->windowMaxTimestamp = PHP_INT_MAX; + + if (!is_null($this->filterDaysAfter)) { + if (is_int($this->filterDaysAfter)) { + $this->windowMaxTimestamp = (new \DateTime('now')) + ->add(new \DateInterval('P' . $this->filterDaysAfter . 'D')) + ->getTimestamp(); + } + + if ($this->filterDaysAfter instanceof \DateTimeInterface) { + $this->windowMaxTimestamp = $this->filterDaysAfter->getTimestamp(); + } + } + + $this->shouldFilterByWindow = !is_null($this->filterDaysBefore) || !is_null($this->filterDaysAfter); + + if ($files !== false) { + $files = is_array($files) ? $files : array($files); + + foreach ($files as $file) { + if (!is_array($file) && $this->isFileOrUrl($file)) { + $lines = $this->fileOrUrl($file); + } else { + $lines = is_array($file) ? $file : array($file); + } + + $this->initLines($lines); + } + } + } + + /** + * Initialises lines from a string + * + * @param string $string + * @return ICal + */ + public function initString($string) + { + $string = str_replace(array("\r\n", "\n\r", "\r"), "\n", $string); + + if ($this->cal === array()) { + $lines = explode("\n", $string); + + $this->initLines($lines); + } else { + trigger_error('ICal::initString: Calendar already initialised in constructor', E_USER_NOTICE); + } + + return $this; + } + + /** + * Initialises lines from a file + * + * @param string $file + * @return ICal + */ + public function initFile($file) + { + if ($this->cal === array()) { + $lines = $this->fileOrUrl($file); + + $this->initLines($lines); + } else { + trigger_error('ICal::initFile: Calendar already initialised in constructor', E_USER_NOTICE); + } + + return $this; + } + + /** + * Initialises lines from a URL + * + * @param string $url + * @param string $username + * @param string $password + * @param string $userAgent + * @param string $acceptLanguage + * @param string $httpProtocolVersion + * @return ICal + */ + public function initUrl($url, $username = null, $password = null, $userAgent = null, $acceptLanguage = null, $httpProtocolVersion = null) + { + if (!is_null($username) && !is_null($password)) { + $this->httpBasicAuth['username'] = $username; + $this->httpBasicAuth['password'] = $password; + } + + if (!is_null($userAgent)) { + $this->httpUserAgent = $userAgent; + } + + if (!is_null($acceptLanguage)) { + $this->httpAcceptLanguage = $acceptLanguage; + } + + if (!is_null($httpProtocolVersion)) { + $this->httpProtocolVersion = $httpProtocolVersion; + } + + $this->initFile($url); + + return $this; + } + + /** + * Initialises the parser using an array + * containing each line of iCal content + * + * @param array $lines + * @return void + */ + protected function initLines(array $lines) + { + $lines = $this->unfold($lines); + + if (stristr($lines[0], 'BEGIN:VCALENDAR') !== false) { + $component = ''; + foreach ($lines as $line) { + $line = rtrim($line); // Trim trailing whitespace + $line = $this->removeUnprintableChars($line); + + if (empty($line)) { + continue; + } + + if (!$this->disableCharacterReplacement) { + $line = str_replace(array( + ' ', + "\t", + "\xc2\xa0", // Non-breaking space + ), ' ', $line); + + $line = $this->cleanCharacters($line); + } + + $add = $this->keyValueFromString($line); + $keyword = $add[0]; + $values = $add[1]; // May be an array containing multiple values + + if (!is_array($values)) { + if (!empty($values)) { + $values = array($values); // Make an array as not one already + $blankArray = array(); // Empty placeholder array + $values[] = $blankArray; + } else { + $values = array(); // Use blank array to ignore this line + } + } elseif (empty($values[0])) { + $values = array(); // Use blank array to ignore this line + } + + // Reverse so that our array of properties is processed first + $values = array_reverse($values); + + foreach ($values as $value) { + switch ($line) { + // https://www.kanzaki.com/docs/ical/vtodo.html + case 'BEGIN:VTODO': + if (!is_array($value)) { + $this->todoCount++; + } + + $component = 'VTODO'; + + break; + + case 'BEGIN:VEVENT': + // https://www.kanzaki.com/docs/ical/vevent.html + if (!is_array($value)) { + $this->eventCount++; + } + + $component = 'VEVENT'; + + break; + + case 'BEGIN:VFREEBUSY': + // https://www.kanzaki.com/docs/ical/vfreebusy.html + if (!is_array($value)) { + $this->freeBusyIndex++; + } + + $component = 'VFREEBUSY'; + + break; + + case 'BEGIN:VALARM': + if (!is_array($value)) { + $this->alarmCount++; + } + + $component = 'VALARM'; + + break; + + case 'END:VALARM': + $component = 'VEVENT'; + + break; + + case 'BEGIN:DAYLIGHT': + case 'BEGIN:STANDARD': + case 'BEGIN:VCALENDAR': + case 'BEGIN:VTIMEZONE': + $component = $value; + + break; + + case 'END:DAYLIGHT': + case 'END:STANDARD': + case 'END:VCALENDAR': + case 'END:VFREEBUSY': + case 'END:VTIMEZONE': + case 'END:VTODO': + $component = 'VCALENDAR'; + + break; + + case 'END:VEVENT': + if ($this->shouldFilterByWindow) { + $this->removeLastEventIfOutsideWindowAndNonRecurring(); + } + + $component = 'VCALENDAR'; + + break; + + default: + $this->addCalendarComponentWithKeyAndValue($component, $keyword, $value); + + break; + } + } + } + + $this->processEvents(); + + if (!$this->skipRecurrence) { + $this->processRecurrences(); + + // Apply changes to altered recurrence instances + if ($this->alteredRecurrenceInstances !== array()) { + $events = $this->cal['VEVENT']; + + foreach ($this->alteredRecurrenceInstances as $alteredRecurrenceInstance) { + if (isset($alteredRecurrenceInstance['altered-event'])) { + $alteredEvent = $alteredRecurrenceInstance['altered-event']; + $key = key($alteredEvent); + $events[$key] = $alteredEvent[$key]; + } + } + + $this->cal['VEVENT'] = $events; + } + } + + if ($this->shouldFilterByWindow) { + $this->reduceEventsToMinMaxRange(); + } + + $this->processDateConversions(); + } + } + + /** + * Removes the last event (i.e. most recently parsed) if its start date is outside the window spanned by + * `$windowMinTimestamp` / `$windowMaxTimestamp`. + * + * @return void + */ + protected function removeLastEventIfOutsideWindowAndNonRecurring() + { + $events = $this->cal['VEVENT']; + + if ($events !== array()) { + $lastIndex = count($events) - 1; + $lastEvent = $events[$lastIndex]; + + if ((!isset($lastEvent['RRULE']) || $lastEvent['RRULE'] === '') && $this->doesEventStartOutsideWindow($lastEvent)) { + $this->eventCount--; + + unset($events[$lastIndex]); + } + + $this->cal['VEVENT'] = $events; + } + } + + /** + * Reduces the number of events to the defined minimum and maximum range + * + * @return void + */ + protected function reduceEventsToMinMaxRange() + { + $events = (isset($this->cal['VEVENT'])) ? $this->cal['VEVENT'] : array(); + + if ($events !== array()) { + foreach ($events as $key => $anEvent) { + if ($anEvent === null) { + unset($events[$key]); + + continue; + } + + if ($this->doesEventStartOutsideWindow($anEvent)) { + $this->eventCount--; + + unset($events[$key]); + + continue; + } + } + + $this->cal['VEVENT'] = $events; + } + } + + /** + * Determines whether the event start date is outside `$windowMinTimestamp` / `$windowMaxTimestamp`. + * Returns `true` for invalid dates. + * + * @param array $event + * @return boolean + */ + protected function doesEventStartOutsideWindow(array $event) + { + return !$this->isValidDate($event['DTSTART']) || $this->isOutOfRange($event['DTSTART'], $this->windowMinTimestamp, $this->windowMaxTimestamp); + } + + /** + * Determines whether a valid iCalendar date is within a given range + * + * @param string $calendarDate + * @param integer $minTimestamp + * @param integer $maxTimestamp + * @return boolean + */ + protected function isOutOfRange($calendarDate, $minTimestamp, $maxTimestamp) + { + $timestamp = strtotime(explode('T', $calendarDate)[0]); + + return $timestamp < $minTimestamp || $timestamp > $maxTimestamp; + } + + /** + * Unfolds an iCal file in preparation for parsing + * (https://icalendar.org/iCalendar-RFC-5545/3-1-content-lines.html) + * + * @param array $lines + * @return array + */ + protected function unfold(array $lines) + { + $string = implode(PHP_EOL, $lines); + $string = str_ireplace(' ', ' ', $string); + + $cleanedString = preg_replace('/' . PHP_EOL . '[ \t]/', '', $string); + + $lines = explode(PHP_EOL, $cleanedString ?: $string); + + return $lines; + } + + /** + * Add one key and value pair to the `$this->cal` array + * + * @param string $component + * @param string|boolean $keyword + * @param string|array $value + * @return void + */ + protected function addCalendarComponentWithKeyAndValue($component, $keyword, $value) + { + if ($keyword === false) { + $keyword = $this->lastKeyword; + } + + switch ($component) { + case 'VALARM': + $key1 = 'VEVENT'; + $key2 = ($this->eventCount - 1); + $key3 = $component; + + if (!isset($this->cal[$key1][$key2][$key3]["{$keyword}_array"])) { + $this->cal[$key1][$key2][$key3]["{$keyword}_array"] = array(); + } + + if (is_array($value)) { + // Add array of properties to the end + $this->cal[$key1][$key2][$key3]["{$keyword}_array"][] = $value; + } else { + if (!isset($this->cal[$key1][$key2][$key3][$keyword])) { + $this->cal[$key1][$key2][$key3][$keyword] = $value; + } + + if ($this->cal[$key1][$key2][$key3][$keyword] !== $value) { + $this->cal[$key1][$key2][$key3][$keyword] .= ',' . $value; + } + } + break; + + case 'VEVENT': + $key1 = $component; + $key2 = ($this->eventCount - 1); + + if (!isset($this->cal[$key1][$key2]["{$keyword}_array"])) { + $this->cal[$key1][$key2]["{$keyword}_array"] = array(); + } + + if (is_array($value)) { + // Add array of properties to the end + $this->cal[$key1][$key2]["{$keyword}_array"][] = $value; + } else { + if (!isset($this->cal[$key1][$key2][$keyword])) { + $this->cal[$key1][$key2][$keyword] = $value; + } + + if ($keyword === 'EXDATE') { + if (trim($value) === $value) { + $array = array_filter(explode(',', $value)); + $this->cal[$key1][$key2]["{$keyword}_array"][] = $array; + } else { + $value = explode(',', implode(',', $this->cal[$key1][$key2]["{$keyword}_array"][1]) . trim($value)); + $this->cal[$key1][$key2]["{$keyword}_array"][1] = $value; + } + } else { + $this->cal[$key1][$key2]["{$keyword}_array"][] = $value; + + if ($keyword === 'DURATION') { + $duration = new \DateInterval($value); + $this->cal[$key1][$key2]["{$keyword}_array"][] = $duration; + } + } + + if (!is_array($value) && $this->cal[$key1][$key2][$keyword] !== $value) { + $this->cal[$key1][$key2][$keyword] .= ',' . $value; + } + } + break; + + case 'VFREEBUSY': + $key1 = $component; + $key2 = ($this->freeBusyIndex - 1); + $key3 = $keyword; + + if ($keyword === 'FREEBUSY') { + if (is_array($value)) { + $this->cal[$key1][$key2][$key3][][] = $value; + } else { + $this->freeBusyCount++; + + end($this->cal[$key1][$key2][$key3]); + $key = key($this->cal[$key1][$key2][$key3]); + + $value = explode('/', $value); + $this->cal[$key1][$key2][$key3][$key][] = $value; + } + } else { + $this->cal[$key1][$key2][$key3][] = $value; + } + break; + + case 'VTODO': + $this->cal[$component][$this->todoCount - 1][$keyword] = $value; + + break; + + default: + $this->cal[$component][$keyword] = $value; + + break; + } + + if (is_string($keyword)) { + $this->lastKeyword = $keyword; + } + } + + /** + * Gets the key value pair from an iCal string + * + * @param string $text + * @return array + */ + public function keyValueFromString($text) + { + $splitLine = $this->parseLine($text); + $object = array(); + $paramObj = array(); + $valueObj = ''; + $i = 0; + + while ($i < count($splitLine)) { + // The first token corresponds to the property name + if ($i === 0) { + $object[0] = $splitLine[$i]; + $i++; + + continue; + } + + // After each semicolon define the property parameters + if ($splitLine[$i] == ';') { + $i++; + $paramName = $splitLine[$i]; + $i += 2; + $paramValue = array(); + $multiValue = false; + // A parameter can have multiple values separated by a comma + while ($i + 1 < count($splitLine) && $splitLine[$i + 1] === ',') { + $paramValue[] = $splitLine[$i]; + $i += 2; + $multiValue = true; + } + + if ($multiValue) { + $paramValue[] = $splitLine[$i]; + } else { + $paramValue = $splitLine[$i]; + } + + // Create object with paramName => paramValue + $paramObj[$paramName] = $paramValue; + } + + // After a colon all tokens are concatenated (non-standard behaviour because the property can have multiple values + // according to RFC5545) + if ($splitLine[$i] === ':') { + $i++; + while ($i < count($splitLine)) { + $valueObj .= $splitLine[$i]; + $i++; + } + } + + $i++; + } + + // Object construction + if ($paramObj !== array()) { + $object[1][0] = $valueObj; + $object[1][1] = $paramObj; + } else { + $object[1] = $valueObj; + } + + return $object; + } + + /** + * Parses a line from an iCal file into an array of tokens + * + * @param string $line + * @return array + */ + protected function parseLine($line) + { + $words = array(); + $word = ''; + // The use of str_split is not a problem here even if the character set is in utf8 + // Indeed we only compare the characters , ; : = " which are on a single byte + $arrayOfChar = str_split($line); + $inDoubleQuotes = false; + + foreach ($arrayOfChar as $char) { + // Don't stop the word on ; , : = if it is enclosed in double quotes + if ($char === '"') { + if ($word !== '') { + $words[] = $word; + } + + $word = ''; + $inDoubleQuotes = !$inDoubleQuotes; + } elseif (!in_array($char, array(';', ':', ',', '=')) || $inDoubleQuotes) { + $word .= $char; + } else { + if ($word !== '') { + $words[] = $word; + } + + $words[] = $char; + $word = ''; + } + } + + $words[] = $word; + + return $words; + } + + /** + * Returns the default time zone if set. + * Falls back to the system default if not set. + * + * @param boolean $forceReturnSystemDefault + * @return string + */ + private function getDefaultTimeZone($forceReturnSystemDefault = false) + { + $systemDefault = date_default_timezone_get(); + + if ($forceReturnSystemDefault) { + return $systemDefault; + } + + return $this->defaultTimeZone ?: $systemDefault; + } + + /** + * Returns a `DateTime` object from an iCal date time format + * + * @param string $icalDate + * @return \DateTime|false + * @throws \Exception + */ + public function iCalDateToDateTime($icalDate) + { + /** + * iCal times may be in 3 formats, (https://www.kanzaki.com/docs/ical/dateTime.html) + * + * UTC: Has a trailing 'Z' + * Floating: No time zone reference specified, no trailing 'Z', use local time + * TZID: Set time zone as specified + * + * Use DateTime class objects to get around limitations with `mktime` and `gmmktime`. + * Must have a local time zone set to process floating times. + */ + $pattern = '/^(?:TZID=)?([^:]*|".*")'; // [1]: Time zone + $pattern .= ':?'; // Time zone delimiter + $pattern .= '(\d{8})'; // [2]: YYYYMMDD + $pattern .= 'T?'; // Time delimiter + $pattern .= '(?(?<=T)(\d{6}))'; // [3]: HHMMSS (filled if delimiter present) + $pattern .= '(Z?)/'; // [4]: UTC flag + + preg_match($pattern, $icalDate, $date); + + if ($date === array()) { + throw new \Exception('Invalid iCal date format.'); + } + + // A Unix timestamp usually cannot represent a date prior to 1 Jan 1970. + // PHP, on the other hand, uses negative numbers for that. Thus we don't + // need to special case them. + + if ($date[4] === 'Z') { + $dateTimeZone = new \DateTimeZone(self::TIME_ZONE_UTC); + } elseif (isset($date[1]) && $date[1] !== '') { + $dateTimeZone = $this->timeZoneStringToDateTimeZone($date[1]); + } else { + $dateTimeZone = new \DateTimeZone($this->getDefaultTimeZone()); + } + + // The exclamation mark at the start of the format string indicates that if a + // time portion is not included, the time in the returned DateTime should be + // set to 00:00:00. Without it, the time would be set to the current system time. + $dateFormat = '!Ymd'; + $dateBasic = $date[2]; + if (isset($date[3]) && $date[3] !== '') { + $dateBasic .= "T{$date[3]}"; + $dateFormat .= '\THis'; + } + + return \DateTime::createFromFormat($dateFormat, $dateBasic, $dateTimeZone); + } + + /** + * Returns a Unix timestamp from an iCal date time format + * + * @param string $icalDate + * @return integer + */ + public function iCalDateToUnixTimestamp($icalDate) + { + $iCalDateToDateTime = $this->iCalDateToDateTime($icalDate); + + if ($iCalDateToDateTime === false) { + trigger_error("ICal::iCalDateToUnixTimestamp: Invalid date passed ({$icalDate})", E_USER_NOTICE); + + return 0; + } + + return $iCalDateToDateTime->getTimestamp(); + } + + /** + * Returns a date adapted to the calendar time zone depending on the event `TZID` + * + * @param array $event + * @param string $key + * @param string|null $format + * @return string|integer|boolean|\DateTime + */ + public function iCalDateWithTimeZone(array $event, $key, $format = self::DATE_TIME_FORMAT) + { + if (!isset($event["{$key}_array"]) || !isset($event[$key])) { + return false; + } + + $dateArray = $event["{$key}_array"]; + + if ($key === 'DURATION') { + $dateTime = $this->parseDuration($event['DTSTART'], $dateArray[2]); + + if ($dateTime instanceof \DateTime === false) { + trigger_error("ICal::iCalDateWithTimeZone: Invalid date passed ({$event['DTSTART']})", E_USER_NOTICE); + + return false; + } + } else { + // When constructing from a Unix Timestamp, no time zone needs passing. + $dateTime = new \DateTime("@{$dateArray[2]}"); + } + + $calendarTimeZone = $this->calendarTimeZone(); + + if (!is_null($calendarTimeZone)) { + // Set the time zone we wish to use when running `$dateTime->format`. + $dateTime->setTimezone(new \DateTimeZone($calendarTimeZone)); + } + + if (is_null($format)) { + return $dateTime; + } + + return $dateTime->format($format); + } + + /** + * Performs admin tasks on all events as read from the iCal file. + * Adds a Unix timestamp to all `{DTSTART|DTEND|RECURRENCE-ID}_array` arrays + * Tracks modified recurrence instances + * + * @return void + */ + protected function processEvents() + { + $checks = null; + $events = (isset($this->cal['VEVENT'])) ? $this->cal['VEVENT'] : array(); + + if ($events !== array()) { + foreach ($events as $key => $anEvent) { + foreach (array('DTSTART', 'DTEND', 'RECURRENCE-ID') as $type) { + if (isset($anEvent[$type])) { + $date = $anEvent["{$type}_array"][1]; + + if (isset($anEvent["{$type}_array"][0]['TZID'])) { + $timeZone = $this->escapeParamText($anEvent["{$type}_array"][0]['TZID']); + $date = sprintf(self::ICAL_DATE_TIME_TEMPLATE, $timeZone) . $date; + } + + $anEvent["{$type}_array"][2] = $this->iCalDateToUnixTimestamp($date); + $anEvent["{$type}_array"][3] = $date; + } + } + + if (isset($anEvent['RECURRENCE-ID'])) { + $uid = $anEvent['UID']; + + if (!isset($this->alteredRecurrenceInstances[$uid])) { + $this->alteredRecurrenceInstances[$uid] = array(); + } + + $recurrenceDateUtc = $this->iCalDateToUnixTimestamp($anEvent['RECURRENCE-ID_array'][3]); + $this->alteredRecurrenceInstances[$uid][$key] = $recurrenceDateUtc; + } + + $events[$key] = $anEvent; + } + + $eventKeysToRemove = array(); + + foreach ($events as $key => $event) { + $checks[] = !isset($event['RECURRENCE-ID']); + $checks[] = isset($event['UID']); + $checks[] = isset($event['UID']) && isset($this->alteredRecurrenceInstances[$event['UID']]); + + if ((bool) array_product($checks)) { + $eventDtstartUnix = $this->iCalDateToUnixTimestamp($event['DTSTART_array'][3]); + + // phpcs:ignore CustomPHPCS.ControlStructures.AssignmentInCondition + if (($alteredEventKey = array_search($eventDtstartUnix, $this->alteredRecurrenceInstances[$event['UID']], true)) !== false) { + $eventKeysToRemove[] = $alteredEventKey; + + $alteredEvent = array_replace_recursive($events[$key], $events[$alteredEventKey]); + $this->alteredRecurrenceInstances[$event['UID']]['altered-event'] = array($key => $alteredEvent); + } + } + + unset($checks); + } + + foreach ($eventKeysToRemove as $eventKeyToRemove) { + $events[$eventKeyToRemove] = null; + } + + $this->cal['VEVENT'] = $events; + } + } + + /** + * Processes recurrence rules + * + * @return void + */ + protected function processRecurrences() + { + $events = (isset($this->cal['VEVENT'])) ? $this->cal['VEVENT'] : array(); + + // If there are no events, then we have nothing to process. + if ($events === array()) { + return; + } + + $allEventRecurrences = array(); + $eventKeysToRemove = array(); + + foreach ($events as $key => $anEvent) { + if (!isset($anEvent['RRULE']) || $anEvent['RRULE'] === '') { + continue; + } + + // Tag as generated by a recurrence rule + $anEvent['RRULE_array'][2] = self::RECURRENCE_EVENT; + + // Create new initial starting point. + $initialEventDate = $this->icalDateToDateTime($anEvent['DTSTART_array'][3]); + + if ($initialEventDate === false) { + trigger_error("ICal::processRecurrences: Invalid date passed ({$anEvent['DTSTART_array'][3]})", E_USER_NOTICE); + + continue; + } + + // Separate the RRULE stanzas, and explode the values that are lists. + $rrules = array(); + foreach (array_filter(explode(';', $anEvent['RRULE'])) as $s) { + list($k, $v) = explode('=', $s); + if (in_array($k, array('BYSETPOS', 'BYDAY', 'BYMONTHDAY', 'BYMONTH', 'BYYEARDAY', 'BYWEEKNO'))) { + $rrules[$k] = explode(',', $v); + } else { + $rrules[$k] = $v; + } + } + + $frequency = $rrules['FREQ']; + + if (!is_string($frequency)) { + trigger_error('ICal::processRecurrences: Invalid frequency passed', E_USER_NOTICE); + + continue; + } + + // Reject RRULE if BYDAY stanza is invalid: + // > The BYDAY rule part MUST NOT be specified with a numeric value + // > when the FREQ rule part is not set to MONTHLY or YEARLY. + // > Furthermore, the BYDAY rule part MUST NOT be specified with a + // > numeric value with the FREQ rule part set to YEARLY when the + // > BYWEEKNO rule part is specified. + if (isset($rrules['BYDAY'])) { + $checkByDays = function ($carry, $weekday) { + return $carry && substr($weekday, -2) === $weekday; + }; + if (!in_array($frequency, array('MONTHLY', 'YEARLY'))) { + if (is_array($rrules['BYDAY']) && !array_reduce($rrules['BYDAY'], $checkByDays, true)) { + trigger_error("ICal::processRecurrences: A {$frequency} RRULE may not contain BYDAY values with numeric prefixes", E_USER_NOTICE); + + continue; + } + } elseif ($frequency === 'YEARLY' && (isset($rrules['BYWEEKNO']) && ($rrules['BYWEEKNO'] !== '' && $rrules['BYWEEKNO'] !== array()))) { + if (is_array($rrules['BYDAY']) && !array_reduce($rrules['BYDAY'], $checkByDays, true)) { + trigger_error('ICal::processRecurrences: A YEARLY RRULE with a BYWEEKNO part may not contain BYDAY values with numeric prefixes', E_USER_NOTICE); + + continue; + } + } + } + + $interval = (empty($rrules['INTERVAL'])) ? 1 : (int) $rrules['INTERVAL']; + + // Throw an error if this isn't an integer. + if (!is_int($this->defaultSpan)) { + trigger_error('ICal::defaultSpan: User defined value is not an integer', E_USER_NOTICE); + } + + // Compute EXDATEs + $exdates = $this->parseExdates($anEvent); + + // Determine if the initial date is also an EXDATE + $initialDateIsExdate = array_reduce($exdates, function ($carry, $exdate) use ($initialEventDate) { + return $carry || $exdate->getTimestamp() === $initialEventDate->getTimestamp(); + }, false); + + if ($initialDateIsExdate) { + $eventKeysToRemove[] = $key; + } + + /** + * Determine at what point we should stop calculating recurrences + * by looking at the UNTIL or COUNT rrule stanza, or, if neither + * if set, using a fallback. + * + * If the initial date is also an EXDATE, it shouldn't be included + * in the count. + * + * Syntax: + * UNTIL={enddate} + * COUNT= + * + * Where: + * enddate = || + */ + $count = 1; + $countLimit = (isset($rrules['COUNT'])) ? intval($rrules['COUNT']) : PHP_INT_MAX; + $now = date_create(); + + $until = $now === false + ? 0 + : $now->modify("{$this->defaultSpan} years")->setTime(23, 59, 59)->getTimestamp(); + + $untilWhile = $until; + + if (isset($rrules['UNTIL']) && is_string($rrules['UNTIL'])) { + $untilDT = $this->iCalDateToDateTime($rrules['UNTIL']); + $until = min($until, ($untilDT === false) ? $until : $untilDT->getTimestamp()); + + // There are certain edge cases where we need to go a little beyond the UNTIL to + // ensure we get all events. Consider: + // + // DTSTART:20200103 + // RRULE:FREQ=MONTHLY;BYDAY=-5FR;UNTIL=20200502 + // + // In this case the last occurrence should be 1st May, however when we transition + // from April to May: + // + // $until ~= 2nd May + // $frequencyRecurringDateTime ~= 3rd May + // + // And as the latter comes after the former, the while loop ends before any dates + // in May have the chance to be considered. + $untilWhile = min($untilWhile, ($untilDT === false) ? $untilWhile : $untilDT->modify("+1 {$this->frequencyConversion[$frequency]}")->getTimestamp()); + } + + $eventRecurrences = array(); + + $frequencyRecurringDateTime = clone $initialEventDate; + while ($frequencyRecurringDateTime->getTimestamp() <= $untilWhile && $count < $countLimit) { + $candidateDateTimes = array(); + + // phpcs:ignore Squiz.ControlStructures.SwitchDeclaration.MissingDefault + switch ($frequency) { + case 'DAILY': + if (isset($rrules['BYMONTHDAY']) && (is_array($rrules['BYMONTHDAY']) && $rrules['BYMONTHDAY'] !== array())) { + if (!isset($monthDays)) { + // This variable is unset when we change months (see below) + $monthDays = $this->getDaysOfMonthMatchingByMonthDayRRule($rrules['BYMONTHDAY'], $frequencyRecurringDateTime); + } + + if (!in_array($frequencyRecurringDateTime->format('j'), $monthDays)) { + break; + } + } + + $candidateDateTimes[] = clone $frequencyRecurringDateTime; + + break; + + case 'WEEKLY': + $initialDayOfWeek = $frequencyRecurringDateTime->format('N'); + $matchingDays = array($initialDayOfWeek); + + if (isset($rrules['BYDAY']) && (is_array($rrules['BYDAY']) && $rrules['BYDAY'] !== array())) { + // setISODate() below uses the ISO-8601 specification of weeks: start on + // a Monday, end on a Sunday. However, RRULEs (or the caller of the + // parser) may state an alternate WeeKSTart. + $wkstTransition = 7; + + if (empty($rrules['WKST'])) { + if ($this->defaultWeekStart !== self::ISO_8601_WEEK_START) { + $wkstTransition = array_search($this->defaultWeekStart, array_keys($this->weekdays), true); + } + } elseif ($rrules['WKST'] !== self::ISO_8601_WEEK_START) { + $wkstTransition = array_search($rrules['WKST'], array_keys($this->weekdays), true); + } + + $matchingDays = array_map( + function ($weekday) use ($initialDayOfWeek, $wkstTransition, $interval) { + $day = array_search($weekday, array_keys($this->weekdays), true); + + if ($day < $initialDayOfWeek) { + $day += 7; + } + + if ($day >= $wkstTransition) { + $day += 7 * ($interval - 1); + } + + // Ignoring alternate week starts, $day at this point will have a + // value between 0 and 6. But setISODate() expects a value of 1 to 7. + // Even with alternate week starts, we still need to +1 to set the + // correct weekday. + $day++; + + return $day; + }, + $rrules['BYDAY'] + ); + } + + sort($matchingDays); + + foreach ($matchingDays as $day) { + $clonedDateTime = clone $frequencyRecurringDateTime; + $candidateDateTimes[] = $clonedDateTime->setISODate( + (int) $frequencyRecurringDateTime->format('o'), + (int) $frequencyRecurringDateTime->format('W'), + (int) $day + ); + } + break; + + case 'MONTHLY': + $matchingDays = array(); + + if (isset($rrules['BYMONTHDAY']) && (is_array($rrules['BYMONTHDAY']) && $rrules['BYMONTHDAY'] !== array())) { + $matchingDays = $this->getDaysOfMonthMatchingByMonthDayRRule($rrules['BYMONTHDAY'], $frequencyRecurringDateTime); + if (isset($rrules['BYDAY']) && (is_array($rrules['BYDAY']) && $rrules['BYDAY'] !== array())) { + $matchingDays = array_filter( + $this->getDaysOfMonthMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime), + function ($monthDay) use ($matchingDays) { + return in_array($monthDay, $matchingDays); + } + ); + } + } elseif (isset($rrules['BYDAY']) && (is_array($rrules['BYDAY']) && $rrules['BYDAY'] !== array())) { + $matchingDays = $this->getDaysOfMonthMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime); + } else { + $matchingDays[] = $frequencyRecurringDateTime->format('d'); + } + + if (isset($rrules['BYSETPOS']) && (is_array($rrules['BYSETPOS']) && $rrules['BYSETPOS'] !== array())) { + $matchingDays = $this->filterValuesUsingBySetPosRRule($rrules['BYSETPOS'], $matchingDays); + } + + foreach ($matchingDays as $day) { + // Skip invalid dates (e.g. 30th February) + if ($day > $frequencyRecurringDateTime->format('t')) { + continue; + } + + $clonedDateTime = clone $frequencyRecurringDateTime; + $candidateDateTimes[] = $clonedDateTime->setDate( + (int) $frequencyRecurringDateTime->format('Y'), + (int) $frequencyRecurringDateTime->format('m'), + $day + ); + } + break; + + case 'YEARLY': + $matchingDays = array(); + + if (isset($rrules['BYMONTH']) && (is_array($rrules['BYMONTH']) && $rrules['BYMONTH'] !== array())) { + $bymonthRecurringDatetime = clone $frequencyRecurringDateTime; + foreach ($rrules['BYMONTH'] as $byMonth) { + $bymonthRecurringDatetime->setDate( + (int) $frequencyRecurringDateTime->format('Y'), + (int) $byMonth, + (int) $frequencyRecurringDateTime->format('d') + ); + + // Determine the days of the month affected + // (The interaction between BYMONTHDAY and BYDAY is resolved later.) + $monthDays = array(); + if (isset($rrules['BYMONTHDAY']) && (is_array($rrules['BYMONTHDAY']) && $rrules['BYMONTHDAY'] !== array())) { + $monthDays = $this->getDaysOfMonthMatchingByMonthDayRRule($rrules['BYMONTHDAY'], $bymonthRecurringDatetime); + } elseif (isset($rrules['BYDAY']) && (is_array($rrules['BYDAY']) && $rrules['BYDAY'] !== array())) { + $monthDays = $this->getDaysOfMonthMatchingByDayRRule($rrules['BYDAY'], $bymonthRecurringDatetime); + } else { + $monthDays[] = $bymonthRecurringDatetime->format('d'); + } + + // And add each of them to the list of recurrences + foreach ($monthDays as $day) { + $matchingDays[] = $bymonthRecurringDatetime->setDate( + (int) $frequencyRecurringDateTime->format('Y'), + (int) $bymonthRecurringDatetime->format('m'), + $day + )->format('z') + 1; + } + } + } elseif (isset($rrules['BYWEEKNO']) && (is_array($rrules['BYWEEKNO']) && $rrules['BYWEEKNO'] !== array())) { + $matchingDays = $this->getDaysOfYearMatchingByWeekNoRRule($rrules['BYWEEKNO'], $frequencyRecurringDateTime); + } elseif (isset($rrules['BYYEARDAY']) && (is_array($rrules['BYYEARDAY']) && $rrules['BYYEARDAY'] !== array())) { + $matchingDays = $this->getDaysOfYearMatchingByYearDayRRule($rrules['BYYEARDAY'], $frequencyRecurringDateTime); + } elseif (isset($rrules['BYMONTHDAY']) && (is_array($rrules['BYMONTHDAY']) && $rrules['BYMONTHDAY'] !== array())) { + $matchingDays = $this->getDaysOfYearMatchingByMonthDayRRule($rrules['BYMONTHDAY'], $frequencyRecurringDateTime); + } + + if (isset($rrules['BYDAY']) && (is_array($rrules['BYDAY']) && $rrules['BYDAY'] !== array())) { + if (isset($rrules['BYYEARDAY']) && ($rrules['BYYEARDAY'] !== '' && $rrules['BYYEARDAY'] !== array()) || isset($rrules['BYMONTHDAY']) && ($rrules['BYMONTHDAY'] !== '' && $rrules['BYMONTHDAY'] !== array()) || isset($rrules['BYWEEKNO']) && ($rrules['BYWEEKNO'] !== '' && $rrules['BYWEEKNO'] !== array())) { + $matchingDays = array_filter( + $this->getDaysOfYearMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime), + function ($yearDay) use ($matchingDays) { + return in_array($yearDay, $matchingDays); + } + ); + } elseif ($matchingDays === array()) { + $matchingDays = $this->getDaysOfYearMatchingByDayRRule($rrules['BYDAY'], $frequencyRecurringDateTime); + } + } + + if ($matchingDays === array()) { + $matchingDays = array($frequencyRecurringDateTime->format('z') + 1); + } else { + sort($matchingDays); + } + + if (isset($rrules['BYSETPOS']) && (is_array($rrules['BYSETPOS']) && $rrules['BYSETPOS'] !== array())) { + $matchingDays = $this->filterValuesUsingBySetPosRRule($rrules['BYSETPOS'], $matchingDays); + } + + foreach ($matchingDays as $day) { + $clonedDateTime = clone $frequencyRecurringDateTime; + $candidateDateTimes[] = $clonedDateTime->setDate( + (int) $frequencyRecurringDateTime->format('Y'), + 1, + $day + ); + } + break; + } + + foreach ($candidateDateTimes as $candidate) { + $timestamp = $candidate->getTimestamp(); + if ($timestamp <= $initialEventDate->getTimestamp()) { + continue; + } + + if ($timestamp > $until) { + break; + } + + // Exclusions + $isExcluded = array_filter($exdates, function ($exdate) use ($timestamp) { + return $exdate->getTimestamp() === $timestamp; + }); + + if (isset($this->alteredRecurrenceInstances[$anEvent['UID']])) { + if (in_array($timestamp, $this->alteredRecurrenceInstances[$anEvent['UID']])) { + $isExcluded = true; + } + } + + if (!$isExcluded) { + $eventRecurrences[] = $candidate; + $this->eventCount++; + } + + // Count all evaluated candidates including excluded ones, + // and if RRULE[COUNT] (if set) is reached then break. + $count++; + if ($count >= $countLimit) { + break 2; + } + } + + // Move forwards $interval $frequency. + $monthPreMove = $frequencyRecurringDateTime->format('m'); + $frequencyRecurringDateTime->modify("{$interval} {$this->frequencyConversion[$frequency]}"); + + // As noted in Example #2 on https://www.php.net/manual/en/datetime.modify.php, + // there are some occasions where adding months doesn't give the month you might + // expect. For instance: January 31st + 1 month == March 3rd (March 2nd on a leap + // year.) The following code crudely rectifies this. + if ($frequency === 'MONTHLY') { + $monthDiff = $frequencyRecurringDateTime->format('m') - $monthPreMove; + + if (($monthDiff > 0 && $monthDiff > $interval) || ($monthDiff < 0 && $monthDiff > $interval - 12)) { + $frequencyRecurringDateTime->modify('-1 month'); + } + } + + // $monthDays is set in the DAILY frequency if the BYMONTHDAY stanza is present in + // the RRULE. The variable only needs to be updated when we change months, so we + // unset it here, prompting a recreation next iteration. + if (isset($monthDays) && $frequencyRecurringDateTime->format('m') !== $monthPreMove) { + unset($monthDays); + } + } + + unset($monthDays); // Unset it here as well, so it doesn't bleed into the calculation of the next recurring event. + + // Determine event length + $eventLength = 0; + if (isset($anEvent['DURATION'])) { + $clonedDateTime = clone $initialEventDate; + $endDate = $clonedDateTime->add($anEvent['DURATION_array'][2]); + $eventLength = $endDate->getTimestamp() - $anEvent['DTSTART_array'][2]; + } elseif (isset($anEvent['DTEND_array'])) { + $eventLength = $anEvent['DTEND_array'][2] - $anEvent['DTSTART_array'][2]; + } + + // Whether or not the initial date was UTC + $initialDateWasUTC = substr($anEvent['DTSTART'], -1) === 'Z'; + + // Build the param array + $dateParamArray = array(); + if ( + !$initialDateWasUTC + && isset($anEvent['DTSTART_array'][0]['TZID']) + && $this->isValidTimeZoneId($anEvent['DTSTART_array'][0]['TZID']) + ) { + $dateParamArray['TZID'] = $anEvent['DTSTART_array'][0]['TZID']; + } + + // Populate the `DT{START|END}[_array]`s + $eventRecurrences = array_map( + function ($recurringDatetime) use ($anEvent, $eventLength, $initialDateWasUTC, $dateParamArray) { + $tzidPrefix = (isset($dateParamArray['TZID'])) ? 'TZID=' . $this->escapeParamText($dateParamArray['TZID']) . ':' : ''; + + foreach (array('DTSTART', 'DTEND') as $dtkey) { + $anEvent[$dtkey] = $recurringDatetime->format(self::DATE_TIME_FORMAT) . (($initialDateWasUTC) ? 'Z' : ''); + + $anEvent["{$dtkey}_array"] = array( + $dateParamArray, // [0] Array of params (incl. TZID) + $anEvent[$dtkey], // [1] ICalDateTime string w/o TZID + $recurringDatetime->getTimestamp(), // [2] Unix Timestamp + "{$tzidPrefix}{$anEvent[$dtkey]}", // [3] Full ICalDateTime string + ); + + if ($dtkey !== 'DTEND') { + $recurringDatetime->modify("{$eventLength} seconds"); + } + } + + return $anEvent; + }, + $eventRecurrences + ); + + $allEventRecurrences = array_merge($allEventRecurrences, $eventRecurrences); + } + + // Nullify the initial events that are also EXDATEs + foreach ($eventKeysToRemove as $eventKeyToRemove) { + $events[$eventKeyToRemove] = null; + } + + $events = array_merge($events, $allEventRecurrences); + + $this->cal['VEVENT'] = $events; + } + + /** + * Resolves values from indices of the range 1 -> $limit. + * + * For instance, if passed [1, 4, -16] and 28, this will return [1, 4, 13]. + * + * @param array $indexes + * @param integer $limit + * @return array + */ + protected function resolveIndicesOfRange(array $indexes, $limit) + { + $matching = array(); + foreach ($indexes as $index) { + if ($index > 0 && $index <= $limit) { + $matching[] = $index; + } elseif ($index < 0 && -$index <= $limit) { + $matching[] = $index + $limit + 1; + } + } + + sort($matching); + + return $matching; + } + + /** + * Find all days of a month that match the BYDAY stanza of an RRULE. + * + * With no {ordwk}, then return the day number of every {weekday} + * within the month. + * + * With a +ve {ordwk}, then return the {ordwk} {weekday} within the + * month. + * + * With a -ve {ordwk}, then return the {ordwk}-to-last {weekday} + * within the month. + * + * RRule Syntax: + * BYDAY={bywdaylist} + * + * Where: + * bywdaylist = {weekdaynum}[,{weekdaynum}...] + * weekdaynum = [[+]{ordwk} || -{ordwk}]{weekday} + * ordwk = 1 to 53 + * weekday = SU || MO || TU || WE || TH || FR || SA + * + * @param array $byDays + * @param \DateTime $initialDateTime + * @return array + */ + protected function getDaysOfMonthMatchingByDayRRule(array $byDays, $initialDateTime) + { + $matchingDays = array(); + $currentMonth = $initialDateTime->format('n'); + + foreach ($byDays as $weekday) { + $bydayDateTime = clone $initialDateTime; + + $ordwk = intval(substr($weekday, 0, -2)); + + // Quantise the date to the first instance of the requested day in a month + // (Or last if we have a -ve {ordwk}) + $bydayDateTime->modify( + (($ordwk < 0) ? 'Last' : 'First') . + ' ' . + $this->weekdays[substr($weekday, -2)] . // e.g. "Monday" + ' of ' . + $initialDateTime->format('F') // e.g. "June" + ); + + if ($ordwk < 0) { // -ve {ordwk} + $bydayDateTime->modify((++$ordwk) . ' week'); + if ($bydayDateTime->format('n') === $currentMonth) { + $matchingDays[] = $bydayDateTime->format('j'); + } + } elseif ($ordwk > 0) { // +ve {ordwk} + $bydayDateTime->modify((--$ordwk) . ' week'); + if ($bydayDateTime->format('n') === $currentMonth) { + $matchingDays[] = $bydayDateTime->format('j'); + } + } else { // No {ordwk} + while ($bydayDateTime->format('n') === $initialDateTime->format('n')) { + $matchingDays[] = $bydayDateTime->format('j'); + $bydayDateTime->modify('+1 week'); + } + } + } + + // Sort into ascending order + sort($matchingDays); + + return $matchingDays; + } + + /** + * Find all days of a month that match the BYMONTHDAY stanza of an RRULE. + * + * RRUle Syntax: + * BYMONTHDAY={bymodaylist} + * + * Where: + * bymodaylist = {monthdaynum}[,{monthdaynum}...] + * monthdaynum = ([+] || -) {ordmoday} + * ordmoday = 1 to 31 + * + * @param array $byMonthDays + * @param \DateTime $initialDateTime + * @return array + */ + protected function getDaysOfMonthMatchingByMonthDayRRule(array $byMonthDays, $initialDateTime) + { + return $this->resolveIndicesOfRange($byMonthDays, (int) $initialDateTime->format('t')); + } + + /** + * Find all days of a year that match the BYDAY stanza of an RRULE. + * + * With no {ordwk}, then return the day number of every {weekday} + * within the year. + * + * With a +ve {ordwk}, then return the {ordwk} {weekday} within the + * year. + * + * With a -ve {ordwk}, then return the {ordwk}-to-last {weekday} + * within the year. + * + * RRule Syntax: + * BYDAY={bywdaylist} + * + * Where: + * bywdaylist = {weekdaynum}[,{weekdaynum}...] + * weekdaynum = [[+]{ordwk} || -{ordwk}]{weekday} + * ordwk = 1 to 53 + * weekday = SU || MO || TU || WE || TH || FR || SA + * + * @param array $byDays + * @param \DateTime $initialDateTime + * @return array + */ + protected function getDaysOfYearMatchingByDayRRule(array $byDays, $initialDateTime) + { + $matchingDays = array(); + + foreach ($byDays as $weekday) { + $bydayDateTime = clone $initialDateTime; + + $ordwk = intval(substr($weekday, 0, -2)); + + // Quantise the date to the first instance of the requested day in a year + // (Or last if we have a -ve {ordwk}) + $bydayDateTime->modify( + (($ordwk < 0) ? 'Last' : 'First') . + ' ' . + $this->weekdays[substr($weekday, -2)] . // e.g. "Monday" + ' of ' . (($ordwk < 0) ? 'December' : 'January') . + ' ' . $initialDateTime->format('Y') // e.g. "2018" + ); + + if ($ordwk < 0) { // -ve {ordwk} + $bydayDateTime->modify((++$ordwk) . ' week'); + $matchingDays[] = $bydayDateTime->format('z') + 1; + } elseif ($ordwk > 0) { // +ve {ordwk} + $bydayDateTime->modify((--$ordwk) . ' week'); + $matchingDays[] = $bydayDateTime->format('z') + 1; + } else { // No {ordwk} + while ($bydayDateTime->format('Y') === $initialDateTime->format('Y')) { + $matchingDays[] = $bydayDateTime->format('z') + 1; + $bydayDateTime->modify('+1 week'); + } + } + } + + // Sort into ascending order + sort($matchingDays); + + return $matchingDays; + } + + /** + * Find all days of a year that match the BYYEARDAY stanza of an RRULE. + * + * RRUle Syntax: + * BYYEARDAY={byyrdaylist} + * + * Where: + * byyrdaylist = {yeardaynum}[,{yeardaynum}...] + * yeardaynum = ([+] || -) {ordyrday} + * ordyrday = 1 to 366 + * + * @param array $byYearDays + * @param \DateTime $initialDateTime + * @return array + */ + protected function getDaysOfYearMatchingByYearDayRRule(array $byYearDays, $initialDateTime) + { + // `\DateTime::format('L')` returns 1 if leap year, 0 if not. + $daysInThisYear = $initialDateTime->format('L') ? 366 : 365; + + return $this->resolveIndicesOfRange($byYearDays, $daysInThisYear); + } + + /** + * Find all days of a year that match the BYWEEKNO stanza of an RRULE. + * + * Unfortunately, the RFC5545 specification does not specify exactly + * how BYWEEKNO should expand on the initial DTSTART when provided + * without any other stanzas. + * + * A comparison of expansions used by other ics parsers may be found + * at https://github.com/s0600204/ics-parser-1/wiki/byweekno + * + * This method uses the same expansion as the python-dateutil module. + * + * RRUle Syntax: + * BYWEEKNO={bywknolist} + * + * Where: + * bywknolist = {weeknum}[,{weeknum}...] + * weeknum = ([+] || -) {ordwk} + * ordwk = 1 to 53 + * + * @param array $byWeekNums + * @param \DateTime $initialDateTime + * @return array + */ + protected function getDaysOfYearMatchingByWeekNoRRule(array $byWeekNums, $initialDateTime) + { + // `\DateTime::format('L')` returns 1 if leap year, 0 if not. + $isLeapYear = $initialDateTime->format('L'); + $initialYear = date_create("first day of January {$initialDateTime->format('Y')}"); + $firstDayOfTheYear = ($initialYear === false) ? null : $initialYear->format('D'); + $weeksInThisYear = ($firstDayOfTheYear === 'Thu' || $isLeapYear && $firstDayOfTheYear === 'Wed') ? 53 : 52; + + $matchingWeeks = $this->resolveIndicesOfRange($byWeekNums, $weeksInThisYear); + $matchingDays = array(); + $byweekDateTime = clone $initialDateTime; + foreach ($matchingWeeks as $weekNum) { + $dayNum = $byweekDateTime->setISODate( + (int) $initialDateTime->format('Y'), + $weekNum, + 1 + )->format('z') + 1; + for ($x = 0; $x < 7; ++$x) { + $matchingDays[] = $x + $dayNum; + } + } + + sort($matchingDays); + + return $matchingDays; + } + + /** + * Find all days of a year that match the BYMONTHDAY stanza of an RRULE. + * + * RRule Syntax: + * BYMONTHDAY={bymodaylist} + * + * Where: + * bymodaylist = {monthdaynum}[,{monthdaynum}...] + * monthdaynum = ([+] || -) {ordmoday} + * ordmoday = 1 to 31 + * + * @param array $byMonthDays + * @param \DateTime $initialDateTime + * @return array + */ + protected function getDaysOfYearMatchingByMonthDayRRule(array $byMonthDays, $initialDateTime) + { + $matchingDays = array(); + $monthDateTime = clone $initialDateTime; + for ($month = 1; $month < 13; $month++) { + $monthDateTime->setDate( + (int) $initialDateTime->format('Y'), + $month, + 1 + ); + + $monthDays = $this->getDaysOfMonthMatchingByMonthDayRRule($byMonthDays, $monthDateTime); + foreach ($monthDays as $day) { + $matchingDays[] = $monthDateTime->setDate( + (int) $initialDateTime->format('Y'), + (int) $monthDateTime->format('m'), + $day + )->format('z') + 1; + } + } + + return $matchingDays; + } + + /** + * Filters a provided values-list by applying a BYSETPOS RRule. + * + * Where a +ve {daynum} is provided, the {ordday} position'd value as + * measured from the start of the list of values should be retained. + * + * Where a -ve {daynum} is provided, the {ordday} position'd value as + * measured from the end of the list of values should be retained. + * + * RRule Syntax: + * BYSETPOS={bysplist} + * + * Where: + * bysplist = {setposday}[,{setposday}...] + * setposday = {daynum} + * daynum = [+ || -] {ordday} + * ordday = 1 to 366 + * + * @param array $bySetPos + * @param array $valuesList + * @return array + */ + protected function filterValuesUsingBySetPosRRule(array $bySetPos, array $valuesList) + { + $filteredMatches = array(); + + foreach ($bySetPos as $setPosition) { + if ($setPosition < 0) { + $setPosition = count($valuesList) + ++$setPosition; + } + + // Positioning starts at 1, array indexes start at 0 + if (isset($valuesList[$setPosition - 1])) { + $filteredMatches[] = $valuesList[$setPosition - 1]; + } + } + + return $filteredMatches; + } + + /** + * Processes date conversions using the time zone + * + * Add keys `DTSTART_tz` and `DTEND_tz` to each Event + * These keys contain dates adapted to the calendar + * time zone depending on the event `TZID`. + * + * @return void + * @throws \Exception + */ + protected function processDateConversions() + { + $events = (isset($this->cal['VEVENT'])) ? $this->cal['VEVENT'] : array(); + + if ($events !== array()) { + foreach ($events as $key => $anEvent) { + if (is_null($anEvent) || !$this->isValidDate($anEvent['DTSTART'])) { + unset($events[$key]); + $this->eventCount--; + + continue; + } + + $events[$key]['DTSTART_tz'] = $this->iCalDateWithTimeZone($anEvent, 'DTSTART'); + + if ($this->iCalDateWithTimeZone($anEvent, 'DTEND')) { + $events[$key]['DTEND_tz'] = $this->iCalDateWithTimeZone($anEvent, 'DTEND'); + } elseif ($this->iCalDateWithTimeZone($anEvent, 'DURATION')) { + $events[$key]['DTEND_tz'] = $this->iCalDateWithTimeZone($anEvent, 'DURATION'); + } else { + $events[$key]['DTEND_tz'] = $events[$key]['DTSTART_tz']; + } + } + + $this->cal['VEVENT'] = $events; + } + } + + /** + * Returns an array of Events. + * Every event is a class with the event + * details being properties within it. + * + * @return array + */ + public function events() + { + $array = $this->cal; + $array = isset($array['VEVENT']) ? $array['VEVENT'] : array(); + + $events = array(); + + foreach ($array as $event) { + $events[] = new Event($event); + } + + return $events; + } + + /** + * Returns the calendar name + * + * @return string + */ + public function calendarName() + { + return isset($this->cal['VCALENDAR']['X-WR-CALNAME']) ? $this->cal['VCALENDAR']['X-WR-CALNAME'] : ''; + } + + /** + * Returns the calendar description + * + * @return string + */ + public function calendarDescription() + { + return isset($this->cal['VCALENDAR']['X-WR-CALDESC']) ? $this->cal['VCALENDAR']['X-WR-CALDESC'] : ''; + } + + /** + * Returns the calendar time zone + * + * @param boolean $ignoreUtc + * @return string|null + */ + public function calendarTimeZone($ignoreUtc = false) + { + if (isset($this->cal['VCALENDAR']['X-WR-TIMEZONE'])) { + $timeZone = $this->cal['VCALENDAR']['X-WR-TIMEZONE']; + } elseif (isset($this->cal['VTIMEZONE']['TZID'])) { + $timeZone = $this->cal['VTIMEZONE']['TZID']; + } else { + $timeZone = $this->defaultTimeZone; + } + + // Validate the time zone, falling back to the time zone set in the PHP environment. + $timeZone = $this->timeZoneStringToDateTimeZone($timeZone)->getName(); + + if ($ignoreUtc && strtoupper($timeZone) === self::TIME_ZONE_UTC) { + return null; + } + + return $timeZone; + } + + /** + * Returns an array of arrays with all free/busy events. + * Every event is an associative array and each property + * is an element it. + * + * @return array + */ + public function freeBusyEvents() + { + $array = $this->cal; + + return isset($array['VFREEBUSY']) ? $array['VFREEBUSY'] : array(); + } + + /** + * Returns a boolean value whether the + * current calendar has events or not + * + * @return boolean + */ + public function hasEvents() + { + return ($this->events() !== array()) ?: false; + } + + /** + * Returns a sorted array of the events in a given range, + * or an empty array if no events exist in the range. + * + * Events will be returned if the start or end date is contained within the + * range (inclusive), or if the event starts before and end after the range. + * + * If a start date is not specified or of a valid format, then the start + * of the range will default to the current time and date of the server. + * + * If an end date is not specified or of a valid format, then the end of + * the range will default to the current time and date of the server, + * plus 20 years. + * + * Note that this function makes use of Unix timestamps. This might be a + * problem for events on, during, or after 29 Jan 2038. + * See https://en.wikipedia.org/wiki/Unix_time#Representing_the_number + * + * @param string|null $rangeStart + * @param string|null $rangeEnd + * @return array + * @throws \Exception + */ + public function eventsFromRange($rangeStart = null, $rangeEnd = null) + { + // Sort events before processing range + $events = $this->sortEventsWithOrder($this->events()); + + if ($events === array()) { + return array(); + } + + $extendedEvents = array(); + + if (!is_null($rangeStart)) { + try { + $rangeStart = new \DateTime($rangeStart, new \DateTimeZone($this->getDefaultTimeZone())); + } catch (\Exception $exception) { + error_log("ICal::eventsFromRange: Invalid date passed ({$rangeStart})"); + $rangeStart = false; + } + } else { + $rangeStart = new \DateTime('now', new \DateTimeZone($this->getDefaultTimeZone())); + } + + if (!is_null($rangeEnd)) { + try { + $rangeEnd = new \DateTime($rangeEnd, new \DateTimeZone($this->getDefaultTimeZone())); + } catch (\Exception $exception) { + error_log("ICal::eventsFromRange: Invalid date passed ({$rangeEnd})"); + $rangeEnd = false; + } + } else { + $rangeEnd = new \DateTime('now', new \DateTimeZone($this->getDefaultTimeZone())); + $rangeEnd->modify('+20 years'); + } + + if ($rangeEnd !== false && $rangeStart !== false) { + // If start and end are identical and are dates with no times... + if ($rangeEnd->format('His') == 0 && $rangeStart->getTimestamp() === $rangeEnd->getTimestamp()) { + $rangeEnd->modify('+1 day'); + } + + $rangeStart = $rangeStart->getTimestamp(); + $rangeEnd = $rangeEnd->getTimestamp(); + } + + foreach ($events as $anEvent) { + $eventStart = $anEvent->dtstart_array[2]; + $eventEnd = (isset($anEvent->dtend_array[2])) ? $anEvent->dtend_array[2] : null; + + if ( + ($eventStart >= $rangeStart && $eventStart < $rangeEnd) // Event start date contained in the range + || ( + $eventEnd !== null + && ( + ($eventEnd > $rangeStart && $eventEnd <= $rangeEnd) // Event end date contained in the range + || ($eventStart < $rangeStart && $eventEnd > $rangeEnd) // Event starts before and finishes after range + ) + ) + ) { + $extendedEvents[] = $anEvent; + } + } + + return $extendedEvents; + } + + /** + * Returns a sorted array of the events following a given string + * + * @param string $interval + * @return array + */ + public function eventsFromInterval($interval) + { + $timeZone = $this->getDefaultTimeZone(); + $rangeStart = new \DateTime('now', new \DateTimeZone($timeZone)); + $rangeEnd = new \DateTime('now', new \DateTimeZone($timeZone)); + + $dateInterval = \DateInterval::createFromDateString($interval); + + if ($dateInterval instanceof \DateInterval) { + $rangeEnd->add($dateInterval); + } + + return $this->eventsFromRange($rangeStart->format('Y-m-d'), $rangeEnd->format('Y-m-d')); + } + + /** + * Sorts events based on a given sort order + * + * @param array $events + * @param integer $sortOrder Either SORT_ASC, SORT_DESC, SORT_REGULAR, SORT_NUMERIC, SORT_STRING + * @return array + */ + public function sortEventsWithOrder(array $events, $sortOrder = SORT_ASC) + { + $extendedEvents = array(); + $timestamp = array(); + + foreach ($events as $key => $anEvent) { + $extendedEvents[] = $anEvent; + $timestamp[$key] = $anEvent->dtstart_array[2]; + } + + array_multisort($timestamp, $sortOrder, $extendedEvents); + + return $extendedEvents; + } + + /** + * Checks if a time zone is valid (IANA, CLDR, or Windows) + * + * @param string $timeZone + * @return boolean + */ + protected function isValidTimeZoneId($timeZone) + { + return $this->isValidIanaTimeZoneId($timeZone) !== false + || $this->isValidCldrTimeZoneId($timeZone) !== false + || $this->isValidWindowsTimeZoneId($timeZone) !== false; + } + + /** + * Checks if a time zone is a valid IANA time zone + * + * @param string $timeZone + * @return boolean + */ + protected function isValidIanaTimeZoneId($timeZone) + { + if (in_array($timeZone, $this->validIanaTimeZones)) { + return true; + } + + $valid = array(); + $tza = timezone_abbreviations_list(); + + foreach ($tza as $zone) { + foreach ($zone as $item) { + $valid[$item['timezone_id']] = true; + } + } + + unset($valid['']); + + if (isset($valid[$timeZone]) || in_array($timeZone, timezone_identifiers_list(\DateTimeZone::ALL_WITH_BC))) { + $this->validIanaTimeZones[] = $timeZone; + + return true; + } + + return false; + } + + /** + * Checks if a time zone is a valid CLDR time zone + * + * @param string $timeZone + * @return boolean + */ + public function isValidCldrTimeZoneId($timeZone) + { + return array_key_exists(html_entity_decode($timeZone), self::$cldrTimeZonesMap); + } + + /** + * Checks if a time zone is a recognised Windows (non-CLDR) time zone + * + * @param string $timeZone + * @return boolean + */ + public function isValidWindowsTimeZoneId($timeZone) + { + return array_key_exists(html_entity_decode($timeZone), self::$windowsTimeZonesMap); + } + + /** + * Parses a duration and applies it to a date + * + * @param string $date + * @param \DateInterval $duration + * @return \DateTime|false + */ + protected function parseDuration($date, $duration) + { + $dateTime = date_create($date); + + if ($dateTime === false) { + return false; + } + + $dateTime->modify("{$duration->y} year"); + $dateTime->modify("{$duration->m} month"); + $dateTime->modify("{$duration->d} day"); + $dateTime->modify("{$duration->h} hour"); + $dateTime->modify("{$duration->i} minute"); + $dateTime->modify("{$duration->s} second"); + + return $dateTime; + } + + /** + * Removes unprintable ASCII and UTF-8 characters + * + * @param string $data + * @return string|null + */ + protected function removeUnprintableChars($data) + { + return preg_replace('/[\x00-\x1F\x7F]/u', '', $data); + } + + /** + * Provides a polyfill for PHP 7.2's `mb_chr()`, which is a multibyte safe version of `chr()`. + * Multibyte safe. + * + * @param integer $code + * @return string + */ + protected function mb_chr($code) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps + { + if (function_exists('mb_chr')) { + return mb_chr($code); + } else { + if (($code %= 0x200000) < 0x80) { + $s = chr($code); + } elseif ($code < 0x800) { + $s = chr(0xc0 | $code >> 6) . chr(0x80 | $code & 0x3f); + } elseif ($code < 0x10000) { + $s = chr(0xe0 | $code >> 12) . chr(0x80 | $code >> 6 & 0x3f) . chr(0x80 | $code & 0x3f); + } else { + $s = chr(0xf0 | $code >> 18) . chr(0x80 | $code >> 12 & 0x3f) . chr(0x80 | $code >> 6 & 0x3f) . chr(0x80 | $code & 0x3f); + } + + return $s; + } + } + + /** + * Places double-quotes around texts that have characters not permitted + * in parameter-texts, but are permitted in quoted-texts. + * + * @param string $candidateText + * @return string + */ + protected function escapeParamText($candidateText) + { + if (strpbrk($candidateText, ':;,') !== false) { + return '"' . $candidateText . '"'; + } + + return $candidateText; + } + + /** + * Replace curly quotes and other special characters with their standard equivalents + * @see https://utf8-chartable.de/unicode-utf8-table.pl?start=8211&utf8=string-literal + * + * @param string $input + * @return string + */ + protected function cleanCharacters($input) + { + return strtr( + $input, + array( + "\xe2\x80\x98" => "'", // ‘ + "\xe2\x80\x99" => "'", // ’ + "\xe2\x80\x9a" => "'", // ‚ + "\xe2\x80\x9b" => "'", // ‛ + "\xe2\x80\x9c" => '"', // “ + "\xe2\x80\x9d" => '"', // ” + "\xe2\x80\x9e" => '"', // „ + "\xe2\x80\x9f" => '"', // ‟ + "\xe2\x80\x93" => '-', // – + "\xe2\x80\x94" => '--', // — + "\xe2\x80\xa6" => '...', // … + $this->mb_chr(145) => "'", // ‘ + $this->mb_chr(146) => "'", // ’ + $this->mb_chr(147) => '"', // “ + $this->mb_chr(148) => '"', // ” + $this->mb_chr(150) => '-', // – + $this->mb_chr(151) => '--', // — + $this->mb_chr(133) => '...', // … + ) + ); + } + + /** + * Parses a list of excluded dates + * to be applied to an Event + * + * @param array $event + * @return array + */ + public function parseExdates(array $event) + { + if (empty($event['EXDATE_array'])) { + return array(); + } else { + $exdates = $event['EXDATE_array']; + } + + $output = array(); + $currentTimeZone = new \DateTimeZone($this->getDefaultTimeZone()); + + foreach ($exdates as $subArray) { + end($subArray); + $finalKey = key($subArray); + + foreach (array_keys($subArray) as $key) { + if ($key === 'TZID') { + $currentTimeZone = $this->timeZoneStringToDateTimeZone($subArray[$key]); + } elseif (is_numeric($key)) { + $icalDate = $subArray[$key]; + + if (substr($icalDate, -1) === 'Z') { + $currentTimeZone = new \DateTimeZone(self::TIME_ZONE_UTC); + } + + $output[] = new \DateTime($icalDate, $currentTimeZone); + + if ($key === $finalKey) { + // Reset to default + $currentTimeZone = new \DateTimeZone($this->getDefaultTimeZone()); + } + } + } + } + + return $output; + } + + /** + * Checks if a date string is a valid date + * + * @param string $value + * @return boolean + * @throws \Exception + */ + public function isValidDate($value) + { + if (!$value) { + return false; + } + + try { + new \DateTime($value); + + return true; + } catch (\Exception $exception) { + return false; + } + } + + /** + * Checks if a filename exists as a file or URL + * + * @param string $filename + * @return boolean + */ + protected function isFileOrUrl($filename) + { + return (file_exists($filename) || filter_var($filename, FILTER_VALIDATE_URL)) ?: false; + } + + /** + * Reads an entire file or URL into an array + * + * @param string $filename + * @return array + * @throws \Exception + */ + protected function fileOrUrl($filename) + { + $options = array(); + $options['http'] = array(); + $options['http']['header'] = array(); + + if ($this->httpBasicAuth !== array() || !empty($this->httpUserAgent) || !empty($this->httpAcceptLanguage)) { + if ($this->httpBasicAuth !== array()) { + $username = $this->httpBasicAuth['username']; + $password = $this->httpBasicAuth['password']; + $basicAuth = base64_encode("{$username}:{$password}"); + + $options['http']['header'][] = "Authorization: Basic {$basicAuth}"; + } + + if (!empty($this->httpUserAgent)) { + $options['http']['header'][] = "User-Agent: {$this->httpUserAgent}"; + } + + if (!empty($this->httpAcceptLanguage)) { + $options['http']['header'][] = "Accept-language: {$this->httpAcceptLanguage}"; + } + } + + if (empty($this->httpUserAgent)) { + if (mb_stripos($filename, 'outlook.office365.com') !== false) { + $options['http']['header'][] = 'User-Agent: A User Agent'; + } + } + + if (!empty($this->httpProtocolVersion)) { + $options['http']['protocol_version'] = $this->httpProtocolVersion; + } else { + $options['http']['protocol_version'] = '1.1'; + } + + $options['http']['header'][] = 'Connection: close'; + + $context = stream_context_create($options); + + // phpcs:ignore CustomPHPCS.ControlStructures.AssignmentInCondition + if (($lines = file($filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES, $context)) === false) { + throw new \Exception("The file path or URL '{$filename}' does not exist."); + } + + return $lines; + } + + /** + * Returns a `DateTimeZone` object based on a string containing a time zone name. + * Falls back to the default time zone if string passed not a recognised time zone. + * + * @param string $timeZoneString + * @return \DateTimeZone + */ + public function timeZoneStringToDateTimeZone($timeZoneString) + { + // Some time zones contain characters that are not permitted in param-texts, + // but are within quoted texts. We need to remove the quotes as they're not + // actually part of the time zone. + $timeZoneString = trim($timeZoneString, '"'); + $timeZoneString = html_entity_decode($timeZoneString); + + if ($this->isValidIanaTimeZoneId($timeZoneString)) { + return new \DateTimeZone($timeZoneString); + } + + if ($this->isValidCldrTimeZoneId($timeZoneString)) { + return new \DateTimeZone(self::$cldrTimeZonesMap[$timeZoneString]); + } + + if ($this->isValidWindowsTimeZoneId($timeZoneString)) { + return new \DateTimeZone(self::$windowsTimeZonesMap[$timeZoneString]); + } + + return new \DateTimeZone($this->getDefaultTimeZone()); + } +} diff --git a/lib/composer/vendor/johngrogg/ics-parser/tests/CleanCharacterTest.php b/lib/composer/vendor/johngrogg/ics-parser/tests/CleanCharacterTest.php new file mode 100644 index 0000000..31ee582 --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/tests/CleanCharacterTest.php @@ -0,0 +1,53 @@ +getMethod($name); + + // < PHP 8.1.0 + $method->setAccessible(true); + + return $method; + } + + public function testCleanCharactersWithUnicodeCharacters() + { + $ical = new ICal(); + + self::assertSame( + '...', + self::getMethod('cleanCharacters')->invokeArgs($ical, array("\xe2\x80\xa6")) + ); + } + + public function testCleanCharactersWithEmojis() + { + $ical = new ICal(); + $input = 'Test with emoji 🔴👍🏻'; + + self::assertSame( + $input, + self::getMethod('cleanCharacters')->invokeArgs($ical, array($input)) + ); + } + + public function testCleanCharactersWithWindowsCharacters() + { + $ical = new ICal(); + $input = self::getMethod('mb_chr')->invokeArgs($ical, array(133)); + + self::assertSame( + '...', + self::getMethod('cleanCharacters')->invokeArgs($ical, array($input)) + ); + } +} diff --git a/lib/composer/vendor/johngrogg/ics-parser/tests/DynamicPropertiesTest.php b/lib/composer/vendor/johngrogg/ics-parser/tests/DynamicPropertiesTest.php new file mode 100644 index 0000000..c683642 --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/tests/DynamicPropertiesTest.php @@ -0,0 +1,24 @@ +events() as $event) { + $this->assertTrue(isset($event->dtstart_array)); + $this->assertTrue(isset($event->dtend_array)); + $this->assertTrue(isset($event->dtstamp_array)); + $this->assertTrue(isset($event->uid_array)); + $this->assertTrue(isset($event->created_array)); + $this->assertTrue(isset($event->last_modified_array)); + $this->assertTrue(isset($event->summary_array)); + } + } +} diff --git a/lib/composer/vendor/johngrogg/ics-parser/tests/KeyValueTest.php b/lib/composer/vendor/johngrogg/ics-parser/tests/KeyValueTest.php new file mode 100644 index 0000000..6bbbe6e --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/tests/KeyValueTest.php @@ -0,0 +1,86 @@ + 'ATTENDEE', + 1 => array( + 0 => 'mailto:julien@ag.com', + 1 => array( + 'PARTSTAT' => 'TENTATIVE', + 'CN' => 'ju: @ag.com = Ju ; ', + ), + ), + ); + + $this->assertLines( + 'ATTENDEE;PARTSTAT=TENTATIVE;CN="ju: @ag.com = Ju ; ":mailto:julien@ag.com', + $checks + ); + } + + public function testUtf8Characters() + { + $checks = array( + 0 => 'ATTENDEE', + 1 => array( + 0 => 'mailto:juëǯ@ag.com', + 1 => array( + 'PARTSTAT' => 'TENTATIVE', + 'CN' => 'juëǯĻ', + ), + ), + ); + + $this->assertLines( + 'ATTENDEE;PARTSTAT=TENTATIVE;CN=juëǯĻ:mailto:juëǯ@ag.com', + $checks + ); + + $checks = array( + 0 => 'SUMMARY', + 1 => ' I love emojis 😀😁😁 ë, ǯ, Ļ', + ); + + $this->assertLines( + 'SUMMARY: I love emojis 😀😁😁 ë, ǯ, Ļ', + $checks + ); + } + + public function testParametersOfKeysWithMultipleValues() + { + $checks = array( + 0 => 'ATTENDEE', + 1 => array( + 0 => 'mailto:jsmith@example.com', + 1 => array( + 'DELEGATED-TO' => array( + 0 => 'mailto:jdoe@example.com', + 1 => 'mailto:jqpublic@example.com', + ), + ), + ), + ); + + $this->assertLines( + 'ATTENDEE;DELEGATED-TO="mailto:jdoe@example.com","mailto:jqpublic@example.com":mailto:jsmith@example.com', + $checks + ); + } + + private function assertLines($lines, array $checks) + { + $ical = new ICal(); + + self::assertSame($ical->keyValueFromString($lines), $checks); + } +} diff --git a/lib/composer/vendor/johngrogg/ics-parser/tests/RecurrencesTest.php b/lib/composer/vendor/johngrogg/ics-parser/tests/RecurrencesTest.php new file mode 100644 index 0000000..add13eb --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/tests/RecurrencesTest.php @@ -0,0 +1,580 @@ +originalTimeZone = date_default_timezone_get(); + } + + /** + * @after + */ + public function tearDownFixtures() + { + date_default_timezone_set($this->originalTimeZone); + } + + public function testYearlyFullDayTimeZoneBerlin() + { + $checks = array( + array('index' => 0, 'dateString' => '20000301', 'message' => '1st event, CET: '), + array('index' => 1, 'dateString' => '20010301T000000', 'message' => '2nd event, CET: '), + array('index' => 2, 'dateString' => '20020301T000000', 'message' => '3rd event, CET: '), + ); + $this->assertVEVENT( + 'Europe/Berlin', + array( + 'DTSTART;VALUE=DATE:20000301', + 'DTEND;VALUE=DATE:20000302', + 'RRULE:FREQ=YEARLY;WKST=SU;COUNT=3', + ), + 3, + $checks + ); + } + + public function testMonthlyFullDayTimeZoneBerlin() + { + $checks = array( + array('index' => 0, 'dateString' => '20000301', 'message' => '1st event, CET: '), + array('index' => 1, 'dateString' => '20000401T000000', 'message' => '2nd event, CEST: '), + array('index' => 2, 'dateString' => '20000501T000000', 'message' => '3rd event, CEST: '), + ); + $this->assertVEVENT( + 'Europe/Berlin', + array( + 'DTSTART;VALUE=DATE:20000301', + 'DTEND;VALUE=DATE:20000302', + 'RRULE:FREQ=MONTHLY;BYMONTHDAY=1;WKST=SU;COUNT=3', + ), + 3, + $checks + ); + } + + public function testMonthlyFullDayTimeZoneBerlinSummerTime() + { + $checks = array( + array('index' => 0, 'dateString' => '20180701', 'message' => '1st event, CEST: '), + array('index' => 1, 'dateString' => '20180801T000000', 'message' => '2nd event, CEST: '), + array('index' => 2, 'dateString' => '20180901T000000', 'message' => '3rd event, CEST: '), + ); + $this->assertVEVENT( + 'Europe/Berlin', + array( + 'DTSTART;VALUE=DATE:20180701', + 'DTEND;VALUE=DATE:20180702', + 'RRULE:FREQ=MONTHLY;WKST=SU;COUNT=3', + ), + 3, + $checks + ); + } + + public function testMonthlyFullDayTimeZoneBerlinFromFile() + { + $checks = array( + array('index' => 0, 'dateString' => '20180701', 'message' => '1st event, CEST: '), + array('index' => 1, 'dateString' => '20180801T000000', 'message' => '2nd event, CEST: '), + array('index' => 2, 'dateString' => '20180901T000000', 'message' => '3rd event, CEST: '), + ); + $this->assertEventFile( + 'Europe/Berlin', + './tests/ical/ical-monthly.ics', + 25, + $checks + ); + } + + public function testIssue196FromFile() + { + $checks = array( + array('index' => 0, 'dateString' => '20191105T190000', 'timezone' => 'Europe/Berlin', 'message' => '1st event, CEST: '), + array('index' => 1, 'dateString' => '20191106T190000', 'timezone' => 'Europe/Berlin', 'message' => '2nd event, CEST: '), + array('index' => 2, 'dateString' => '20191107T190000', 'timezone' => 'Europe/Berlin', 'message' => '3rd event, CEST: '), + array('index' => 3, 'dateString' => '20191108T190000', 'timezone' => 'Europe/Berlin', 'message' => '4th event, CEST: '), + array('index' => 4, 'dateString' => '20191109T170000', 'timezone' => 'Europe/Berlin', 'message' => '5th event, CEST: '), + array('index' => 5, 'dateString' => '20191110T180000', 'timezone' => 'Europe/Berlin', 'message' => '6th event, CEST: '), + ); + $this->assertEventFile( + 'UTC', + './tests/ical/issue-196.ics', + 7, + $checks + ); + } + + public function testWeeklyFullDayTimeZoneBerlin() + { + $checks = array( + array('index' => 0, 'dateString' => '20000301', 'message' => '1st event, CET: '), + array('index' => 1, 'dateString' => '20000308T000000', 'message' => '2nd event, CET: '), + array('index' => 2, 'dateString' => '20000315T000000', 'message' => '3rd event, CET: '), + array('index' => 3, 'dateString' => '20000322T000000', 'message' => '4th event, CET: '), + array('index' => 4, 'dateString' => '20000329T000000', 'message' => '5th event, CEST: '), + array('index' => 5, 'dateString' => '20000405T000000', 'message' => '6th event, CEST: '), + ); + $this->assertVEVENT( + 'Europe/Berlin', + array( + 'DTSTART;VALUE=DATE:20000301', + 'DTEND;VALUE=DATE:20000302', + 'RRULE:FREQ=WEEKLY;WKST=SU;COUNT=6', + ), + 6, + $checks + ); + } + + public function testDailyFullDayTimeZoneBerlin() + { + $checks = array( + array('index' => 0, 'dateString' => '20000301', 'message' => '1st event, CET: '), + array('index' => 1, 'dateString' => '20000302T000000', 'message' => '2nd event, CET: '), + array('index' => 30, 'dateString' => '20000331T000000', 'message' => '31st event, CEST: '), + ); + $this->assertVEVENT( + 'Europe/Berlin', + array( + 'DTSTART;VALUE=DATE:20000301', + 'DTEND;VALUE=DATE:20000302', + 'RRULE:FREQ=DAILY;WKST=SU;COUNT=31', + ), + 31, + $checks + ); + } + + public function testWeeklyFullDayTimeZoneBerlinLocal() + { + $checks = array( + array('index' => 0, 'dateString' => '20000301T000000', 'message' => '1st event, CET: '), + array('index' => 1, 'dateString' => '20000308T000000', 'message' => '2nd event, CET: '), + array('index' => 2, 'dateString' => '20000315T000000', 'message' => '3rd event, CET: '), + array('index' => 3, 'dateString' => '20000322T000000', 'message' => '4th event, CET: '), + array('index' => 4, 'dateString' => '20000329T000000', 'message' => '5th event, CEST: '), + array('index' => 5, 'dateString' => '20000405T000000', 'message' => '6th event, CEST: '), + ); + $this->assertVEVENT( + 'Europe/Berlin', + array( + 'DTSTART;TZID=Europe/Berlin:20000301T000000', + 'DTEND;TZID=Europe/Berlin:20000302T000000', + 'RRULE:FREQ=WEEKLY;WKST=SU;COUNT=6', + ), + 6, + $checks + ); + } + + public function testRFCDaily10NewYork() + { + $checks = array( + array('index' => 0, 'dateString' => '19970902T090000', 'timezone' => 'America/New_York', 'message' => '1st event, EDT: '), + array('index' => 1, 'dateString' => '19970903T090000', 'timezone' => 'America/New_York', 'message' => '2nd event, EDT: '), + array('index' => 9, 'dateString' => '19970911T090000', 'timezone' => 'America/New_York', 'message' => '10th event, EDT: '), + ); + $this->assertVEVENT( + 'Europe/Berlin', + array( + 'DTSTART;TZID=America/New_York:19970902T090000', + 'RRULE:FREQ=DAILY;COUNT=10', + ), + 10, + $checks + ); + } + + public function testRFCDaily10Berlin() + { + $checks = array( + array('index' => 0, 'dateString' => '19970902T090000', 'timezone' => 'Europe/Berlin', 'message' => '1st event, CEST: '), + array('index' => 1, 'dateString' => '19970903T090000', 'timezone' => 'Europe/Berlin', 'message' => '2nd event, CEST: '), + array('index' => 9, 'dateString' => '19970911T090000', 'timezone' => 'Europe/Berlin', 'message' => '10th event, CEST: '), + ); + $this->assertVEVENT( + 'Europe/Berlin', + array( + 'DTSTART;TZID=Europe/Berlin:19970902T090000', + 'RRULE:FREQ=DAILY;COUNT=10', + ), + 10, + $checks + ); + } + + public function testStartDateIsExdateUsingUntil() + { + $checks = array( + array('index' => 0, 'dateString' => '20190918T095000', 'timezone' => 'Europe/London', 'message' => '1st event: '), + array('index' => 1, 'dateString' => '20191002T095000', 'timezone' => 'Europe/London', 'message' => '2nd event: '), + array('index' => 2, 'dateString' => '20191016T095000', 'timezone' => 'Europe/London', 'message' => '3rd event: '), + ); + $this->assertVEVENT( + 'Europe/London', + array( + 'DTSTART;TZID=Europe/London:20190911T095000', + 'RRULE:FREQ=WEEKLY;WKST=SU;UNTIL=20191027T235959Z;BYDAY=WE', + 'EXDATE;TZID=Europe/London:20191023T095000', + 'EXDATE;TZID=Europe/London:20191009T095000', + 'EXDATE;TZID=Europe/London:20190925T095000', + 'EXDATE;TZID=Europe/London:20190911T095000', + ), + 3, + $checks + ); + } + + public function testStartDateIsExdateUsingCount() + { + $checks = array( + array('index' => 0, 'dateString' => '20190918T095000', 'timezone' => 'Europe/London', 'message' => '1st event: '), + array('index' => 1, 'dateString' => '20191002T095000', 'timezone' => 'Europe/London', 'message' => '2nd event: '), + array('index' => 2, 'dateString' => '20191016T095000', 'timezone' => 'Europe/London', 'message' => '3rd event: '), + ); + $this->assertVEVENT( + 'Europe/London', + array( + 'DTSTART;TZID=Europe/London:20190911T095000', + 'RRULE:FREQ=WEEKLY;WKST=SU;COUNT=7;BYDAY=WE', + 'EXDATE;TZID=Europe/London:20191023T095000', + 'EXDATE;TZID=Europe/London:20191009T095000', + 'EXDATE;TZID=Europe/London:20190925T095000', + 'EXDATE;TZID=Europe/London:20190911T095000', + ), + 3, + $checks + ); + } + + public function testCountWithExdate() + { + $checks = array( + array('index' => 0, 'dateString' => '20200323T050000', 'timezone' => 'Europe/Paris', 'message' => '1st event: '), + array('index' => 1, 'dateString' => '20200324T050000', 'timezone' => 'Europe/Paris', 'message' => '2nd event: '), + array('index' => 2, 'dateString' => '20200327T050000', 'timezone' => 'Europe/Paris', 'message' => '3rd event: '), + ); + $this->assertVEVENT( + 'Europe/London', + array( + 'DTSTART;TZID=Europe/Paris:20200323T050000', + 'DTEND;TZID=Europe/Paris:20200323T070000', + 'RRULE:FREQ=DAILY;COUNT=5', + 'EXDATE;TZID=Europe/Paris:20200326T050000', + 'EXDATE;TZID=Europe/Paris:20200325T050000', + 'DTSTAMP:20200318T141057Z', + ), + 3, + $checks + ); + } + + public function testRFCDaily10BerlinFromNewYork() + { + $checks = array( + array('index' => 0, 'dateString' => '19970902T090000', 'timezone' => 'Europe/Berlin', 'message' => '1st event, CEST: '), + array('index' => 1, 'dateString' => '19970903T090000', 'timezone' => 'Europe/Berlin', 'message' => '2nd event, CEST: '), + array('index' => 9, 'dateString' => '19970911T090000', 'timezone' => 'Europe/Berlin', 'message' => '10th event, CEST: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=Europe/Berlin:19970902T090000', + 'RRULE:FREQ=DAILY;COUNT=10', + ), + 10, + $checks + ); + } + + public function testExdatesInDifferentTimezone() + { + $checks = array( + array('index' => 0, 'dateString' => '20170503T190000', 'message' => '1st event: '), + array('index' => 1, 'dateString' => '20170510T190000', 'message' => '2nd event: '), + array('index' => 9, 'dateString' => '20170712T190000', 'message' => '10th event: '), + array('index' => 19, 'dateString' => '20171004T190000', 'message' => '20th event: '), + ); + $this->assertVEVENT( + 'America/Chicago', + array( + 'DTSTART;TZID=America/Chicago:20170503T190000', + 'RRULE:FREQ=WEEKLY;BYDAY=WE;WKST=SU;UNTIL=20180101', + 'EXDATE:20170601T000000Z', + 'EXDATE:20170803T000000Z', + 'EXDATE:20170824T000000Z', + 'EXDATE:20171026T000000Z', + 'EXDATE:20171102T000000Z', + 'EXDATE:20171123T010000Z', + 'EXDATE:20171221T010000Z', + ), + 28, + $checks + ); + } + + public function testYearlyWithBySetPos() + { + $checks = array( + array('index' => 0, 'dateString' => '19970306T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19970313T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19970325T090000', 'message' => '3rd occurrence: '), + array('index' => 3, 'dateString' => '19980305T090000', 'message' => '4th occurrence: '), + array('index' => 4, 'dateString' => '19980312T090000', 'message' => '5th occurrence: '), + array('index' => 5, 'dateString' => '19980326T090000', 'message' => '6th occurrence: '), + array('index' => 9, 'dateString' => '20000307T090000', 'message' => '10th occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970306T090000', + 'RRULE:FREQ=YEARLY;COUNT=10;BYMONTH=3;BYDAY=TU,TH;BYSETPOS=2,4,-2', + ), + 10, + $checks + ); + } + + public function testDailyWithByMonthDay() + { + $checks = array( + array('index' => 0, 'dateString' => '20000206T120000', 'message' => '1st event: '), + array('index' => 1, 'dateString' => '20000211T120000', 'message' => '2nd event: '), + array('index' => 2, 'dateString' => '20000216T120000', 'message' => '3rd event: '), + array('index' => 4, 'dateString' => '20000226T120000', 'message' => '5th event, transition from February to March: '), + array('index' => 5, 'dateString' => '20000301T120000', 'message' => '6th event, transition to March from February: '), + array('index' => 11, 'dateString' => '20000331T120000', 'message' => '12th event, transition from March to April: '), + array('index' => 12, 'dateString' => '20000401T120000', 'message' => '13th event, transition to April from March: '), + ); + $this->assertVEVENT( + 'Europe/Berlin', + array( + 'DTSTART:20000206T120000', + 'DTEND:20000206T130000', + 'RRULE:FREQ=DAILY;BYMONTHDAY=1,6,11,16,21,26,31;COUNT=16', + ), + 16, + $checks + ); + } + + public function testYearlyWithByMonthDay() + { + $checks = array( + array('index' => 0, 'dateString' => '20001214T120000', 'message' => '1st event: '), + array('index' => 1, 'dateString' => '20001221T120000', 'message' => '2nd event: '), + array('index' => 2, 'dateString' => '20010107T120000', 'message' => '3rd event: '), + array('index' => 3, 'dateString' => '20010114T120000', 'message' => '4th event: '), + array('index' => 6, 'dateString' => '20010214T120000', 'message' => '7th event: '), + ); + $this->assertVEVENT( + 'Europe/Berlin', + array( + 'DTSTART:20001214T120000', + 'DTEND:20001214T130000', + 'RRULE:FREQ=YEARLY;BYMONTHDAY=7,14,21;COUNT=8', + ), + 8, + $checks + ); + } + + public function testYearlyWithByMonthDayAndByDay() + { + $checks = array( + array('index' => 0, 'dateString' => '20001214T120000', 'message' => '1st event: '), + array('index' => 1, 'dateString' => '20001221T120000', 'message' => '2nd event: '), + array('index' => 2, 'dateString' => '20010607T120000', 'message' => '3rd event: '), + array('index' => 3, 'dateString' => '20010614T120000', 'message' => '4th event: '), + array('index' => 6, 'dateString' => '20020214T120000', 'message' => '7th event: '), + ); + $this->assertVEVENT( + 'Europe/Berlin', + array( + 'DTSTART:20001214T120000', + 'DTEND:20001214T130000', + 'RRULE:FREQ=YEARLY;BYMONTHDAY=7,14,21;BYDAY=TH;COUNT=8', + ), + 8, + $checks + ); + } + + public function testYearlyWithByMonthAndByMonthDay() + { + $checks = array( + array('index' => 0, 'dateString' => '20001214T120000', 'message' => '1st event: '), + array('index' => 1, 'dateString' => '20001221T120000', 'message' => '2nd event: '), + array('index' => 2, 'dateString' => '20010607T120000', 'message' => '3rd event: '), + array('index' => 3, 'dateString' => '20010614T120000', 'message' => '4th event: '), + array('index' => 6, 'dateString' => '20011214T120000', 'message' => '7th event: '), + ); + $this->assertVEVENT( + 'Europe/Berlin', + array( + 'DTSTART:20001214T120000', + 'DTEND:20001214T130000', + 'RRULE:FREQ=YEARLY;BYMONTH=12,6;BYMONTHDAY=7,14,21;COUNT=8', + ), + 8, + $checks + ); + } + + public function testCountIsOne() + { + $checks = array( + array('index' => 0, 'dateString' => '20211201T090000', 'message' => '1st and only expected event: '), + ); + $this->assertVEVENT( + 'UTC', + array( + 'DTSTART:20211201T090000', + 'DTEND:20211201T100000', + 'RRULE:FREQ=DAILY;COUNT=1', + ), + 1, + $checks + ); + } + + public function test5thByDayOfMonth() + { + $checks = array( + array('index' => 0, 'dateString' => '20200103T090000', 'message' => '1st event: '), + array('index' => 1, 'dateString' => '20200129T090000', 'message' => '2nd event: '), + array('index' => 2, 'dateString' => '20200429T090000', 'message' => '3rd event: '), + array('index' => 3, 'dateString' => '20200501T090000', 'message' => '4th event: '), + array('index' => 4, 'dateString' => '20200703T090000', 'message' => '5th event: '), + array('index' => 5, 'dateString' => '20200729T090000', 'message' => '6th event: '), + array('index' => 6, 'dateString' => '20200930T090000', 'message' => '7th event: '), + array('index' => 7, 'dateString' => '20201002T090000', 'message' => '8th event: '), + array('index' => 8, 'dateString' => '20201230T090000', 'message' => '9th event: '), + array('index' => 9, 'dateString' => '20210101T090000', 'message' => '10th and last event: '), + ); + $this->assertVEVENT( + 'UTC', + array( + 'DTSTART:20200103T090000', + 'DTEND:20200103T100000', + 'RRULE:FREQ=MONTHLY;BYDAY=5WE,-5FR;UNTIL=20210102T090000', + ), + 10, + $checks + ); + } + + public function assertVEVENT($defaultTimezone, $veventParts, $count, $checks) + { + $options = $this->getOptions($defaultTimezone); + + $testIcal = implode(PHP_EOL, $this->getIcalHeader()); + $testIcal .= PHP_EOL; + $testIcal .= implode(PHP_EOL, $this->formatIcalEvent($veventParts)); + $testIcal .= PHP_EOL; + $testIcal .= implode(PHP_EOL, $this->getIcalFooter()); + + $ical = new ICal(false, $options); + $ical->initString($testIcal); + + $events = $ical->events(); + + $this->assertCount($count, $events); + + foreach ($checks as $check) { + $this->assertEvent($events[$check['index']], $check['dateString'], $check['message'], isset($check['timezone']) ? $check['timezone'] : $defaultTimezone); + } + } + + public function assertEventFile($defaultTimezone, $file, $count, $checks) + { + $options = $this->getOptions($defaultTimezone); + + $ical = new ICal($file, $options); + + $events = $ical->events(); + + $this->assertCount($count, $events); + + $events = $ical->sortEventsWithOrder($events); + + foreach ($checks as $check) { + $this->assertEvent($events[$check['index']], $check['dateString'], $check['message'], isset($check['timezone']) ? $check['timezone'] : $defaultTimezone); + } + } + + public function assertEvent($event, $expectedDateString, $message, $timeZone = null) + { + if (!is_null($timeZone)) { + date_default_timezone_set($timeZone); + } + + $expectedTimeStamp = strtotime($expectedDateString); + + $this->assertSame($expectedTimeStamp, $event->dtstart_array[2], $message . 'timestamp mismatch (expected ' . $expectedDateString . ' vs actual ' . $event->dtstart . ')'); + $this->assertSame($expectedDateString, $event->dtstart, $message . 'dtstart mismatch (timestamp is okay)'); + } + + public function getOptions($defaultTimezone) + { + $options = array( + 'defaultSpan' => 2, // Default value + 'defaultTimeZone' => $defaultTimezone, // Default value: UTC + 'defaultWeekStart' => 'MO', // Default value + 'disableCharacterReplacement' => false, // Default value + 'filterDaysAfter' => null, // Default value + 'filterDaysBefore' => null, // Default value + 'httpUserAgent' => null, // Default value + 'skipRecurrence' => false, // Default value + ); + + return $options; + } + + public function formatIcalEvent($veventParts) + { + return array_merge( + array( + 'BEGIN:VEVENT', + 'CREATED:' . gmdate('Ymd\THis\Z'), + 'UID:M2CD-1-1-5FB000FB-BBE4-4F3F-9E7E-217F1FF97209', + ), + $veventParts, + array( + 'SUMMARY:test', + 'LAST-MODIFIED:' . gmdate('Ymd\THis\Z', filemtime(__FILE__)), + 'END:VEVENT', + ) + ); + } + + public function getIcalHeader() + { + return array( + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'PRODID:-//Google Inc//Google Calendar 70.9054//EN', + 'X-WR-CALNAME:Private', + 'X-APPLE-CALENDAR-COLOR:#FF2968', + 'X-WR-CALDESC:', + ); + } + + public function getIcalFooter() + { + return array('END:VCALENDAR'); + } +} diff --git a/lib/composer/vendor/johngrogg/ics-parser/tests/Rfc5545RecurrenceTest.php b/lib/composer/vendor/johngrogg/ics-parser/tests/Rfc5545RecurrenceTest.php new file mode 100644 index 0000000..a617445 --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/tests/Rfc5545RecurrenceTest.php @@ -0,0 +1,1059 @@ +originalTimeZone = date_default_timezone_get(); + } + + /** + * @after + */ + public function tearDownFixtures() + { + date_default_timezone_set($this->originalTimeZone); + } + + // Page 123, Test 1 :: Daily, 10 Occurrences + public function test_page123_test1() + { + $checks = array( + array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19970903T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19970904T090000', 'message' => '3rd occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970902T090000', + 'RRULE:FREQ=DAILY;COUNT=10', + ), + 10, + $checks + ); + } + + // Page 123, Test 2 :: Daily, until December 24th + public function test_page123_test2() + { + $checks = array( + array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19970903T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19970904T090000', 'message' => '3rd occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970902T090000', + 'RRULE:FREQ=DAILY;UNTIL=19971224T000000Z', + ), + 113, + $checks + ); + } + + // Page 123, Test 3 :: Daily, until December 24th, with trailing semicolon + public function test_page123_test3() + { + $checks = array( + array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19970903T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19970904T090000', 'message' => '3rd occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970902T090000', + 'RRULE:FREQ=DAILY;UNTIL=19971224T000000Z;', + ), + 113, + $checks + ); + } + + // Page 124, Test 1 :: Daily, every other day, Forever + // + // UNTIL rule does not exist in original example + public function test_page124_test1() + { + $checks = array( + array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19970904T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19970906T090000', 'message' => '3rd occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970902T090000', + 'RRULE:FREQ=DAILY;INTERVAL=2;UNTIL=19971201Z', + ), + 45, + $checks + ); + } + + // Page 124, Test 2 :: Daily, 10-day intervals, 5 occurrences + public function test_page124_test2() + { + $checks = array( + array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19970912T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19970922T090000', 'message' => '3rd occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970902T090000', + 'RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5', + ), + 5, + $checks + ); + } + + // Page 124, Test 3a :: Every January day, for 3 years (Variant A) + public function test_page124_test3a() + { + $checks = array( + array('index' => 0, 'dateString' => '19980101T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19980102T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19980103T090000', 'message' => '3rd occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19980101T090000', + 'RRULE:FREQ=YEARLY;UNTIL=20000131T140000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA', + ), + 93, + $checks + ); + } + + /* Requires support for BYMONTH under DAILY [No ticket] + * + // Page 124, Test 3b :: Every January day, for 3 years (Variant B) + public function test_page124_test3b() + { + $checks = array( + array('index' => 0, 'dateString' => '19980101T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19980102T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19980103T090000', 'message' => '3rd occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19980101T090000', + 'RRULE:FREQ=DAILY;UNTIL=20000131T140000Z;BYMONTH=1', + ), + 93, + $checks + ); + } + */ + + // Page 124, Test 4 :: Weekly, 10 occurrences + public function test_page124_test4() + { + $checks = array( + array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19970909T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19970916T090000', 'message' => '3rd occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970902T090000', + 'RRULE:FREQ=WEEKLY;COUNT=10', + ), + 10, + $checks + ); + } + + // Page 125, Test 1 :: Weekly, until December 24th + public function test_page125_test1() + { + $checks = array( + array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19970909T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19970916T090000', 'message' => '3rd occurrence: '), + array('index' => 16, 'dateString' => '19971223T090000', 'message' => 'last occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970902T090000', + 'RRULE:FREQ=WEEKLY;UNTIL=19971224T000000Z', + ), + 17, + $checks + ); + } + + // Page 125, Test 2 :: Every other week, forever + // + // UNTIL rule does not exist in original example + public function test_page125_test2() + { + $checks = array( + array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19970916T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19970930T090000', 'message' => '3rd occurrence: '), + array('index' => 3, 'dateString' => '19971014T090000', 'message' => '4th occurrence: '), + array('index' => 4, 'dateString' => '19971028T090000', 'message' => '5th occurrence: '), + array('index' => 5, 'dateString' => '19971111T090000', 'message' => '6th occurrence: '), + array('index' => 6, 'dateString' => '19971125T090000', 'message' => '7th occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970902T090000', + 'RRULE:FREQ=WEEKLY;INTERVAL=2;WKST=SU;UNTIL=19971201Z', + ), + 7, + $checks + ); + } + + // Page 125, Test 3a :: Tuesday & Thursday every week, for five weeks (Variant A) + public function test_page125_test3a() + { + $checks = array( + array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19970904T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19970909T090000', 'message' => '3rd occurrence: '), + array('index' => 9, 'dateString' => '19971002T090000', 'message' => 'final occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970902T090000', + 'RRULE:FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH', + ), + 10, + $checks + ); + } + + // Page 125, Test 3b :: Tuesday & Thursday every week, for five weeks (Variant B) + public function test_page125_test3b() + { + $checks = array( + array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19970904T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19970909T090000', 'message' => '3rd occurrence: '), + array('index' => 9, 'dateString' => '19971002T090000', 'message' => 'final occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970902T090000', + 'RRULE:FREQ=WEEKLY;COUNT=10;WKST=SU;BYDAY=TU,TH', + ), + 10, + $checks + ); + } + + // Page 125, Test 4 :: Monday, Wednesday & Friday of every other week until December 24th + public function test_page125_test4() + { + $checks = array( + array('index' => 0, 'dateString' => '19970901T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19970903T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19970905T090000', 'message' => '3rd occurrence: '), + array('index' => 24, 'dateString' => '19971222T090000', 'message' => 'final occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970901T090000', + 'RRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR', + ), + 25, + $checks + ); + } + + // Page 126, Test 1 :: Tuesday & Thursday, every other week, for 8 occurrences + public function test_page126_test1() + { + $checks = array( + array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19970904T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19970916T090000', 'message' => '3rd occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970902T090000', + 'RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH', + ), + 8, + $checks + ); + } + + // Page 126, Test 2 :: First Friday of the Month, for 10 occurrences + public function test_page126_test2() + { + $checks = array( + array('index' => 0, 'dateString' => '19970905T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19971003T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19971107T090000', 'message' => '3rd occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970905T090000', + 'RRULE:FREQ=MONTHLY;COUNT=10;BYDAY=1FR', + ), + 10, + $checks + ); + } + + // Page 126, Test 3 :: First Friday of the Month, until 24th December + public function test_page126_test3() + { + $checks = array( + array('index' => 0, 'dateString' => '19970905T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19971003T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19971107T090000', 'message' => '3rd occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970905T090000', + 'RRULE:FREQ=MONTHLY;UNTIL=19971224T000000Z;BYDAY=1FR', + ), + 4, + $checks + ); + } + + // Page 126, Test 4 :: First and last Sunday, every other Month, for 10 occurrences + public function test_page126_test4() + { + $checks = array( + array('index' => 0, 'dateString' => '19970907T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19970928T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19971102T090000', 'message' => '3rd occurrence: '), + array('index' => 3, 'dateString' => '19971130T090000', 'message' => '4th occurrence: '), + array('index' => 4, 'dateString' => '19980104T090000', 'message' => '5th occurrence: '), + array('index' => 5, 'dateString' => '19980125T090000', 'message' => '6th occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970907T090000', + 'RRULE:FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU', + ), + 10, + $checks + ); + } + + // Page 126, Test 5 :: Second-to-last Monday of the Month, for six months + public function test_page126_test5() + { + $checks = array( + array('index' => 0, 'dateString' => '19970922T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19971020T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19971117T090000', 'message' => '3rd occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970922T090000', + 'RRULE:FREQ=MONTHLY;COUNT=6;BYDAY=-2MO', + ), + 6, + $checks + ); + } + + // Page 127, Test 1 :: Third-to-last day of the month, forever + // + // UNTIL rule does not exist in original example. + public function test_page127_test1() + { + $checks = array( + array('index' => 0, 'dateString' => '19970928T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19971029T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19971128T090000', 'message' => '3rd occurrence: '), + array('index' => 3, 'dateString' => '19971229T090000', 'message' => '4th occurrence: '), + array('index' => 4, 'dateString' => '19980129T090000', 'message' => '5th occurrence: '), + array('index' => 5, 'dateString' => '19980226T090000', 'message' => '6th occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970928T090000', + 'RRULE:FREQ=MONTHLY;BYMONTHDAY=-3;UNTIL=19980401', + ), + 7, + $checks + ); + } + + // Page 127, Test 2 :: 2nd and 15th of each Month, for 10 occurrences + public function test_page127_test2() + { + $checks = array( + array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19970915T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19971002T090000', 'message' => '3rd occurrence: '), + array('index' => 3, 'dateString' => '19971015T090000', 'message' => '4th occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970902T090000', + 'RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15', + ), + 10, + $checks + ); + } + + // Page 127, Test 3 :: First and last day of the month, for 10 occurrences + public function test_page127_test3() + { + $checks = array( + array('index' => 0, 'dateString' => '19970930T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19971001T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19971031T090000', 'message' => '3rd occurrence: '), + array('index' => 3, 'dateString' => '19971101T090000', 'message' => '4th occurrence: '), + array('index' => 4, 'dateString' => '19971130T090000', 'message' => '5th occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970930T090000', + 'RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1', + ), + 10, + $checks + ); + } + + // Page 127, Test 4 :: 10th through 15th, every 18 months, for 10 occurrences + public function test_page127_test4() + { + $checks = array( + array('index' => 0, 'dateString' => '19970910T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19970911T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19970912T090000', 'message' => '3rd occurrence: '), + array('index' => 6, 'dateString' => '19990310T090000', 'message' => '7th occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970910T090000', + 'RRULE:FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15', + ), + 10, + $checks + ); + } + + // Page 127, Test 5 :: Every Tuesday, every other Month, forever + // + // UNTIL rule does not exist in original example. + public function test_page127_test5() + { + $checks = array( + array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19970909T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19970916T090000', 'message' => '3rd occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970902T090000', + 'RRULE:FREQ=MONTHLY;INTERVAL=2;BYDAY=TU;UNTIL=19980101', + ), + 9, + $checks + ); + } + + // Page 128, Test 1 :: June & July of each Year, for 10 occurrences + public function test_page128_test1() + { + $checks = array( + array('index' => 0, 'dateString' => '19970610T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19970710T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19980610T090000', 'message' => '3rd occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970610T090000', + 'RRULE:FREQ=YEARLY;COUNT=10;BYMONTH=6,7', + ), + 10, + $checks + ); + } + + // Page 128, Test 2 :: January, February, & March, every other Year, for 10 occurrences + public function test_page128_test2() + { + $checks = array( + array('index' => 0, 'dateString' => '19970310T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19990110T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19990210T090000', 'message' => '3rd occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970310T090000', + 'RRULE:FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3', + ), + 10, + $checks + ); + } + + // Page 128, Test 3 :: Every third Year on the 1st, 100th, & 200th day for 10 occurrences + public function test_page128_test3() + { + $checks = array( + array('index' => 0, 'dateString' => '19970101T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19970410T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19970719T090000', 'message' => '3rd occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970101T090000', + 'RRULE:FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200', + ), + 10, + $checks + ); + } + + // Page 128, Test 4 :: 20th Monday of a Year, forever + // + // COUNT rule does not exist in original example. + public function test_page128_test4() + { + $checks = array( + array('index' => 0, 'dateString' => '19970519T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19980518T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19990517T090000', 'message' => '3rd occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970519T090000', + 'RRULE:FREQ=YEARLY;BYDAY=20MO;COUNT=4', + ), + 4, + $checks + ); + } + + // Page 129, Test 1 :: Monday of Week 20, where the default start of the week is Monday, forever + // + // COUNT rule does not exist in original example. + public function test_page129_test1() + { + $checks = array( + array('index' => 0, 'dateString' => '19970512T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19980511T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19990517T090000', 'message' => '3rd occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970512T090000', + 'RRULE:FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO;COUNT=4', + ), + 4, + $checks + ); + } + + // Page 129, Test 2 :: Every Thursday in March, forever + // + // UNTIL rule does not exist in original example. + public function test_page129_test2() + { + $checks = array( + array('index' => 0, 'dateString' => '19970313T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19970320T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19970327T090000', 'message' => '3rd occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970313T090000', + 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=TH;UNTIL=19990401Z', + ), + 11, + $checks + ); + } + + // Page 129, Test 3 :: Every Thursday in June, July, & August, forever + // + // UNTIL rule does not exist in original example. + public function test_page129_test3() + { + $checks = array( + array('index' => 0, 'dateString' => '19970605T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19970612T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19970619T090000', 'message' => '3rd occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970605T090000', + 'RRULE:FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8;UNTIL=19970901Z', + ), + 13, + $checks + ); + } + + /* Requires support for BYMONTHDAY and BYDAY in the same MONTHLY RRULE [No ticket] + * + // Page 129, Test 4 :: Every Friday 13th, forever + // + // COUNT rule does not exist in original example. + public function test_page129_test4() + { + $checks = array( + array('index' => 0, 'dateString' => '19980213T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19980313T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19981113T090000', 'message' => '3rd occurrence: '), + array('index' => 3, 'dateString' => '19990813T090000', 'message' => '4th occurrence: '), + array('index' => 4, 'dateString' => '20001013T090000', 'message' => '5th occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970902T090000', + 'EXDATE;TZID=America/New_York:19970902T090000', + 'RRULE:FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13;COUNT=5', + ), + 5, + $checks + ); + } + */ + + // Page 130, Test 1 :: The first Saturday that follows the first Sunday of the month, forever: + // + // COUNT rule does not exist in original example. + public function test_page130_test1() + { + $checks = array( + array('index' => 0, 'dateString' => '19970913T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19971011T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19971108T090000', 'message' => '3rd occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970913T090000', + 'RRULE:FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13;COUNT=7', + ), + 7, + $checks + ); + } + + // Page 130, Test 2 :: The first Tuesday after a Monday in November, every 4 Years (U.S. Presidential Election Day), forever + // + // COUNT rule does not exist in original example. + public function test_page130_test2() + { + $checks = array( + array('index' => 0, 'dateString' => '19961105T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '20001107T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '20041102T090000', 'message' => '3rd occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19961105T090000', + 'RRULE:FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8;COUNT=4', + ), + 4, + $checks + ); + } + + // Page 130, Test 3 :: Third instance of either a Tuesday, Wednesday, or Thursday of a Month, for 3 months. + public function test_page130_test3() + { + $checks = array( + array('index' => 0, 'dateString' => '19970904T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19971007T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19971106T090000', 'message' => '3rd occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970904T090000', + 'RRULE:FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3', + ), + 3, + $checks + ); + } + + // Page 130, Test 4 :: Second-to-last weekday of the month, indefinitely + // + // UNTIL rule does not exist in original example. + public function test_page130_test4() + { + $checks = array( + array('index' => 0, 'dateString' => '19970929T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19971030T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19971127T090000', 'message' => '3rd occurrence: '), + array('index' => 3, 'dateString' => '19971230T090000', 'message' => '4th occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970929T090000', + 'RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2;UNTIL=19980101', + ), + 4, + $checks + ); + } + + /* Requires support of HOURLY frequency [#101] + * + // Page 131, Test 1 :: Every 3 hours from 09:00 to 17:00 on a specific day + public function test_page131_test1() + { + $checks = array( + array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19970902T120000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19970902T150000', 'message' => '3rd occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970902T090000', + 'FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T170000Z', + ), + 3, + $checks + ); + } + */ + + /* Requires support of MINUTELY frequency [#101] + * + // Page 131, Test 2 :: Every 15 minutes for 6 occurrences + public function test_page131_test2() + { + $checks = array( + array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19970902T091500', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19970902T093000', 'message' => '3rd occurrence: '), + array('index' => 3, 'dateString' => '19970902T094500', 'message' => '4th occurrence: '), + array('index' => 4, 'dateString' => '19970902T100000', 'message' => '5th occurrence: '), + array('index' => 5, 'dateString' => '19970902T101500', 'message' => '6th occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970902T090000', + 'RRULE:FREQ=MINUTELY;INTERVAL=15;COUNT=6', + ), + 6, + $checks + ); + } + */ + + /* Requires support of MINUTELY frequency [#101] + * + // Page 131, Test 3 :: Every hour and a half for 4 occurrences + public function test_page131_test3() + { + $checks = array( + array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19970902T103000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19970902T120000', 'message' => '3rd occurrence: '), + array('index' => 3, 'dateString' => '19970902T133000', 'message' => '4th occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970902T090000', + 'RRULE:FREQ=MINUTELY;INTERVAL=90;COUNT=4', + ), + 4, + $checks + ); + } + */ + + /* Requires support of BYHOUR and BYMINUTE under DAILY [#11] + * + // Page 131, Test 4a :: Every 20 minutes from 9:00 to 16:40 every day, using DAILY + // + // UNTIL rule does not exist in original example + public function test_page131_test4a() + { + $checks = array( + array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence, Day 1: '), + array('index' => 1, 'dateString' => '19970902T092000', 'message' => '2nd occurrence, Day 1: '), + array('index' => 2, 'dateString' => '19970902T094000', 'message' => '3rd occurrence, Day 1: '), + array('index' => 3, 'dateString' => '19970902T100000', 'message' => '4th occurrence, Day 1: '), + array('index' => 20, 'dateString' => '19970902T164000', 'message' => 'Last occurrence, Day 1: '), + array('index' => 21, 'dateString' => '19970903T090000', 'message' => '1st occurrence, Day 2: '), + array('index' => 41, 'dateString' => '19970903T164000', 'message' => 'Last occurrence, Day 2: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970902T090000', + 'RRULE:FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40;UNTIL=19970904T000000Z', + ), + 42, + $checks + ); + } + */ + + /* Requires support of MINUTELY frequency [#101] + * + // Page 131, Test 4b :: Every 20 minutes from 9:00 to 16:40 every day, using MINUTELY + // + // UNTIL rule does not exist in original example + public function test_page131_test4b() + { + $checks = array( + array('index' => 0, 'dateString' => '19970902T090000', 'message' => '1st occurrence, Day 1: '), + array('index' => 1, 'dateString' => '19970902T092000', 'message' => '2nd occurrence, Day 1: '), + array('index' => 2, 'dateString' => '19970902T094000', 'message' => '3rd occurrence, Day 1: '), + array('index' => 3, 'dateString' => '19970902T100000', 'message' => '4th occurrence, Day 1: '), + array('index' => 20, 'dateString' => '19970902T164000', 'message' => 'Last occurrence, Day 1: '), + array('index' => 21, 'dateString' => '19970903T090000', 'message' => '1st occurrence, Day 2: '), + array('index' => 41, 'dateString' => '19970903T164000', 'message' => 'Last occurrence, Day 2: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970902T090000', + 'RRULE:FREQ=MINUTELY;INTERVAL=20;BYHOUR=9,10,11,12,13,14,15,16;UNTIL=19970904T000000Z', + ), + 42, + $checks + ); + } + */ + + // Page 131, Test 5a :: Changing the passed WKST rule, before... + public function test_page131_test5a() + { + $checks = array( + array('index' => 0, 'dateString' => '19970805T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19970810T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19970819T090000', 'message' => '3rd occurrence: '), + array('index' => 3, 'dateString' => '19970824T090000', 'message' => '4th occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970805T090000', + 'RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO', + ), + 4, + $checks + ); + } + + // Page 131, Test 5b :: ...and after + public function test_page131_test5b() + { + $checks = array( + array('index' => 0, 'dateString' => '19970805T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '19970817T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '19970819T090000', 'message' => '3rd occurrence: '), + array('index' => 3, 'dateString' => '19970831T090000', 'message' => '4th occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:19970805T090000', + 'RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU', + ), + 4, + $checks + ); + } + + // Page 132, Test 1 :: Automatically ignoring an invalid date (30 February) + public function test_page132_test1() + { + $checks = array( + array('index' => 0, 'dateString' => '20070115T090000', 'message' => '1st occurrence: '), + array('index' => 1, 'dateString' => '20070130T090000', 'message' => '2nd occurrence: '), + array('index' => 2, 'dateString' => '20070215T090000', 'message' => '3rd occurrence: '), + array('index' => 3, 'dateString' => '20070315T090000', 'message' => '4th occurrence: '), + array('index' => 4, 'dateString' => '20070330T090000', 'message' => '5th occurrence: '), + ); + $this->assertVEVENT( + 'America/New_York', + array( + 'DTSTART;TZID=America/New_York:20070115T090000', + 'RRULE:FREQ=MONTHLY;BYMONTHDAY=15,30;COUNT=5', + ), + 5, + $checks + ); + } + + public function assertVEVENT($defaultTimezone, $veventParts, $count, $checks) + { + $options = $this->getOptions($defaultTimezone); + + $testIcal = implode(PHP_EOL, $this->getIcalHeader()); + $testIcal .= PHP_EOL; + $testIcal .= implode(PHP_EOL, $this->formatIcalEvent($veventParts)); + $testIcal .= PHP_EOL; + $testIcal .= implode(PHP_EOL, $this->getIcalFooter()); + + $ical = new ICal(false, $options); + $ical->initString($testIcal); + + $events = $ical->events(); + + $this->assertCount($count, $events); + + foreach ($checks as $check) { + $this->assertEvent($events[$check['index']], $check['dateString'], $check['message'], isset($check['timezone']) ? $check['timezone'] : $defaultTimezone); + } + } + + public function assertEvent($event, $expectedDateString, $message, $timeZone = null) + { + if (!is_null($timeZone)) { + date_default_timezone_set($timeZone); + } + + $expectedTimeStamp = strtotime($expectedDateString); + + $this->assertSame($expectedTimeStamp, $event->dtstart_array[2], $message . 'timestamp mismatch (expected ' . $expectedDateString . ' vs actual ' . $event->dtstart . ')'); + $this->assertSame($expectedDateString, $event->dtstart, $message . 'dtstart mismatch (timestamp is okay)'); + } + + public function getOptions($defaultTimezone) + { + $options = array( + 'defaultSpan' => 2, // Default value: 2 + 'defaultTimeZone' => $defaultTimezone, // Default value: UTC + 'defaultWeekStart' => 'MO', // Default value + 'disableCharacterReplacement' => false, // Default value + 'filterDaysAfter' => null, // Default value + 'filterDaysBefore' => null, // Default value + 'httpUserAgent' => null, // Default value + 'skipRecurrence' => false, // Default value + ); + + return $options; + } + + public function formatIcalEvent($veventParts) + { + return array_merge( + array( + 'BEGIN:VEVENT', + 'CREATED:' . gmdate('Ymd\THis\Z'), + 'UID:RFC5545-examples-test', + ), + $veventParts, + array( + 'SUMMARY:test', + 'LAST-MODIFIED:' . gmdate('Ymd\THis\Z', filemtime(__FILE__)), + 'END:VEVENT', + ) + ); + } + + public function getIcalHeader() + { + return array( + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'PRODID:-//Google Inc//Google Calendar 70.9054//EN', + 'X-WR-CALNAME:Private', + 'X-APPLE-CALENDAR-COLOR:#FF2968', + 'X-WR-CALDESC:', + ); + } + + public function getIcalFooter() + { + return array('END:VCALENDAR'); + } +} diff --git a/lib/composer/vendor/johngrogg/ics-parser/tests/SingleEventsTest.php b/lib/composer/vendor/johngrogg/ics-parser/tests/SingleEventsTest.php new file mode 100644 index 0000000..fc89c67 --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/tests/SingleEventsTest.php @@ -0,0 +1,509 @@ +originalTimeZone = date_default_timezone_get(); + } + + /** + * @after + */ + public function tearDownFixtures() + { + date_default_timezone_set($this->originalTimeZone); + } + + public function testFullDayTimeZoneBerlin() + { + $checks = array( + array('index' => 0, 'dateString' => '20000301', 'message' => '1st event, CET: '), + ); + $this->assertVEVENT( + 'Europe/Berlin', + 'DTSTART;VALUE=DATE:20000301', + 'DTEND;VALUE=DATE:20000302', + 1, + $checks + ); + } + + public function testSeveralFullDaysTimeZoneBerlin() + { + $checks = array( + array('index' => 0, 'dateString' => '20000301', 'message' => '1st event, CET: '), + ); + $this->assertVEVENT( + 'Europe/Berlin', + 'DTSTART;VALUE=DATE:20000301', + 'DTEND;VALUE=DATE:20000304', + 1, + $checks + ); + } + + public function testEventTimeZoneUTC() + { + $checks = array( + array('index' => 0, 'dateString' => '20180626T070000Z', 'message' => '1st event, UTC: '), + ); + $this->assertVEVENT( + 'Europe/Berlin', + 'DTSTART:20180626T070000Z', + 'DTEND:20180626T110000Z', + 1, + $checks + ); + } + + public function testEventTimeZoneBerlin() + { + $checks = array( + array('index' => 0, 'dateString' => '20180626T070000', 'message' => '1st event, CEST: '), + ); + $this->assertVEVENT( + 'Europe/Berlin', + 'DTSTART:20180626T070000', + 'DTEND:20180626T110000', + 1, + $checks + ); + } + + public function assertVEVENT($defaultTimezone, $dtstart, $dtend, $count, $checks) + { + $options = $this->getOptions($defaultTimezone); + + $testIcal = implode(PHP_EOL, $this->getIcalHeader()); + $testIcal .= PHP_EOL; + $testIcal .= implode(PHP_EOL, $this->formatIcalEvent($dtstart, $dtend)); + $testIcal .= PHP_EOL; + $testIcal .= implode(PHP_EOL, $this->getIcalTimezones()); + $testIcal .= PHP_EOL; + $testIcal .= implode(PHP_EOL, $this->getIcalFooter()); + + date_default_timezone_set('UTC'); + + $ical = new ICal(false, $options); + $ical->initString($testIcal); + + $events = $ical->events(); + + $this->assertCount($count, $events); + + foreach ($checks as $check) { + $this->assertEvent( + $events[$check['index']], + $check['dateString'], + $check['message'], + isset($check['timezone']) ? $check['timezone'] : $defaultTimezone + ); + } + } + + public function getOptions($defaultTimezone) + { + $options = array( + 'defaultSpan' => 2, // Default value + 'defaultTimeZone' => $defaultTimezone, // Default value: UTC + 'defaultWeekStart' => 'MO', // Default value + 'disableCharacterReplacement' => false, // Default value + 'filterDaysAfter' => null, // Default value + 'filterDaysBefore' => null, // Default value + 'httpUserAgent' => null, // Default value + 'skipRecurrence' => false, // Default value + ); + + return $options; + } + + public function getIcalHeader() + { + return array( + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'PRODID:-//Google Inc//Google Calendar 70.9054//EN', + 'X-WR-CALNAME:Private', + 'X-APPLE-CALENDAR-COLOR:#FF2968', + 'X-WR-CALDESC:', + ); + } + + public function formatIcalEvent($dtstart, $dtend) + { + return array( + 'BEGIN:VEVENT', + 'CREATED:20090213T195947Z', + 'UID:M2CD-1-1-5FB000FB-BBE4-4F3F-9E7E-217F1FF97209', + $dtstart, + $dtend, + 'SUMMARY:test', + 'DESCRIPTION;LANGUAGE=en-gb:This is a short description\nwith a new line. Some "special" \'s', + ' igns\' may be interesting\, too.', + '  And a non-breaking space.', + 'LAST-MODIFIED:20110429T222101Z', + 'DTSTAMP:20170630T105724Z', + 'SEQUENCE:0', + 'END:VEVENT', + ); + } + + public function getIcalTimezones() + { + return array( + 'BEGIN:VTIMEZONE', + 'TZID:Europe/Berlin', + 'X-LIC-LOCATION:Europe/Berlin', + 'BEGIN:STANDARD', + 'DTSTART:18930401T000000', + 'RDATE:18930401T000000', + 'TZNAME:CEST', + 'TZOFFSETFROM:+005328', + 'TZOFFSETTO:+0100', + 'END:STANDARD', + 'BEGIN:DAYLIGHT', + 'DTSTART:19160430T230000', + 'RDATE:19160430T230000', + 'RDATE:19400401T020000', + 'RDATE:19430329T020000', + 'RDATE:19460414T020000', + 'RDATE:19470406T030000', + 'RDATE:19480418T020000', + 'RDATE:19490410T020000', + 'RDATE:19800406T020000', + 'TZNAME:CEST', + 'TZOFFSETFROM:+0100', + 'TZOFFSETTO:+0200', + 'END:DAYLIGHT', + 'BEGIN:STANDARD', + 'DTSTART:19161001T010000', + 'RDATE:19161001T010000', + 'RDATE:19421102T030000', + 'RDATE:19431004T030000', + 'RDATE:19441002T030000', + 'RDATE:19451118T030000', + 'RDATE:19461007T030000', + 'TZNAME:CET', + 'TZOFFSETFROM:+0200', + 'TZOFFSETTO:+0100', + 'END:STANDARD', + 'BEGIN:DAYLIGHT', + 'DTSTART:19170416T020000', + 'RRULE:FREQ=YEARLY;UNTIL=19180415T010000Z;BYMONTH=4;BYDAY=3MO', + 'TZNAME:CEST', + 'TZOFFSETFROM:+0100', + 'TZOFFSETTO:+0200', + 'END:DAYLIGHT', + 'BEGIN:STANDARD', + 'DTSTART:19170917T030000', + 'RRULE:FREQ=YEARLY;UNTIL=19180916T010000Z;BYMONTH=9;BYDAY=3MO', + 'TZNAME:CET', + 'TZOFFSETFROM:+0200', + 'TZOFFSETTO:+0100', + 'END:STANDARD', + 'BEGIN:DAYLIGHT', + 'DTSTART:19440403T020000', + 'RRULE:FREQ=YEARLY;UNTIL=19450402T010000Z;BYMONTH=4;BYDAY=1MO', + 'TZNAME:CEST', + 'TZOFFSETFROM:+0100', + 'TZOFFSETTO:+0200', + 'END:DAYLIGHT', + 'BEGIN:DAYLIGHT', + 'DTSTART:19450524T020000', + 'RDATE:19450524T020000', + 'RDATE:19470511T030000', + 'TZNAME:CEMT', + 'TZOFFSETFROM:+0200', + 'TZOFFSETTO:+0300', + 'END:DAYLIGHT', + 'BEGIN:DAYLIGHT', + 'DTSTART:19450924T030000', + 'RDATE:19450924T030000', + 'RDATE:19470629T030000', + 'TZNAME:CEST', + 'TZOFFSETFROM:+0300', + 'TZOFFSETTO:+0200', + 'END:DAYLIGHT', + 'BEGIN:STANDARD', + 'DTSTART:19460101T000000', + 'RDATE:19460101T000000', + 'RDATE:19800101T000000', + 'TZNAME:CEST', + 'TZOFFSETFROM:+0100', + 'TZOFFSETTO:+0100', + 'END:STANDARD', + 'BEGIN:STANDARD', + 'DTSTART:19471005T030000', + 'RRULE:FREQ=YEARLY;UNTIL=19491002T010000Z;BYMONTH=10;BYDAY=1SU', + 'TZNAME:CET', + 'TZOFFSETFROM:+0200', + 'TZOFFSETTO:+0100', + 'END:STANDARD', + 'BEGIN:STANDARD', + 'DTSTART:19800928T030000', + 'RRULE:FREQ=YEARLY;UNTIL=19950924T010000Z;BYMONTH=9;BYDAY=-1SU', + 'TZNAME:CET', + 'TZOFFSETFROM:+0200', + 'TZOFFSETTO:+0100', + 'END:STANDARD', + 'BEGIN:DAYLIGHT', + 'DTSTART:19810329T020000', + 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU', + 'TZNAME:CEST', + 'TZOFFSETFROM:+0100', + 'TZOFFSETTO:+0200', + 'END:DAYLIGHT', + 'BEGIN:STANDARD', + 'DTSTART:19961027T030000', + 'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU', + 'TZNAME:CET', + 'TZOFFSETFROM:+0200', + 'TZOFFSETTO:+0100', + 'END:STANDARD', + 'END:VTIMEZONE', + 'BEGIN:VTIMEZONE', + 'TZID:Europe/Paris', + 'X-LIC-LOCATION:Europe/Paris', + 'BEGIN:STANDARD', + 'DTSTART:18910315T000100', + 'RDATE:18910315T000100', + 'TZNAME:PMT', + 'TZOFFSETFROM:+000921', + 'TZOFFSETTO:+000921', + 'END:STANDARD', + 'BEGIN:STANDARD', + 'DTSTART:19110311T000100', + 'RDATE:19110311T000100', + 'TZNAME:WEST', + 'TZOFFSETFROM:+000921', + 'TZOFFSETTO:+0000', + 'END:STANDARD', + 'BEGIN:DAYLIGHT', + 'DTSTART:19160614T230000', + 'RDATE:19160614T230000', + 'RDATE:19170324T230000', + 'RDATE:19180309T230000', + 'RDATE:19190301T230000', + 'RDATE:19200214T230000', + 'RDATE:19210314T230000', + 'RDATE:19220325T230000', + 'RDATE:19230526T230000', + 'RDATE:19240329T230000', + 'RDATE:19250404T230000', + 'RDATE:19260417T230000', + 'RDATE:19270409T230000', + 'RDATE:19280414T230000', + 'RDATE:19290420T230000', + 'RDATE:19300412T230000', + 'RDATE:19310418T230000', + 'RDATE:19320402T230000', + 'RDATE:19330325T230000', + 'RDATE:19340407T230000', + 'RDATE:19350330T230000', + 'RDATE:19360418T230000', + 'RDATE:19370403T230000', + 'RDATE:19380326T230000', + 'RDATE:19390415T230000', + 'RDATE:19400225T020000', + 'TZNAME:WEST', + 'TZOFFSETFROM:+0000', + 'TZOFFSETTO:+0100', + 'END:DAYLIGHT', + 'BEGIN:STANDARD', + 'DTSTART:19161002T000000', + 'RRULE:FREQ=YEARLY;UNTIL=19191005T230000Z;BYMONTH=10;BYMONTHDAY=2,3,4,5,6,', + ' 7,8;BYDAY=MO', + 'TZNAME:WET', + 'TZOFFSETFROM:+0100', + 'TZOFFSETTO:+0000', + 'END:STANDARD', + 'BEGIN:STANDARD', + 'DTSTART:19201024T000000', + 'RDATE:19201024T000000', + 'RDATE:19211026T000000', + 'RDATE:19391119T000000', + 'TZNAME:WET', + 'TZOFFSETFROM:+0100', + 'TZOFFSETTO:+0000', + 'END:STANDARD', + 'BEGIN:STANDARD', + 'DTSTART:19221008T000000', + 'RRULE:FREQ=YEARLY;UNTIL=19381001T230000Z;BYMONTH=10;BYMONTHDAY=2,3,4,5,6,', + ' 7,8;BYDAY=SU', + 'TZNAME:WET', + 'TZOFFSETFROM:+0100', + 'TZOFFSETTO:+0000', + 'END:STANDARD', + 'BEGIN:STANDARD', + 'DTSTART:19400614T230000', + 'RDATE:19400614T230000', + 'TZNAME:CEST', + 'TZOFFSETFROM:+0100', + 'TZOFFSETTO:+0200', + 'END:STANDARD', + 'BEGIN:STANDARD', + 'DTSTART:19421102T030000', + 'RDATE:19421102T030000', + 'RDATE:19431004T030000', + 'RDATE:19760926T010000', + 'RDATE:19770925T030000', + 'RDATE:19781001T030000', + 'TZNAME:CET', + 'TZOFFSETFROM:+0200', + 'TZOFFSETTO:+0100', + 'END:STANDARD', + 'BEGIN:DAYLIGHT', + 'DTSTART:19430329T020000', + 'RDATE:19430329T020000', + 'RDATE:19440403T020000', + 'RDATE:19760328T010000', + 'TZNAME:CEST', + 'TZOFFSETFROM:+0100', + 'TZOFFSETTO:+0200', + 'END:DAYLIGHT', + 'BEGIN:STANDARD', + 'DTSTART:19440825T000000', + 'RDATE:19440825T000000', + 'TZNAME:WEST', + 'TZOFFSETFROM:+0200', + 'TZOFFSETTO:+0200', + 'END:STANDARD', + 'BEGIN:DAYLIGHT', + 'DTSTART:19441008T010000', + 'RDATE:19441008T010000', + 'TZNAME:WEST', + 'TZOFFSETFROM:+0200', + 'TZOFFSETTO:+0100', + 'END:DAYLIGHT', + 'BEGIN:DAYLIGHT', + 'DTSTART:19450402T020000', + 'RDATE:19450402T020000', + 'TZNAME:WEMT', + 'TZOFFSETFROM:+0100', + 'TZOFFSETTO:+0200', + 'END:DAYLIGHT', + 'BEGIN:STANDARD', + 'DTSTART:19450916T030000', + 'RDATE:19450916T030000', + 'TZNAME:CEST', + 'TZOFFSETFROM:+0200', + 'TZOFFSETTO:+0100', + 'END:STANDARD', + 'BEGIN:STANDARD', + 'DTSTART:19770101T000000', + 'RDATE:19770101T000000', + 'TZNAME:CEST', + 'TZOFFSETFROM:+0100', + 'TZOFFSETTO:+0100', + 'END:STANDARD', + 'BEGIN:DAYLIGHT', + 'DTSTART:19770403T020000', + 'RRULE:FREQ=YEARLY;UNTIL=19800406T010000Z;BYMONTH=4;BYDAY=1SU', + 'TZNAME:CEST', + 'TZOFFSETFROM:+0100', + 'TZOFFSETTO:+0200', + 'END:DAYLIGHT', + 'BEGIN:STANDARD', + 'DTSTART:19790930T030000', + 'RRULE:FREQ=YEARLY;UNTIL=19950924T010000Z;BYMONTH=9;BYDAY=-1SU', + 'TZNAME:CET', + 'TZOFFSETFROM:+0200', + 'TZOFFSETTO:+0100', + 'END:STANDARD', + 'BEGIN:DAYLIGHT', + 'DTSTART:19810329T020000', + 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU', + 'TZNAME:CEST', + 'TZOFFSETFROM:+0100', + 'TZOFFSETTO:+0200', + 'END:DAYLIGHT', + 'BEGIN:STANDARD', + 'DTSTART:19961027T030000', + 'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU', + 'TZNAME:CET', + 'TZOFFSETFROM:+0200', + 'TZOFFSETTO:+0100', + 'END:STANDARD', + 'END:VTIMEZONE', + 'BEGIN:VTIMEZONE', + 'TZID:US-Eastern', + 'LAST-MODIFIED:19870101T000000Z', + 'TZURL:http://zones.stds_r_us.net/tz/US-Eastern', + 'BEGIN:STANDARD', + 'DTSTART:19671029T020000', + 'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10', + 'TZOFFSETFROM:-0400', + 'TZOFFSETTO:-0500', + 'TZNAME:EST', + 'END:STANDARD', + 'BEGIN:DAYLIGHT', + 'DTSTART:19870405T020000', + 'RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4', + 'TZOFFSETFROM:-0500', + 'TZOFFSETTO:-0400', + 'TZNAME:EDT', + 'END:DAYLIGHT', + 'END:VTIMEZONE', + ); + } + + public function getIcalFooter() + { + return array('END:VCALENDAR'); + } + + public function assertEvent($event, $expectedDateString, $message, $timezone = null) + { + if ($timezone !== null) { + date_default_timezone_set($timezone); + } + + $expectedTimeStamp = strtotime($expectedDateString); + + $this->assertSame( + $expectedTimeStamp, + $event->dtstart_array[2], + $message . 'timestamp mismatch (expected ' . $expectedDateString . ' vs actual ' . $event->dtstart . ')' + ); + $this->assertSame( + $expectedDateString, + $event->dtstart, + $message . 'dtstart mismatch (timestamp is okay)' + ); + } + + public function assertEventFile($defaultTimezone, $file, $count, $checks) + { + $options = $this->getOptions($defaultTimezone); + + date_default_timezone_set('UTC'); + + $ical = new ICal($file, $options); + + $events = $ical->events(); + + $this->assertCount($count, $events); + + foreach ($checks as $check) { + $this->assertEvent( + $events[$check['index']], + $check['dateString'], + $check['message'], + isset($check['timezone']) ? $check['timezone'] : $defaultTimezone + ); + } + } +} diff --git a/lib/composer/vendor/johngrogg/ics-parser/tests/ical/ical-monthly.ics b/lib/composer/vendor/johngrogg/ics-parser/tests/ical/ical-monthly.ics new file mode 100644 index 0000000..d80e221 --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/tests/ical/ical-monthly.ics @@ -0,0 +1,18 @@ +BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +X-WR-CALNAME:Private +X-APPLE-CALENDAR-COLOR:#FF2968 +X-WR-CALDESC: +BEGIN:VEVENT +CREATED:20090213T195947Z +UID:M2CD-1-1-5FB000FB-BBE4-4F3F-9E7E-217F1FF97208 +RRULE:FREQ=MONTHLY;BYMONTHDAY=1;WKST=SU;COUNT=25 +DTSTART;VALUE=DATE:20180701 +DTEND;VALUE=DATE:20180702 +SUMMARY:Monthly +LAST-MODIFIED:20110429T222101Z +DTSTAMP:20170630T105724Z +SEQUENCE:0 +END:VEVENT +END:VCALENDAR diff --git a/lib/composer/vendor/johngrogg/ics-parser/tests/ical/issue-196.ics b/lib/composer/vendor/johngrogg/ics-parser/tests/ical/issue-196.ics new file mode 100644 index 0000000..3ffcb18 --- /dev/null +++ b/lib/composer/vendor/johngrogg/ics-parser/tests/ical/issue-196.ics @@ -0,0 +1,64 @@ +BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +X-WR-CALNAME:Test-Calendar +X-WR-TIMEZONE:Europe/Berlin +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20180101T152047Z +LAST-MODIFIED:20181202T202056Z +DTSTAMP:20181202T202056Z +UID:529b1ea3-8de8-484d-b878-c20c7fb72bf5 +SUMMARY:test +RRULE:FREQ=DAILY;UNTIL=20191111T180000Z +DTSTART;TZID=Europe/Berlin:20191105T190000 +DTEND;TZID=Europe/Berlin:20191105T220000 +TRANSP:OPAQUE +SEQUENCE:24 +X-MOZ-GENERATION:37 +END:VEVENT +BEGIN:VEVENT +CREATED:20181202T202042Z +LAST-MODIFIED:20181202T202053Z +DTSTAMP:20181202T202053Z +UID:529b1ea3-8de8-484d-b878-c20c7fb72bf5 +SUMMARY:test +RECURRENCE-ID;TZID=Europe/Berlin:20191109T190000 +DTSTART;TZID=Europe/Berlin:20191109T170000 +DTEND;TZID=Europe/Berlin:20191109T220000 +TRANSP:OPAQUE +SEQUENCE:25 +X-MOZ-GENERATION:37 +DURATION:PT0S +END:VEVENT +BEGIN:VEVENT +CREATED:20181202T202053Z +LAST-MODIFIED:20181202T202056Z +DTSTAMP:20181202T202056Z +UID:529b1ea3-8de8-484d-b878-c20c7fb72bf5 +SUMMARY:test +RECURRENCE-ID;TZID=Europe/Berlin:20191110T190000 +DTSTART;TZID=Europe/Berlin:20191110T180000 +DTEND;TZID=Europe/Berlin:20191110T220000 +TRANSP:OPAQUE +SEQUENCE:25 +X-MOZ-GENERATION:37 +DURATION:PT0S +END:VEVENT +END:VCALENDAR diff --git a/lib/composer/vendor/psr/log/LICENSE b/lib/composer/vendor/psr/log/LICENSE new file mode 100644 index 0000000..474c952 --- /dev/null +++ b/lib/composer/vendor/psr/log/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2012 PHP Framework Interoperability Group + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/lib/composer/vendor/psr/log/README.md b/lib/composer/vendor/psr/log/README.md new file mode 100644 index 0000000..a9f20c4 --- /dev/null +++ b/lib/composer/vendor/psr/log/README.md @@ -0,0 +1,58 @@ +PSR Log +======= + +This repository holds all interfaces/classes/traits related to +[PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md). + +Note that this is not a logger of its own. It is merely an interface that +describes a logger. See the specification for more details. + +Installation +------------ + +```bash +composer require psr/log +``` + +Usage +----- + +If you need a logger, you can use the interface like this: + +```php +logger = $logger; + } + + public function doSomething() + { + if ($this->logger) { + $this->logger->info('Doing work'); + } + + try { + $this->doSomethingElse(); + } catch (Exception $exception) { + $this->logger->error('Oh no!', array('exception' => $exception)); + } + + // do something useful + } +} +``` + +You can then pick one of the implementations of the interface to get a logger. + +If you want to implement the interface, you can require this package and +implement `Psr\Log\LoggerInterface` in your code. Please read the +[specification text](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md) +for details. diff --git a/lib/composer/vendor/psr/log/composer.json b/lib/composer/vendor/psr/log/composer.json new file mode 100644 index 0000000..879fc6f --- /dev/null +++ b/lib/composer/vendor/psr/log/composer.json @@ -0,0 +1,26 @@ +{ + "name": "psr/log", + "description": "Common interface for logging libraries", + "keywords": ["psr", "psr-3", "log"], + "homepage": "https://github.com/php-fig/log", + "license": "MIT", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "require": { + "php": ">=8.0.0" + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + } +} diff --git a/lib/composer/vendor/psr/log/src/AbstractLogger.php b/lib/composer/vendor/psr/log/src/AbstractLogger.php new file mode 100644 index 0000000..d60a091 --- /dev/null +++ b/lib/composer/vendor/psr/log/src/AbstractLogger.php @@ -0,0 +1,15 @@ +logger = $logger; + } +} diff --git a/lib/composer/vendor/psr/log/src/LoggerInterface.php b/lib/composer/vendor/psr/log/src/LoggerInterface.php new file mode 100644 index 0000000..cb4cf64 --- /dev/null +++ b/lib/composer/vendor/psr/log/src/LoggerInterface.php @@ -0,0 +1,98 @@ +log(LogLevel::EMERGENCY, $message, $context); + } + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + */ + public function alert(string|\Stringable $message, array $context = []): void + { + $this->log(LogLevel::ALERT, $message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + */ + public function critical(string|\Stringable $message, array $context = []): void + { + $this->log(LogLevel::CRITICAL, $message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + */ + public function error(string|\Stringable $message, array $context = []): void + { + $this->log(LogLevel::ERROR, $message, $context); + } + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + */ + public function warning(string|\Stringable $message, array $context = []): void + { + $this->log(LogLevel::WARNING, $message, $context); + } + + /** + * Normal but significant events. + */ + public function notice(string|\Stringable $message, array $context = []): void + { + $this->log(LogLevel::NOTICE, $message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + */ + public function info(string|\Stringable $message, array $context = []): void + { + $this->log(LogLevel::INFO, $message, $context); + } + + /** + * Detailed debug information. + */ + public function debug(string|\Stringable $message, array $context = []): void + { + $this->log(LogLevel::DEBUG, $message, $context); + } + + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * + * @throws \Psr\Log\InvalidArgumentException + */ + abstract public function log($level, string|\Stringable $message, array $context = []): void; +} diff --git a/lib/composer/vendor/psr/log/src/NullLogger.php b/lib/composer/vendor/psr/log/src/NullLogger.php new file mode 100644 index 0000000..de0561e --- /dev/null +++ b/lib/composer/vendor/psr/log/src/NullLogger.php @@ -0,0 +1,26 @@ +logger) { }` + * blocks. + */ +class NullLogger extends AbstractLogger +{ + /** + * Logs with an arbitrary level. + * + * @param mixed[] $context + * + * @throws \Psr\Log\InvalidArgumentException + */ + public function log($level, string|\Stringable $message, array $context = []): void + { + // noop + } +} diff --git a/lib/composer/vendor/sabre/dav/LICENSE b/lib/composer/vendor/sabre/dav/LICENSE new file mode 100644 index 0000000..fd3539e --- /dev/null +++ b/lib/composer/vendor/sabre/dav/LICENSE @@ -0,0 +1,27 @@ +Copyright (C) 2007-2016 fruux GmbH (https://fruux.com/). + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of SabreDAV nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/composer/vendor/sabre/dav/README.md b/lib/composer/vendor/sabre/dav/README.md new file mode 100644 index 0000000..32ca1c3 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/README.md @@ -0,0 +1,39 @@ +![sabre'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/composer/vendor/sabre/dav/bin/build.php b/lib/composer/vendor/sabre/dav/bin/build.php new file mode 100755 index 0000000..4dd25d9 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/bin/build.php @@ -0,0 +1,169 @@ +#!/usr/bin/env php + [ + 'init', 'test', 'clean', + ], + 'markrelease' => [ + 'init', 'test', 'clean', + ], + 'clean' => [], + 'test' => [ + 'composerupdate', + ], + 'init' => [], + 'composerupdate' => [], + ]; + +$default = 'buildzip'; + +$baseDir = __DIR__.'/../'; +chdir($baseDir); + +$currentTask = $default; +if ($argc > 1) { + $currentTask = $argv[1]; +} +$version = null; +if ($argc > 2) { + $version = $argv[2]; +} + +if (!isset($tasks[$currentTask])) { + echo 'Task not found: ', $currentTask, "\n"; + exit(1); +} + +// Creating the dependency graph +$newTaskList = []; +$oldTaskList = [$currentTask => true]; + +while (count($oldTaskList) > 0) { + foreach ($oldTaskList as $task => $foo) { + if (!isset($tasks[$task])) { + echo 'Dependency not found: '.$task, "\n"; + exit(1); + } + $dependencies = $tasks[$task]; + + $fullFilled = true; + foreach ($dependencies as $dependency) { + if (isset($newTaskList[$dependency])) { + // Already in the fulfilled task list. + continue; + } else { + $oldTaskList[$dependency] = true; + $fullFilled = false; + } + } + if ($fullFilled) { + unset($oldTaskList[$task]); + $newTaskList[$task] = 1; + } + } +} + +foreach (array_keys($newTaskList) as $task) { + echo 'task: '.$task, "\n"; + call_user_func($task); + echo "\n"; +} + +function init() +{ + global $version; + if (!$version) { + include __DIR__.'/../vendor/autoload.php'; + $version = Sabre\DAV\Version::VERSION; + } + + echo ' Building sabre/dav '.$version, "\n"; +} + +function clean() +{ + global $baseDir; + echo " Removing build files\n"; + $outputDir = $baseDir.'/build/SabreDAV'; + if (is_dir($outputDir)) { + system('rm -r '.$baseDir.'/build/SabreDAV'); + } +} + +function composerupdate() +{ + global $baseDir; + echo " Updating composer packages to latest version\n\n"; + system('cd '.$baseDir.'; composer update'); +} + +function test() +{ + global $baseDir; + + echo " Running all unittests.\n"; + echo " This may take a while.\n\n"; + system(__DIR__.'/phpunit --configuration '.$baseDir.'/tests/phpunit.xml.dist --stop-on-failure', $code); + if (0 != $code) { + echo "PHPUnit reported error code $code\n"; + exit(1); + } +} + +function buildzip() +{ + global $baseDir, $version; + echo " Generating composer.json\n"; + + $input = json_decode(file_get_contents(__DIR__.'/../composer.json'), true); + $newComposer = [ + 'require' => $input['require'], + 'config' => [ + 'bin-dir' => './bin', + ], + 'prefer-stable' => true, + 'minimum-stability' => 'alpha', + ]; + unset( + $newComposer['require']['sabre/vobject'], + $newComposer['require']['sabre/http'], + $newComposer['require']['sabre/uri'], + $newComposer['require']['sabre/event'] + ); + $newComposer['require']['sabre/dav'] = $version; + mkdir('build/SabreDAV'); + file_put_contents('build/SabreDAV/composer.json', json_encode($newComposer, JSON_PRETTY_PRINT)); + + echo " Downloading dependencies\n"; + system('cd build/SabreDAV; composer install -n', $code); + if (0 !== $code) { + echo "Composer reported error code $code\n"; + exit(1); + } + + echo " Removing pointless files\n"; + unlink('build/SabreDAV/composer.json'); + unlink('build/SabreDAV/composer.lock'); + + echo " Moving important files to the root of the project\n"; + + $fileNames = [ + 'CHANGELOG.md', + 'LICENSE', + 'README.md', + 'examples', + ]; + foreach ($fileNames as $fileName) { + echo " $fileName\n"; + rename('build/SabreDAV/vendor/sabre/dav/'.$fileName, 'build/SabreDAV/'.$fileName); + } + + // + + echo "\n"; + echo "Zipping the sabredav distribution\n\n"; + system('cd build; zip -qr sabredav-'.$version.'.zip SabreDAV'); + + echo 'Done.'; +} diff --git a/lib/composer/vendor/sabre/dav/bin/migrateto20.php b/lib/composer/vendor/sabre/dav/bin/migrateto20.php new file mode 100755 index 0000000..fb24fe5 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/bin/migrateto20.php @@ -0,0 +1,414 @@ +#!/usr/bin/env php +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); +$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); + +$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME); + +switch ($driver) { + case 'mysql': + echo "Detected MySQL.\n"; + break; + case 'sqlite': + echo "Detected SQLite.\n"; + break; + default: + echo 'Error: unsupported driver: '.$driver."\n"; + exit(-1); +} + +foreach (['calendar', 'addressbook'] as $itemType) { + $tableName = $itemType.'s'; + $tableNameOld = $tableName.'_old'; + $changesTable = $itemType.'changes'; + + echo "Upgrading '$tableName'\n"; + + // The only cross-db way to do this, is to just fetch a single record. + $row = $pdo->query("SELECT * FROM $tableName LIMIT 1")->fetch(); + + if (!$row) { + echo "No records were found in the '$tableName' table.\n"; + echo "\n"; + echo "We're going to rename the old table to $tableNameOld (just in case).\n"; + echo "and re-create the new table.\n"; + + switch ($driver) { + case 'mysql': + $pdo->exec("RENAME TABLE $tableName TO $tableNameOld"); + switch ($itemType) { + case 'calendar': + $pdo->exec(" + CREATE TABLE calendars ( + id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + principaluri VARCHAR(100), + displayname VARCHAR(100), + uri VARCHAR(200), + synctoken INT(11) UNSIGNED NOT NULL DEFAULT '1', + description TEXT, + calendarorder INT(11) UNSIGNED NOT NULL DEFAULT '0', + calendarcolor VARCHAR(10), + timezone TEXT, + components VARCHAR(20), + transparent TINYINT(1) NOT NULL DEFAULT '0', + UNIQUE(principaluri, uri) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + "); + break; + case 'addressbook': + $pdo->exec(" + CREATE TABLE addressbooks ( + id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + principaluri VARCHAR(255), + displayname VARCHAR(255), + uri VARCHAR(200), + description TEXT, + synctoken INT(11) UNSIGNED NOT NULL DEFAULT '1', + UNIQUE(principaluri, uri) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + "); + break; + } + break; + + case 'sqlite': + $pdo->exec("ALTER TABLE $tableName RENAME TO $tableNameOld"); + + switch ($itemType) { + case 'calendar': + $pdo->exec(' + CREATE TABLE calendars ( + id integer primary key asc, + principaluri text, + displayname text, + uri text, + synctoken integer, + description text, + calendarorder integer, + calendarcolor text, + timezone text, + components text, + transparent bool + ); + '); + break; + case 'addressbook': + $pdo->exec(' + CREATE TABLE addressbooks ( + id integer primary key asc, + principaluri text, + displayname text, + uri text, + description text, + synctoken integer + ); + '); + + break; + } + break; + } + echo "Creation of 2.0 $tableName table is complete\n"; + } else { + // Checking if there's a synctoken field already. + if (array_key_exists('synctoken', $row)) { + echo "The 'synctoken' field already exists in the $tableName table.\n"; + echo "It's likely you already upgraded, so we're simply leaving\n"; + echo "the $tableName table alone\n"; + } else { + echo "1.8 table schema detected\n"; + switch ($driver) { + case 'mysql': + $pdo->exec("ALTER TABLE $tableName ADD synctoken INT(11) UNSIGNED NOT NULL DEFAULT '1'"); + $pdo->exec("ALTER TABLE $tableName DROP ctag"); + $pdo->exec("UPDATE $tableName SET synctoken = '1'"); + break; + case 'sqlite': + $pdo->exec("ALTER TABLE $tableName ADD synctoken integer"); + $pdo->exec("UPDATE $tableName SET synctoken = '1'"); + echo "Note: there's no easy way to remove fields in sqlite.\n"; + echo "The ctag field is no longer used, but it's kept in place\n"; + break; + } + + echo "Upgraded '$tableName' to 2.0 schema.\n"; + } + } + + try { + $pdo->query("SELECT * FROM $changesTable LIMIT 1"); + + echo "'$changesTable' already exists. Assuming that this part of the\n"; + echo "upgrade was already completed.\n"; + } catch (Exception $e) { + echo "Creating '$changesTable' table.\n"; + + switch ($driver) { + case 'mysql': + $pdo->exec(" + CREATE TABLE $changesTable ( + id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + uri VARCHAR(200) NOT NULL, + synctoken INT(11) UNSIGNED NOT NULL, + {$itemType}id INT(11) UNSIGNED NOT NULL, + operation TINYINT(1) NOT NULL, + INDEX {$itemType}id_synctoken ({$itemType}id, synctoken) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + + "); + break; + case 'sqlite': + $pdo->exec(" + + CREATE TABLE $changesTable ( + id integer primary key asc, + uri text, + synctoken integer, + {$itemType}id integer, + operation bool + ); + + "); + $pdo->exec("CREATE INDEX {$itemType}id_synctoken ON $changesTable ({$itemType}id, synctoken);"); + break; + } + } +} + +try { + $pdo->query('SELECT * FROM calendarsubscriptions LIMIT 1'); + + echo "'calendarsubscriptions' already exists. Assuming that this part of the\n"; + echo "upgrade was already completed.\n"; +} catch (Exception $e) { + echo "Creating calendarsubscriptions table.\n"; + + switch ($driver) { + case 'mysql': + $pdo->exec(" +CREATE TABLE calendarsubscriptions ( + id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + uri VARCHAR(200) NOT NULL, + principaluri VARCHAR(100) NOT NULL, + source TEXT, + displayname VARCHAR(100), + refreshrate VARCHAR(10), + calendarorder INT(11) UNSIGNED NOT NULL DEFAULT '0', + calendarcolor VARCHAR(10), + striptodos TINYINT(1) NULL, + stripalarms TINYINT(1) NULL, + stripattachments TINYINT(1) NULL, + lastmodified INT(11) UNSIGNED, + UNIQUE(principaluri, uri) +); + "); + break; + case 'sqlite': + $pdo->exec(' + +CREATE TABLE calendarsubscriptions ( + id integer primary key asc, + uri text, + principaluri text, + source text, + displayname text, + refreshrate text, + calendarorder integer, + calendarcolor text, + striptodos bool, + stripalarms bool, + stripattachments bool, + lastmodified int +); + '); + + $pdo->exec('CREATE INDEX principaluri_uri ON calendarsubscriptions (principaluri, uri);'); + break; + } +} + +try { + $pdo->query('SELECT * FROM propertystorage LIMIT 1'); + + echo "'propertystorage' already exists. Assuming that this part of the\n"; + echo "upgrade was already completed.\n"; +} catch (Exception $e) { + echo "Creating propertystorage table.\n"; + + switch ($driver) { + case 'mysql': + $pdo->exec(' +CREATE TABLE propertystorage ( + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + path VARBINARY(1024) NOT NULL, + name VARBINARY(100) NOT NULL, + value MEDIUMBLOB +); + '); + $pdo->exec(' +CREATE UNIQUE INDEX path_property ON propertystorage (path(600), name(100)); + '); + break; + case 'sqlite': + $pdo->exec(' +CREATE TABLE propertystorage ( + id integer primary key asc, + path TEXT, + name TEXT, + value TEXT +); + '); + $pdo->exec(' +CREATE UNIQUE INDEX path_property ON propertystorage (path, name); + '); + + break; + } +} + +echo "Upgrading cards table to 2.0 schema\n"; + +try { + $create = false; + $row = $pdo->query('SELECT * FROM cards LIMIT 1')->fetch(); + if (!$row) { + $random = mt_rand(1000, 9999); + echo "There was no data in the cards table, so we're re-creating it\n"; + echo "The old table will be renamed to cards_old$random, just in case.\n"; + + $create = true; + + switch ($driver) { + case 'mysql': + $pdo->exec("RENAME TABLE cards TO cards_old$random"); + break; + case 'sqlite': + $pdo->exec("ALTER TABLE cards RENAME TO cards_old$random"); + break; + } + } +} catch (Exception $e) { + echo "Exception while checking cards table. Assuming that the table does not yet exist.\n"; + echo 'Debug: ', $e->getMessage(), "\n"; + $create = true; +} + +if ($create) { + switch ($driver) { + case 'mysql': + $pdo->exec(' +CREATE TABLE cards ( + id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + addressbookid INT(11) UNSIGNED NOT NULL, + carddata MEDIUMBLOB, + uri VARCHAR(200), + lastmodified INT(11) UNSIGNED, + etag VARBINARY(32), + size INT(11) UNSIGNED NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + + '); + break; + + case 'sqlite': + $pdo->exec(' +CREATE TABLE cards ( + id integer primary key asc, + addressbookid integer, + carddata blob, + uri text, + lastmodified integer, + etag text, + size integer +); + '); + break; + } +} else { + switch ($driver) { + case 'mysql': + $pdo->exec(' + ALTER TABLE cards + ADD etag VARBINARY(32), + ADD size INT(11) UNSIGNED NOT NULL; + '); + break; + + case 'sqlite': + $pdo->exec(' + ALTER TABLE cards ADD etag text; + ALTER TABLE cards ADD size integer; + '); + break; + } + echo "Reading all old vcards and populating etag and size fields.\n"; + $result = $pdo->query('SELECT id, carddata FROM cards'); + $stmt = $pdo->prepare('UPDATE cards SET etag = ?, size = ? WHERE id = ?'); + while ($row = $result->fetch(\PDO::FETCH_ASSOC)) { + $stmt->execute([ + md5($row['carddata']), + strlen($row['carddata']), + $row['id'], + ]); + } +} + +echo "Upgrade to 2.0 schema completed.\n"; diff --git a/lib/composer/vendor/sabre/dav/bin/migrateto21.php b/lib/composer/vendor/sabre/dav/bin/migrateto21.php new file mode 100755 index 0000000..2c15b0a --- /dev/null +++ b/lib/composer/vendor/sabre/dav/bin/migrateto21.php @@ -0,0 +1,166 @@ +#!/usr/bin/env php +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); +$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); + +$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME); + +switch ($driver) { + case 'mysql': + echo "Detected MySQL.\n"; + break; + case 'sqlite': + echo "Detected SQLite.\n"; + break; + default: + echo 'Error: unsupported driver: '.$driver."\n"; + exit(-1); +} + +echo "Upgrading 'calendarobjects'\n"; +$addUid = false; +try { + $result = $pdo->query('SELECT * FROM calendarobjects LIMIT 1'); + $row = $result->fetch(\PDO::FETCH_ASSOC); + + if (!$row) { + echo "No data in table. Going to try to add the uid field anyway.\n"; + $addUid = true; + } elseif (array_key_exists('uid', $row)) { + echo "uid field exists. Assuming that this part of the migration has\n"; + echo "Already been completed.\n"; + } else { + echo "2.0 schema detected.\n"; + $addUid = true; + } +} catch (Exception $e) { + echo "Could not find a calendarobjects table. Skipping this part of the\n"; + echo "upgrade.\n"; +} + +if ($addUid) { + switch ($driver) { + case 'mysql': + $pdo->exec('ALTER TABLE calendarobjects ADD uid VARCHAR(200)'); + break; + case 'sqlite': + $pdo->exec('ALTER TABLE calendarobjects ADD uid TEXT'); + break; + } + + $result = $pdo->query('SELECT id, calendardata FROM calendarobjects'); + $stmt = $pdo->prepare('UPDATE calendarobjects SET uid = ? WHERE id = ?'); + $counter = 0; + + while ($row = $result->fetch(\PDO::FETCH_ASSOC)) { + try { + $vobj = \Sabre\VObject\Reader::read($row['calendardata']); + } catch (\Exception $e) { + echo "Warning! Item with id $row[id] could not be parsed!\n"; + continue; + } + $uid = null; + $item = $vobj->getBaseComponent(); + if (!isset($item->UID)) { + echo "Warning! Item with id $item[id] does NOT have a UID property and this is required.\n"; + continue; + } + $uid = (string) $item->UID; + $stmt->execute([$uid, $row['id']]); + ++$counter; + } +} + +echo "Creating 'schedulingobjects'\n"; + +switch ($driver) { + case 'mysql': + $pdo->exec('CREATE TABLE IF NOT EXISTS schedulingobjects +( + id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + principaluri VARCHAR(255), + calendardata MEDIUMBLOB, + uri VARCHAR(200), + lastmodified INT(11) UNSIGNED, + etag VARCHAR(32), + size INT(11) UNSIGNED NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + '); + break; + + case 'sqlite': + $pdo->exec('CREATE TABLE IF NOT EXISTS schedulingobjects ( + id integer primary key asc, + principaluri text, + calendardata blob, + uri text, + lastmodified integer, + etag text, + size integer +) +'); + break; +} + +echo "Done.\n"; + +echo "Upgrade to 2.1 schema completed.\n"; diff --git a/lib/composer/vendor/sabre/dav/bin/migrateto30.php b/lib/composer/vendor/sabre/dav/bin/migrateto30.php new file mode 100755 index 0000000..9798cad --- /dev/null +++ b/lib/composer/vendor/sabre/dav/bin/migrateto30.php @@ -0,0 +1,161 @@ +#!/usr/bin/env php +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); +$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); + +$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME); + +switch ($driver) { + case 'mysql': + echo "Detected MySQL.\n"; + break; + case 'sqlite': + echo "Detected SQLite.\n"; + break; + default: + echo 'Error: unsupported driver: '.$driver."\n"; + exit(-1); +} + +echo "Upgrading 'propertystorage'\n"; +$addValueType = false; +try { + $result = $pdo->query('SELECT * FROM propertystorage LIMIT 1'); + $row = $result->fetch(\PDO::FETCH_ASSOC); + + if (!$row) { + echo "No data in table. Going to re-create the table.\n"; + $random = mt_rand(1000, 9999); + echo "Renaming propertystorage -> propertystorage_old$random and creating new table.\n"; + + switch ($driver) { + case 'mysql': + $pdo->exec('RENAME TABLE propertystorage TO propertystorage_old'.$random); + $pdo->exec(' + CREATE TABLE propertystorage ( + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + path VARBINARY(1024) NOT NULL, + name VARBINARY(100) NOT NULL, + valuetype INT UNSIGNED, + value MEDIUMBLOB + ); + '); + $pdo->exec('CREATE UNIQUE INDEX path_property_'.$random.' ON propertystorage (path(600), name(100));'); + break; + case 'sqlite': + $pdo->exec('ALTER TABLE propertystorage RENAME TO propertystorage_old'.$random); + $pdo->exec(' +CREATE TABLE propertystorage ( + id integer primary key asc, + path text, + name text, + valuetype integer, + value blob +);'); + + $pdo->exec('CREATE UNIQUE INDEX path_property_'.$random.' ON propertystorage (path, name);'); + break; + } + } elseif (array_key_exists('valuetype', $row)) { + echo "valuetype field exists. Assuming that this part of the migration has\n"; + echo "Already been completed.\n"; + } else { + echo "2.1 schema detected. Going to perform upgrade.\n"; + $addValueType = true; + } +} catch (Exception $e) { + echo "Could not find a propertystorage table. Skipping this part of the\n"; + echo "upgrade.\n"; + echo $e->getMessage(), "\n"; +} + +if ($addValueType) { + switch ($driver) { + case 'mysql': + $pdo->exec('ALTER TABLE propertystorage ADD valuetype INT UNSIGNED'); + break; + case 'sqlite': + $pdo->exec('ALTER TABLE propertystorage ADD valuetype INT'); + + break; + } + + $pdo->exec('UPDATE propertystorage SET valuetype = 1 WHERE valuetype IS NULL '); +} + +echo "Migrating vcardurl\n"; + +$result = $pdo->query('SELECT id, uri, vcardurl FROM principals WHERE vcardurl IS NOT NULL'); +$stmt1 = $pdo->prepare('INSERT INTO propertystorage (path, name, valuetype, value) VALUES (?, ?, 3, ?)'); + +while ($row = $result->fetch(\PDO::FETCH_ASSOC)) { + // Inserting the new record + $stmt1->execute([ + 'addressbooks/'.basename($row['uri']), + '{http://calendarserver.org/ns/}me-card', + serialize(new Sabre\DAV\Xml\Property\Href($row['vcardurl'])), + ]); + + echo serialize(new Sabre\DAV\Xml\Property\Href($row['vcardurl'])); +} + +echo "Done.\n"; +echo "Upgrade to 3.0 schema completed.\n"; diff --git a/lib/composer/vendor/sabre/dav/bin/migrateto32.php b/lib/composer/vendor/sabre/dav/bin/migrateto32.php new file mode 100755 index 0000000..09ac55d --- /dev/null +++ b/lib/composer/vendor/sabre/dav/bin/migrateto32.php @@ -0,0 +1,258 @@ +#!/usr/bin/env php +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); +$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); + +$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME); + +switch ($driver) { + case 'mysql': + echo "Detected MySQL.\n"; + break; + case 'sqlite': + echo "Detected SQLite.\n"; + break; + default: + echo 'Error: unsupported driver: '.$driver."\n"; + exit(-1); +} + +echo "Creating 'calendarinstances'\n"; +$addValueType = false; +try { + $result = $pdo->query('SELECT * FROM calendarinstances LIMIT 1'); + $result->fetch(\PDO::FETCH_ASSOC); + echo "calendarinstances exists. Assuming this part of the migration has already been done.\n"; +} catch (Exception $e) { + echo "calendarinstances does not yet exist. Creating table and migrating data.\n"; + + switch ($driver) { + case 'mysql': + $pdo->exec(<<exec(' +INSERT INTO calendarinstances + ( + calendarid, + principaluri, + access, + displayname, + uri, + description, + calendarorder, + calendarcolor, + transparent + ) +SELECT + id, + principaluri, + 1, + displayname, + uri, + description, + calendarorder, + calendarcolor, + transparent +FROM calendars +'); + break; + case 'sqlite': + $pdo->exec(<<exec(' +INSERT INTO calendarinstances + ( + calendarid, + principaluri, + access, + displayname, + uri, + description, + calendarorder, + calendarcolor, + transparent + ) +SELECT + id, + principaluri, + 1, + displayname, + uri, + description, + calendarorder, + calendarcolor, + transparent +FROM calendars +'); + break; + } +} +try { + $result = $pdo->query('SELECT * FROM calendars LIMIT 1'); + $row = $result->fetch(\PDO::FETCH_ASSOC); + + if (!$row) { + echo "Source table is empty.\n"; + $migrateCalendars = true; + } + + $columnCount = count($row); + if (3 === $columnCount) { + echo "The calendars table has 3 columns already. Assuming this part of the migration was already done.\n"; + $migrateCalendars = false; + } else { + echo 'The calendars table has '.$columnCount." columns.\n"; + $migrateCalendars = true; + } +} catch (Exception $e) { + echo "calendars table does not exist. This is a major problem. Exiting.\n"; + exit(-1); +} + +if ($migrateCalendars) { + $calendarBackup = 'calendars_3_1_'.$backupPostfix; + echo "Backing up 'calendars' to '", $calendarBackup, "'\n"; + + switch ($driver) { + case 'mysql': + $pdo->exec('RENAME TABLE calendars TO '.$calendarBackup); + break; + case 'sqlite': + $pdo->exec('ALTER TABLE calendars RENAME TO '.$calendarBackup); + break; + } + + echo "Creating new calendars table.\n"; + switch ($driver) { + case 'mysql': + $pdo->exec(<<exec(<<exec(<<0): + print "Bytes to go before we hit threshold:", bytes + else: + print "Threshold exceeded with:", -bytes, "bytes" + dir = os.listdir(cacheDir) + dir2 = [] + for file in dir: + path = cacheDir + '/' + file + dir2.append({ + "path" : path, + "atime": os.stat(path).st_atime, + "size" : os.stat(path).st_size + }) + + dir2.sort(lambda x,y: int(x["atime"]-y["atime"])) + + filesunlinked = 0 + gainedspace = 0 + + # Left is the amount of bytes that need to be freed up + # The default is the 'min_erase setting' + left = min_erase + + # If the min_erase setting is lower than the amount of bytes over + # the threshold, we use that number instead. + if left < -bytes : + left = -bytes + + print "Need to delete at least:", left; + + for file in dir2: + + # Only deleting files if we're not simulating + if not simulate: os.unlink(file["path"]) + left = int(left - file["size"]) + gainedspace = gainedspace + file["size"] + filesunlinked = filesunlinked + 1 + + if(left<0): + break + + print "%d files deleted (%d bytes)" % (filesunlinked, gainedspace) + + + time.sleep(sleep) + + + +def main(): + parser = OptionParser( + version="naturalselection v0.3", + description="Cache directory manager. Deletes cache entries based on accesstime and free space thresholds.\n" + + "This utility is distributed alongside SabreDAV.", + usage="usage: %prog [options] cacheDirectory", + ) + parser.add_option( + '-s', + dest="simulate", + action="store_true", + help="Don't actually make changes, but just simulate the behaviour", + ) + parser.add_option( + '-r','--runs', + help="How many times to check before exiting. -1 is infinite, which is the default", + type="int", + dest="runs", + default=-1 + ) + parser.add_option( + '-n','--interval', + help="Sleep time in seconds (default = 5)", + type="int", + dest="sleep", + default=5 + ) + parser.add_option( + '-l','--threshold', + help="Threshold in bytes (default = 10737418240, which is 10GB)", + type="int", + dest="threshold", + default=10737418240 + ) + parser.add_option( + '-m', '--min-erase', + help="Minimum number of bytes to erase when the threshold is reached. " + + "Setting this option higher will reduce the number of times the cache directory will need to be scanned. " + + "(the default is 1073741824, which is 1GB.)", + type="int", + dest="min_erase", + default=1073741824 + ) + + options,args = parser.parse_args() + if len(args)<1: + parser.error("This utility requires at least 1 argument") + cacheDir = args[0] + + print "Natural Selection" + print "Cache directory:", cacheDir + free = getfreespace(cacheDir); + print "Current free disk space:", free + + runs = options.runs; + while runs!=0 : + run( + cacheDir, + sleep=options.sleep, + simulate=options.simulate, + threshold=options.threshold, + min_erase=options.min_erase + ) + if runs>0: + runs = runs - 1 + +if __name__ == '__main__' : + main() diff --git a/lib/composer/vendor/sabre/dav/bin/sabredav b/lib/composer/vendor/sabre/dav/bin/sabredav new file mode 100755 index 0000000..032371b --- /dev/null +++ b/lib/composer/vendor/sabre/dav/bin/sabredav @@ -0,0 +1,2 @@ +#!/bin/sh +php -S 0.0.0.0:8080 `dirname $0`/sabredav.php diff --git a/lib/composer/vendor/sabre/dav/bin/sabredav.php b/lib/composer/vendor/sabre/dav/bin/sabredav.php new file mode 100755 index 0000000..71047b8 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/bin/sabredav.php @@ -0,0 +1,51 @@ +stream = fopen('php://stdout', 'w'); + } + + public function log($msg) + { + fwrite($this->stream, $msg."\n"); + } +} + +$log = new CliLog(); + +if ('cli-server' !== php_sapi_name()) { + exit('This script is intended to run on the built-in php webserver'); +} + +// Finding composer + +$paths = [ + __DIR__.'/../vendor/autoload.php', + __DIR__.'/../../../autoload.php', +]; + +foreach ($paths as $path) { + if (file_exists($path)) { + include $path; + break; + } +} + +use Sabre\DAV; + +// Root +$root = new DAV\FS\Directory(getcwd()); + +// Setting up server. +$server = new DAV\Server($root); + +// Browser plugin +$server->addPlugin(new DAV\Browser\Plugin()); + +$server->exec(); diff --git a/lib/composer/vendor/sabre/dav/composer.json b/lib/composer/vendor/sabre/dav/composer.json new file mode 100644 index 0000000..3154173 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/composer.json @@ -0,0 +1,81 @@ +{ + "name": "sabre/dav", + "type": "library", + "description": "WebDAV Framework for PHP", + "keywords": ["Framework", "WebDAV", "CalDAV", "CardDAV", "iCalendar"], + "homepage": "http://sabre.io/", + "license" : "BSD-3-Clause", + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage" : "http://evertpot.com/", + "role" : "Developer" + } + ], + "require": { + "php": "^7.1.0 || ^8.0", + "sabre/vobject": "^4.2.1", + "sabre/event" : "^5.0", + "sabre/xml" : "^2.0.1", + "sabre/http" : "^5.0.5", + "sabre/uri" : "^2.0", + "ext-dom": "*", + "ext-pcre": "*", + "ext-spl": "*", + "ext-simplexml": "*", + "ext-mbstring" : "*", + "ext-ctype" : "*", + "ext-date" : "*", + "ext-iconv" : "*", + "lib-libxml" : ">=2.7.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "ext-json": "*" + }, + "require-dev" : { + "friendsofphp/php-cs-fixer": "^2.19", + "monolog/monolog": "^1.27 || ^2.0", + "phpstan/phpstan": "^0.12 || ^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "suggest" : { + "ext-curl" : "*", + "ext-pdo" : "*", + "ext-imap": "*" + }, + "autoload": { + "psr-4" : { + "Sabre\\" : "lib/" + } + }, + "autoload-dev" : { + "psr-4" : { + "Sabre\\" : "tests/Sabre/" + } + }, + "support" : { + "forum" : "https://groups.google.com/group/sabredav-discuss", + "source" : "https://github.com/fruux/sabre-dav" + }, + "bin" : [ + "bin/sabredav", + "bin/naturalselection" + ], + "scripts": { + "phpstan": [ + "phpstan analyse lib tests" + ], + "cs-fixer": [ + "php-cs-fixer fix" + ], + "phpunit": [ + "phpunit --configuration tests/phpunit.xml" + ], + "test": [ + "composer phpstan", + "composer cs-fixer", + "composer phpunit" + ] + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php new file mode 100644 index 0000000..c761bff --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php @@ -0,0 +1,216 @@ +getCalendarObject($calendarId, $uri); + }, $uris); + } + + /** + * Performs a calendar-query on the contents of this calendar. + * + * The calendar-query is defined in RFC4791 : CalDAV. Using the + * calendar-query it is possible for a client to request a specific set of + * object, based on contents of iCalendar properties, date-ranges and + * iCalendar component types (VTODO, VEVENT). + * + * This method should just return a list of (relative) urls that match this + * query. + * + * The list of filters are specified as an array. The exact array is + * documented by \Sabre\CalDAV\CalendarQueryParser. + * + * Note that it is extremely likely that getCalendarObject for every path + * returned from this method will be called almost immediately after. You + * may want to anticipate this to speed up these requests. + * + * This method provides a default implementation, which parses *all* the + * iCalendar objects in the specified calendar. + * + * This default may well be good enough for personal use, and calendars + * that aren't very large. But if you anticipate high usage, big calendars + * or high loads, you are strongly advised to optimize certain paths. + * + * The best way to do so is override this method and to optimize + * specifically for 'common filters'. + * + * Requests that are extremely common are: + * * requests for just VEVENTS + * * requests for just VTODO + * * requests with a time-range-filter on either VEVENT or VTODO. + * + * ..and combinations of these requests. It may not be worth it to try to + * handle every possible situation and just rely on the (relatively + * easy to use) CalendarQueryValidator to handle the rest. + * + * Note that especially time-range-filters may be difficult to parse. A + * time-range filter specified on a VEVENT must for instance also handle + * recurrence rules correctly. + * A good example of how to interpret all these filters can also simply + * be found in \Sabre\CalDAV\CalendarQueryFilter. This class is as correct + * as possible, so it gives you a good idea on what type of stuff you need + * to think of. + * + * @param mixed $calendarId + * + * @return array + */ + public function calendarQuery($calendarId, array $filters) + { + $result = []; + $objects = $this->getCalendarObjects($calendarId); + + foreach ($objects as $object) { + if ($this->validateFilterForObject($object, $filters)) { + $result[] = $object['uri']; + } + } + + return $result; + } + + /** + * This method validates if a filter (as passed to calendarQuery) matches + * the given object. + * + * @return bool + */ + protected function validateFilterForObject(array $object, array $filters) + { + // Unfortunately, setting the 'calendardata' here is optional. If + // it was excluded, we actually need another call to get this as + // well. + if (!isset($object['calendardata'])) { + $object = $this->getCalendarObject($object['calendarid'], $object['uri']); + } + + $vObject = VObject\Reader::read($object['calendardata']); + + $validator = new CalDAV\CalendarQueryValidator(); + $result = $validator->validate($vObject, $filters); + + // Destroy circular references so PHP will GC the object. + $vObject->destroy(); + + return $result; + } + + /** + * Searches through all of a users calendars and calendar objects to find + * an object with a specific UID. + * + * This method should return the path to this object, relative to the + * calendar home, so this path usually only contains two parts: + * + * calendarpath/objectpath.ics + * + * If the uid is not found, return null. + * + * This method should only consider * objects that the principal owns, so + * any calendars owned by other principals that also appear in this + * collection should be ignored. + * + * @param string $principalUri + * @param string $uid + * + * @return string|null + */ + public function getCalendarObjectByUID($principalUri, $uid) + { + // Note: this is a super slow naive implementation of this method. You + // are highly recommended to optimize it, if your backend allows it. + foreach ($this->getCalendarsForUser($principalUri) as $calendar) { + // We must ignore calendars owned by other principals. + if ($calendar['principaluri'] !== $principalUri) { + continue; + } + + // Ignore calendars that are shared. + if (isset($calendar['{http://sabredav.org/ns}owner-principal']) && $calendar['{http://sabredav.org/ns}owner-principal'] !== $principalUri) { + continue; + } + + $results = $this->calendarQuery( + $calendar['id'], + [ + 'name' => 'VCALENDAR', + 'prop-filters' => [], + 'comp-filters' => [ + [ + 'name' => 'VEVENT', + 'is-not-defined' => false, + 'time-range' => null, + 'comp-filters' => [], + 'prop-filters' => [ + [ + 'name' => 'UID', + 'is-not-defined' => false, + 'time-range' => null, + 'text-match' => [ + 'value' => $uid, + 'negate-condition' => false, + 'collation' => 'i;octet', + ], + 'param-filters' => [], + ], + ], + ], + ], + ] + ); + if ($results) { + // We have a match + return $calendar['uri'].'/'.$results[0]; + } + } + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/BackendInterface.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/BackendInterface.php new file mode 100644 index 0000000..ccaa251 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/BackendInterface.php @@ -0,0 +1,273 @@ + 'displayname', + '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description', + '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => 'timezone', + '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder', + '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor', + ]; + + /** + * List of subscription properties, and how they map to database fieldnames. + * + * @var array + */ + public $subscriptionPropertyMap = [ + '{DAV:}displayname' => 'displayname', + '{http://apple.com/ns/ical/}refreshrate' => 'refreshrate', + '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder', + '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor', + '{http://calendarserver.org/ns/}subscribed-strip-todos' => 'striptodos', + '{http://calendarserver.org/ns/}subscribed-strip-alarms' => 'stripalarms', + '{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments', + ]; + + /** + * Creates the backend. + */ + public function __construct(\PDO $pdo) + { + $this->pdo = $pdo; + } + + /** + * Returns a list of calendars for a principal. + * + * Every project is an array with the following keys: + * * id, a unique id that will be used by other functions to modify the + * calendar. This can be the same as the uri or a database key. + * * uri. This is just the 'base uri' or 'filename' of the calendar. + * * principaluri. The owner of the calendar. Almost always the same as + * principalUri passed to this method. + * + * Furthermore it can contain webdav properties in clark notation. A very + * common one is '{DAV:}displayname'. + * + * Many clients also require: + * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set + * For this property, you can just return an instance of + * Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet. + * + * If you return {http://sabredav.org/ns}read-only and set the value to 1, + * ACL will automatically be put in read-only mode. + * + * @param string $principalUri + * + * @return array + */ + public function getCalendarsForUser($principalUri) + { + $fields = array_values($this->propertyMap); + $fields[] = 'calendarid'; + $fields[] = 'uri'; + $fields[] = 'synctoken'; + $fields[] = 'components'; + $fields[] = 'principaluri'; + $fields[] = 'transparent'; + $fields[] = 'access'; + + // Making fields a comma-delimited list + $fields = implode(', ', $fields); + $stmt = $this->pdo->prepare(<<calendarInstancesTableName}.id as id, $fields FROM {$this->calendarInstancesTableName} + LEFT JOIN {$this->calendarTableName} ON + {$this->calendarInstancesTableName}.calendarid = {$this->calendarTableName}.id +WHERE principaluri = ? ORDER BY calendarorder ASC +SQL + ); + $stmt->execute([$principalUri]); + + $calendars = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $components = []; + if ($row['components']) { + $components = explode(',', $row['components']); + } + + $calendar = [ + 'id' => [(int) $row['calendarid'], (int) $row['id']], + 'uri' => $row['uri'], + 'principaluri' => $row['principaluri'], + '{'.CalDAV\Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ? $row['synctoken'] : '0'), + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0', + '{'.CalDAV\Plugin::NS_CALDAV.'}supported-calendar-component-set' => new CalDAV\Xml\Property\SupportedCalendarComponentSet($components), + '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-calendar-transp' => new CalDAV\Xml\Property\ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'), + 'share-resource-uri' => '/ns/share/'.$row['calendarid'], + ]; + + $calendar['share-access'] = (int) $row['access']; + // 1 = owner, 2 = readonly, 3 = readwrite + if ($row['access'] > 1) { + // We need to find more information about the original owner. + //$stmt2 = $this->pdo->prepare('SELECT principaluri FROM ' . $this->calendarInstancesTableName . ' WHERE access = 1 AND id = ?'); + //$stmt2->execute([$row['id']]); + + // read-only is for backwards compatibility. Might go away in + // the future. + $calendar['read-only'] = \Sabre\DAV\Sharing\Plugin::ACCESS_READ === (int) $row['access']; + } + + foreach ($this->propertyMap as $xmlName => $dbName) { + $calendar[$xmlName] = $row[$dbName]; + } + + $calendars[] = $calendar; + } + + return $calendars; + } + + /** + * Creates a new calendar for a principal. + * + * If the creation was a success, an id must be returned that can be used + * to reference this calendar in other methods, such as updateCalendar. + * + * @param string $principalUri + * @param string $calendarUri + * + * @return string + */ + public function createCalendar($principalUri, $calendarUri, array $properties) + { + $fieldNames = [ + 'principaluri', + 'uri', + 'transparent', + 'calendarid', + ]; + $values = [ + ':principaluri' => $principalUri, + ':uri' => $calendarUri, + ':transparent' => 0, + ]; + + $sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'; + if (!isset($properties[$sccs])) { + // Default value + $components = 'VEVENT,VTODO'; + } else { + if (!($properties[$sccs] instanceof CalDAV\Xml\Property\SupportedCalendarComponentSet)) { + throw new DAV\Exception('The '.$sccs.' property must be of type: \Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet'); + } + $components = implode(',', $properties[$sccs]->getValue()); + } + $transp = '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-calendar-transp'; + if (isset($properties[$transp])) { + $values[':transparent'] = 'transparent' === $properties[$transp]->getValue() ? 1 : 0; + } + $stmt = $this->pdo->prepare('INSERT INTO '.$this->calendarTableName.' (synctoken, components) VALUES (1, ?)'); + $stmt->execute([$components]); + + $calendarId = $this->pdo->lastInsertId( + $this->calendarTableName.'_id_seq' + ); + + $values[':calendarid'] = $calendarId; + + foreach ($this->propertyMap as $xmlName => $dbName) { + if (isset($properties[$xmlName])) { + $values[':'.$dbName] = $properties[$xmlName]; + $fieldNames[] = $dbName; + } + } + + $stmt = $this->pdo->prepare('INSERT INTO '.$this->calendarInstancesTableName.' ('.implode(', ', $fieldNames).') VALUES ('.implode(', ', array_keys($values)).')'); + + $stmt->execute($values); + + return [ + $calendarId, + $this->pdo->lastInsertId($this->calendarInstancesTableName.'_id_seq'), + ]; + } + + /** + * Updates properties for a calendar. + * + * The list of mutations is stored in a Sabre\DAV\PropPatch object. + * To do the actual updates, you must tell this object which properties + * you're going to process with the handle() method. + * + * Calling the handle method is like telling the PropPatch object "I + * promise I can handle updating this property". + * + * Read the PropPatch documentation for more info and examples. + * + * @param mixed $calendarId + */ + public function updateCalendar($calendarId, PropPatch $propPatch) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $supportedProperties = array_keys($this->propertyMap); + $supportedProperties[] = '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-calendar-transp'; + + $propPatch->handle($supportedProperties, function ($mutations) use ($calendarId, $instanceId) { + $newValues = []; + foreach ($mutations as $propertyName => $propertyValue) { + switch ($propertyName) { + case '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-calendar-transp': + $fieldName = 'transparent'; + $newValues[$fieldName] = 'transparent' === $propertyValue->getValue(); + break; + default: + $fieldName = $this->propertyMap[$propertyName]; + $newValues[$fieldName] = $propertyValue; + break; + } + } + $valuesSql = []; + foreach ($newValues as $fieldName => $value) { + $valuesSql[] = $fieldName.' = ?'; + } + + $stmt = $this->pdo->prepare('UPDATE '.$this->calendarInstancesTableName.' SET '.implode(', ', $valuesSql).' WHERE id = ?'); + $newValues['id'] = $instanceId; + $stmt->execute(array_values($newValues)); + + $this->addChange($calendarId, '', 2); + + return true; + }); + } + + /** + * Delete a calendar and all it's objects. + * + * @param mixed $calendarId + */ + public function deleteCalendar($calendarId) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $stmt = $this->pdo->prepare('SELECT access FROM '.$this->calendarInstancesTableName.' where id = ?'); + $stmt->execute([$instanceId]); + $access = (int) $stmt->fetchColumn(); + + if (\Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER === $access) { + /** + * If the user is the owner of the calendar, we delete all data and all + * instances. + **/ + $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarObjectTableName.' WHERE calendarid = ?'); + $stmt->execute([$calendarId]); + + $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarChangesTableName.' WHERE calendarid = ?'); + $stmt->execute([$calendarId]); + + $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarInstancesTableName.' WHERE calendarid = ?'); + $stmt->execute([$calendarId]); + + $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarTableName.' WHERE id = ?'); + $stmt->execute([$calendarId]); + } else { + /** + * If it was an instance of a shared calendar, we only delete that + * instance. + */ + $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarInstancesTableName.' WHERE id = ?'); + $stmt->execute([$instanceId]); + } + } + + /** + * Returns all calendar objects within a calendar. + * + * Every item contains an array with the following keys: + * * calendardata - The iCalendar-compatible calendar data + * * uri - a unique key which will be used to construct the uri. This can + * be any arbitrary string, but making sure it ends with '.ics' is a + * good idea. This is only the basename, or filename, not the full + * path. + * * lastmodified - a timestamp of the last modification time + * * etag - An arbitrary string, surrounded by double-quotes. (e.g.: + * ' "abcdef"') + * * size - The size of the calendar objects, in bytes. + * * component - optional, a string containing the type of object, such + * as 'vevent' or 'vtodo'. If specified, this will be used to populate + * the Content-Type header. + * + * Note that the etag is optional, but it's highly encouraged to return for + * speed reasons. + * + * The calendardata is also optional. If it's not returned + * 'getCalendarObject' will be called later, which *is* expected to return + * calendardata. + * + * If neither etag or size are specified, the calendardata will be + * used/fetched to determine these numbers. If both are specified the + * amount of times this is needed is reduced by a great degree. + * + * @param mixed $calendarId + * + * @return array + */ + public function getCalendarObjects($calendarId) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size, componenttype FROM '.$this->calendarObjectTableName.' WHERE calendarid = ?'); + $stmt->execute([$calendarId]); + + $result = []; + foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) { + $result[] = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'lastmodified' => (int) $row['lastmodified'], + 'etag' => '"'.$row['etag'].'"', + 'size' => (int) $row['size'], + 'component' => strtolower($row['componenttype']), + ]; + } + + return $result; + } + + /** + * Returns information from a single calendar object, based on it's object + * uri. + * + * The object uri is only the basename, or filename and not a full path. + * + * The returned array must have the same keys as getCalendarObjects. The + * 'calendardata' object is required here though, while it's not required + * for getCalendarObjects. + * + * This method must return null if the object did not exist. + * + * @param mixed $calendarId + * @param string $objectUri + * + * @return array|null + */ + public function getCalendarObject($calendarId, $objectUri) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM '.$this->calendarObjectTableName.' WHERE calendarid = ? AND uri = ?'); + $stmt->execute([$calendarId, $objectUri]); + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (!$row) { + return null; + } + + return [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'lastmodified' => (int) $row['lastmodified'], + 'etag' => '"'.$row['etag'].'"', + 'size' => (int) $row['size'], + 'calendardata' => $row['calendardata'], + 'component' => strtolower($row['componenttype']), + ]; + } + + /** + * Returns a list of calendar objects. + * + * This method should work identical to getCalendarObject, but instead + * return all the calendar objects in the list as an array. + * + * If the backend supports this, it may allow for some speed-ups. + * + * @param mixed $calendarId + * + * @return array + */ + public function getMultipleCalendarObjects($calendarId, array $uris) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $result = []; + foreach (array_chunk($uris, 900) as $chunk) { + $query = 'SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM '.$this->calendarObjectTableName.' WHERE calendarid = ? AND uri IN ('; + // Inserting a whole bunch of question marks + $query .= implode(',', array_fill(0, count($chunk), '?')); + $query .= ')'; + + $stmt = $this->pdo->prepare($query); + $stmt->execute(array_merge([$calendarId], $chunk)); + + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $result[] = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'lastmodified' => (int) $row['lastmodified'], + 'etag' => '"'.$row['etag'].'"', + 'size' => (int) $row['size'], + 'calendardata' => $row['calendardata'], + 'component' => strtolower($row['componenttype']), + ]; + } + } + + return $result; + } + + /** + * Creates a new calendar object. + * + * The object uri is only the basename, or filename and not a full path. + * + * It is possible return an etag from this function, which will be used in + * the response to this PUT request. Note that the ETag must be surrounded + * by double-quotes. + * + * However, you should only really return this ETag if you don't mangle the + * calendar-data. If the result of a subsequent GET to this object is not + * the exact same as this request body, you should omit the ETag. + * + * @param mixed $calendarId + * @param string $objectUri + * @param string $calendarData + * + * @return string|null + */ + public function createCalendarObject($calendarId, $objectUri, $calendarData) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $extraData = $this->getDenormalizedData($calendarData); + + $stmt = $this->pdo->prepare('INSERT INTO '.$this->calendarObjectTableName.' (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES (?,?,?,?,?,?,?,?,?,?)'); + $stmt->execute([ + $calendarId, + $objectUri, + $calendarData, + time(), + $extraData['etag'], + $extraData['size'], + $extraData['componentType'], + $extraData['firstOccurence'], + $extraData['lastOccurence'], + $extraData['uid'], + ]); + $this->addChange($calendarId, $objectUri, 1); + + return '"'.$extraData['etag'].'"'; + } + + /** + * Updates an existing calendarobject, based on it's uri. + * + * The object uri is only the basename, or filename and not a full path. + * + * It is possible return an etag from this function, which will be used in + * the response to this PUT request. Note that the ETag must be surrounded + * by double-quotes. + * + * However, you should only really return this ETag if you don't mangle the + * calendar-data. If the result of a subsequent GET to this object is not + * the exact same as this request body, you should omit the ETag. + * + * @param mixed $calendarId + * @param string $objectUri + * @param string $calendarData + * + * @return string|null + */ + public function updateCalendarObject($calendarId, $objectUri, $calendarData) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $extraData = $this->getDenormalizedData($calendarData); + + $stmt = $this->pdo->prepare('UPDATE '.$this->calendarObjectTableName.' SET calendardata = ?, lastmodified = ?, etag = ?, size = ?, componenttype = ?, firstoccurence = ?, lastoccurence = ?, uid = ? WHERE calendarid = ? AND uri = ?'); + $stmt->execute([$calendarData, time(), $extraData['etag'], $extraData['size'], $extraData['componentType'], $extraData['firstOccurence'], $extraData['lastOccurence'], $extraData['uid'], $calendarId, $objectUri]); + + $this->addChange($calendarId, $objectUri, 2); + + return '"'.$extraData['etag'].'"'; + } + + /** + * Parses some information from calendar objects, used for optimized + * calendar-queries. + * + * Returns an array with the following keys: + * * etag - An md5 checksum of the object without the quotes. + * * size - Size of the object in bytes + * * componentType - VEVENT, VTODO or VJOURNAL + * * firstOccurence + * * lastOccurence + * * uid - value of the UID property + * + * @param string $calendarData + * + * @return array + */ + protected function getDenormalizedData($calendarData) + { + $vObject = VObject\Reader::read($calendarData); + $componentType = null; + $component = null; + $firstOccurence = null; + $lastOccurence = null; + $uid = null; + foreach ($vObject->getComponents() as $component) { + if ('VTIMEZONE' !== $component->name) { + $componentType = $component->name; + $uid = (string) $component->UID; + break; + } + } + if (!$componentType) { + throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component'); + } + if ('VEVENT' === $componentType) { + $firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp(); + // Finding the last occurence is a bit harder + if (!isset($component->RRULE)) { + if (isset($component->DTEND)) { + $lastOccurence = $component->DTEND->getDateTime()->getTimeStamp(); + } elseif (isset($component->DURATION)) { + $endDate = clone $component->DTSTART->getDateTime(); + $endDate = $endDate->add(VObject\DateTimeParser::parse($component->DURATION->getValue())); + $lastOccurence = $endDate->getTimeStamp(); + } elseif (!$component->DTSTART->hasTime()) { + $endDate = clone $component->DTSTART->getDateTime(); + $endDate = $endDate->modify('+1 day'); + $lastOccurence = $endDate->getTimeStamp(); + } else { + $lastOccurence = $firstOccurence; + } + } else { + $it = new VObject\Recur\EventIterator($vObject, (string) $component->UID); + $maxDate = new \DateTime(self::MAX_DATE); + if ($it->isInfinite()) { + $lastOccurence = $maxDate->getTimeStamp(); + } else { + $end = $it->getDtEnd(); + while ($it->valid() && $end < $maxDate) { + $end = $it->getDtEnd(); + $it->next(); + } + $lastOccurence = $end->getTimeStamp(); + } + } + + // Ensure Occurence values are positive + if ($firstOccurence < 0) { + $firstOccurence = 0; + } + if ($lastOccurence < 0) { + $lastOccurence = 0; + } + } + + // Destroy circular references to PHP will GC the object. + $vObject->destroy(); + + return [ + 'etag' => md5($calendarData), + 'size' => strlen($calendarData), + 'componentType' => $componentType, + 'firstOccurence' => $firstOccurence, + 'lastOccurence' => $lastOccurence, + 'uid' => $uid, + ]; + } + + /** + * Deletes an existing calendar object. + * + * The object uri is only the basename, or filename and not a full path. + * + * @param mixed $calendarId + * @param string $objectUri + */ + public function deleteCalendarObject($calendarId, $objectUri) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarObjectTableName.' WHERE calendarid = ? AND uri = ?'); + $stmt->execute([$calendarId, $objectUri]); + + $this->addChange($calendarId, $objectUri, 3); + } + + /** + * Performs a calendar-query on the contents of this calendar. + * + * The calendar-query is defined in RFC4791 : CalDAV. Using the + * calendar-query it is possible for a client to request a specific set of + * object, based on contents of iCalendar properties, date-ranges and + * iCalendar component types (VTODO, VEVENT). + * + * This method should just return a list of (relative) urls that match this + * query. + * + * The list of filters are specified as an array. The exact array is + * documented by \Sabre\CalDAV\CalendarQueryParser. + * + * Note that it is extremely likely that getCalendarObject for every path + * returned from this method will be called almost immediately after. You + * may want to anticipate this to speed up these requests. + * + * This method provides a default implementation, which parses *all* the + * iCalendar objects in the specified calendar. + * + * This default may well be good enough for personal use, and calendars + * that aren't very large. But if you anticipate high usage, big calendars + * or high loads, you are strongly advised to optimize certain paths. + * + * The best way to do so is override this method and to optimize + * specifically for 'common filters'. + * + * Requests that are extremely common are: + * * requests for just VEVENTS + * * requests for just VTODO + * * requests with a time-range-filter on a VEVENT. + * + * ..and combinations of these requests. It may not be worth it to try to + * handle every possible situation and just rely on the (relatively + * easy to use) CalendarQueryValidator to handle the rest. + * + * Note that especially time-range-filters may be difficult to parse. A + * time-range filter specified on a VEVENT must for instance also handle + * recurrence rules correctly. + * A good example of how to interpret all these filters can also simply + * be found in \Sabre\CalDAV\CalendarQueryFilter. This class is as correct + * as possible, so it gives you a good idea on what type of stuff you need + * to think of. + * + * This specific implementation (for the PDO) backend optimizes filters on + * specific components, and VEVENT time-ranges. + * + * @param mixed $calendarId + * + * @return array + */ + public function calendarQuery($calendarId, array $filters) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $componentType = null; + $requirePostFilter = true; + $timeRange = null; + + // if no filters were specified, we don't need to filter after a query + if (!$filters['prop-filters'] && !$filters['comp-filters']) { + $requirePostFilter = false; + } + + // Figuring out if there's a component filter + if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) { + $componentType = $filters['comp-filters'][0]['name']; + + // Checking if we need post-filters + $has_time_range = array_key_exists('time-range', $filters['comp-filters'][0]) && $filters['comp-filters'][0]['time-range']; + if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$has_time_range && !$filters['comp-filters'][0]['prop-filters']) { + $requirePostFilter = false; + } + // There was a time-range filter + if ('VEVENT' == $componentType && $has_time_range) { + $timeRange = $filters['comp-filters'][0]['time-range']; + + // If start time OR the end time is not specified, we can do a + // 100% accurate mysql query. + if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && $timeRange) { + if ((array_key_exists('start', $timeRange) && !$timeRange['start']) || (array_key_exists('end', $timeRange) && !$timeRange['end'])) { + $requirePostFilter = false; + } + } + } + } + + if ($requirePostFilter) { + $query = 'SELECT uri, calendardata FROM '.$this->calendarObjectTableName.' WHERE calendarid = :calendarid'; + } else { + $query = 'SELECT uri FROM '.$this->calendarObjectTableName.' WHERE calendarid = :calendarid'; + } + + $values = [ + 'calendarid' => $calendarId, + ]; + + if ($componentType) { + $query .= ' AND componenttype = :componenttype'; + $values['componenttype'] = $componentType; + } + + if ($timeRange && array_key_exists('start', $timeRange) && $timeRange['start']) { + $query .= ' AND lastoccurence > :startdate'; + $values['startdate'] = $timeRange['start']->getTimeStamp(); + } + if ($timeRange && array_key_exists('end', $timeRange) && $timeRange['end']) { + $query .= ' AND firstoccurence < :enddate'; + $values['enddate'] = $timeRange['end']->getTimeStamp(); + } + + $stmt = $this->pdo->prepare($query); + $stmt->execute($values); + + $result = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + if ($requirePostFilter) { + if (!$this->validateFilterForObject($row, $filters)) { + continue; + } + } + $result[] = $row['uri']; + } + + return $result; + } + + /** + * Searches through all of a users calendars and calendar objects to find + * an object with a specific UID. + * + * This method should return the path to this object, relative to the + * calendar home, so this path usually only contains two parts: + * + * calendarpath/objectpath.ics + * + * If the uid is not found, return null. + * + * This method should only consider * objects that the principal owns, so + * any calendars owned by other principals that also appear in this + * collection should be ignored. + * + * @param string $principalUri + * @param string $uid + * + * @return string|null + */ + public function getCalendarObjectByUID($principalUri, $uid) + { + $query = <<calendarObjectTableName AS calendarobjects +LEFT JOIN + $this->calendarInstancesTableName AS calendar_instances + ON calendarobjects.calendarid = calendar_instances.calendarid +WHERE + calendar_instances.principaluri = ? + AND + calendarobjects.uid = ? + AND + calendar_instances.access = 1 +SQL; + + $stmt = $this->pdo->prepare($query); + $stmt->execute([$principalUri, $uid]); + + if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + return $row['calendaruri'].'/'.$row['objecturi']; + } + } + + /** + * The getChanges method returns all the changes that have happened, since + * the specified syncToken in the specified calendar. + * + * This function should return an array, such as the following: + * + * [ + * 'syncToken' => 'The current synctoken', + * 'added' => [ + * 'new.txt', + * ], + * 'modified' => [ + * 'modified.txt', + * ], + * 'deleted' => [ + * 'foo.php.bak', + * 'old.txt' + * ] + * ]; + * + * The returned syncToken property should reflect the *current* syncToken + * of the calendar, as reported in the {http://sabredav.org/ns}sync-token + * property this is needed here too, to ensure the operation is atomic. + * + * If the $syncToken argument is specified as null, this is an initial + * sync, and all members should be reported. + * + * The modified property is an array of nodenames that have changed since + * the last token. + * + * The deleted property is an array with nodenames, that have been deleted + * from collection. + * + * The $syncLevel argument is basically the 'depth' of the report. If it's + * 1, you only have to report changes that happened only directly in + * immediate descendants. If it's 2, it should also include changes from + * the nodes below the child collections. (grandchildren) + * + * The $limit argument allows a client to specify how many results should + * be returned at most. If the limit is not specified, it should be treated + * as infinite. + * + * If the limit (infinite or not) is higher than you're willing to return, + * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. + * + * If the syncToken is expired (due to data cleanup) or unknown, you must + * return null. + * + * The limit is 'suggestive'. You are free to ignore it. + * + * @param mixed $calendarId + * @param string $syncToken + * @param int $syncLevel + * @param int $limit + * + * @return array|null + */ + public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $result = [ + 'added' => [], + 'modified' => [], + 'deleted' => [], + ]; + + if ($syncToken) { + $query = 'SELECT uri, operation, synctoken FROM '.$this->calendarChangesTableName.' WHERE synctoken >= ? AND calendarid = ? ORDER BY synctoken'; + if ($limit > 0) { + // Fetch one more raw to detect result truncation + $query .= ' LIMIT '.((int) $limit + 1); + } + + // Fetching all changes + $stmt = $this->pdo->prepare($query); + $stmt->execute([$syncToken, $calendarId]); + + $changes = []; + + // This loop ensures that any duplicates are overwritten, only the + // last change on a node is relevant. + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $changes[$row['uri']] = $row; + } + $currentToken = null; + + $result_count = 0; + foreach ($changes as $uri => $operation) { + if (!is_null($limit) && $result_count >= $limit) { + $result['result_truncated'] = true; + break; + } + + if (null === $currentToken || $currentToken < $operation['synctoken'] + 1) { + // SyncToken in CalDAV perspective is consistently the next number of the last synced change event in this class. + $currentToken = $operation['synctoken'] + 1; + } + + ++$result_count; + switch ($operation['operation']) { + case 1: + $result['added'][] = $uri; + break; + case 2: + $result['modified'][] = $uri; + break; + case 3: + $result['deleted'][] = $uri; + break; + } + } + + if (!is_null($currentToken)) { + $result['syncToken'] = $currentToken; + } else { + // This means returned value is equivalent to syncToken + $result['syncToken'] = $syncToken; + } + } else { + // Current synctoken + $stmt = $this->pdo->prepare('SELECT synctoken FROM '.$this->calendarTableName.' WHERE id = ?'); + $stmt->execute([$calendarId]); + $currentToken = $stmt->fetchColumn(0); + + if (is_null($currentToken)) { + return null; + } + $result['syncToken'] = $currentToken; + + // No synctoken supplied, this is the initial sync. + $query = 'SELECT uri FROM '.$this->calendarObjectTableName.' WHERE calendarid = ?'; + $stmt = $this->pdo->prepare($query); + $stmt->execute([$calendarId]); + + $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN); + } + + return $result; + } + + /** + * Adds a change record to the calendarchanges table. + * + * @param mixed $calendarId + * @param string $objectUri + * @param int $operation 1 = add, 2 = modify, 3 = delete + */ + protected function addChange($calendarId, $objectUri, $operation) + { + $stmt = $this->pdo->prepare('INSERT INTO '.$this->calendarChangesTableName.' (uri, synctoken, calendarid, operation) SELECT ?, synctoken, ?, ? FROM '.$this->calendarTableName.' WHERE id = ?'); + $stmt->execute([ + $objectUri, + $calendarId, + $operation, + $calendarId, + ]); + $stmt = $this->pdo->prepare('UPDATE '.$this->calendarTableName.' SET synctoken = synctoken + 1 WHERE id = ?'); + $stmt->execute([ + $calendarId, + ]); + } + + /** + * Returns a list of subscriptions for a principal. + * + * Every subscription is an array with the following keys: + * * id, a unique id that will be used by other functions to modify the + * subscription. This can be the same as the uri or a database key. + * * uri. This is just the 'base uri' or 'filename' of the subscription. + * * principaluri. The owner of the subscription. Almost always the same as + * principalUri passed to this method. + * * source. Url to the actual feed + * + * Furthermore, all the subscription info must be returned too: + * + * 1. {DAV:}displayname + * 2. {http://apple.com/ns/ical/}refreshrate + * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos + * should not be stripped). + * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms + * should not be stripped). + * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if + * attachments should not be stripped). + * 7. {http://apple.com/ns/ical/}calendar-color + * 8. {http://apple.com/ns/ical/}calendar-order + * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set + * (should just be an instance of + * Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of + * default components). + * + * @param string $principalUri + * + * @return array + */ + public function getSubscriptionsForUser($principalUri) + { + $fields = array_values($this->subscriptionPropertyMap); + $fields[] = 'id'; + $fields[] = 'uri'; + $fields[] = 'source'; + $fields[] = 'principaluri'; + $fields[] = 'lastmodified'; + + // Making fields a comma-delimited list + $fields = implode(', ', $fields); + $stmt = $this->pdo->prepare('SELECT '.$fields.' FROM '.$this->calendarSubscriptionsTableName.' WHERE principaluri = ? ORDER BY calendarorder ASC'); + $stmt->execute([$principalUri]); + + $subscriptions = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $subscription = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'principaluri' => $row['principaluri'], + 'source' => $row['source'], + 'lastmodified' => $row['lastmodified'], + + '{'.CalDAV\Plugin::NS_CALDAV.'}supported-calendar-component-set' => new CalDAV\Xml\Property\SupportedCalendarComponentSet(['VTODO', 'VEVENT']), + ]; + + foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) { + if (!is_null($row[$dbName])) { + $subscription[$xmlName] = $row[$dbName]; + } + } + + $subscriptions[] = $subscription; + } + + return $subscriptions; + } + + /** + * Creates a new subscription for a principal. + * + * If the creation was a success, an id must be returned that can be used to reference + * this subscription in other methods, such as updateSubscription. + * + * @param string $principalUri + * @param string $uri + * + * @return mixed + */ + public function createSubscription($principalUri, $uri, array $properties) + { + $fieldNames = [ + 'principaluri', + 'uri', + 'source', + 'lastmodified', + ]; + + if (!isset($properties['{http://calendarserver.org/ns/}source'])) { + throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions'); + } + + $values = [ + ':principaluri' => $principalUri, + ':uri' => $uri, + ':source' => $properties['{http://calendarserver.org/ns/}source']->getHref(), + ':lastmodified' => time(), + ]; + + foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) { + if (isset($properties[$xmlName])) { + $values[':'.$dbName] = $properties[$xmlName]; + $fieldNames[] = $dbName; + } + } + + $stmt = $this->pdo->prepare('INSERT INTO '.$this->calendarSubscriptionsTableName.' ('.implode(', ', $fieldNames).') VALUES ('.implode(', ', array_keys($values)).')'); + $stmt->execute($values); + + return $this->pdo->lastInsertId( + $this->calendarSubscriptionsTableName.'_id_seq' + ); + } + + /** + * Updates a subscription. + * + * The list of mutations is stored in a Sabre\DAV\PropPatch object. + * To do the actual updates, you must tell this object which properties + * you're going to process with the handle() method. + * + * Calling the handle method is like telling the PropPatch object "I + * promise I can handle updating this property". + * + * Read the PropPatch documentation for more info and examples. + * + * @param mixed $subscriptionId + */ + public function updateSubscription($subscriptionId, PropPatch $propPatch) + { + $supportedProperties = array_keys($this->subscriptionPropertyMap); + $supportedProperties[] = '{http://calendarserver.org/ns/}source'; + + $propPatch->handle($supportedProperties, function ($mutations) use ($subscriptionId) { + $newValues = []; + + foreach ($mutations as $propertyName => $propertyValue) { + if ('{http://calendarserver.org/ns/}source' === $propertyName) { + $newValues['source'] = $propertyValue->getHref(); + } else { + $fieldName = $this->subscriptionPropertyMap[$propertyName]; + $newValues[$fieldName] = $propertyValue; + } + } + + // Now we're generating the sql query. + $valuesSql = []; + foreach ($newValues as $fieldName => $value) { + $valuesSql[] = $fieldName.' = ?'; + } + + $stmt = $this->pdo->prepare('UPDATE '.$this->calendarSubscriptionsTableName.' SET '.implode(', ', $valuesSql).', lastmodified = ? WHERE id = ?'); + $newValues['lastmodified'] = time(); + $newValues['id'] = $subscriptionId; + $stmt->execute(array_values($newValues)); + + return true; + }); + } + + /** + * Deletes a subscription. + * + * @param mixed $subscriptionId + */ + public function deleteSubscription($subscriptionId) + { + $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarSubscriptionsTableName.' WHERE id = ?'); + $stmt->execute([$subscriptionId]); + } + + /** + * Returns a single scheduling object. + * + * The returned array should contain the following elements: + * * uri - A unique basename for the object. This will be used to + * construct a full uri. + * * calendardata - The iCalendar object + * * lastmodified - The last modification date. Can be an int for a unix + * timestamp, or a PHP DateTime object. + * * etag - A unique token that must change if the object changed. + * * size - The size of the object, in bytes. + * + * @param string $principalUri + * @param string $objectUri + * + * @return array + */ + public function getSchedulingObject($principalUri, $objectUri) + { + $stmt = $this->pdo->prepare('SELECT uri, calendardata, lastmodified, etag, size FROM '.$this->schedulingObjectTableName.' WHERE principaluri = ? AND uri = ?'); + $stmt->execute([$principalUri, $objectUri]); + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (!$row) { + return null; + } + + return [ + 'uri' => $row['uri'], + 'calendardata' => $row['calendardata'], + 'lastmodified' => $row['lastmodified'], + 'etag' => '"'.$row['etag'].'"', + 'size' => (int) $row['size'], + ]; + } + + /** + * Returns all scheduling objects for the inbox collection. + * + * These objects should be returned as an array. Every item in the array + * should follow the same structure as returned from getSchedulingObject. + * + * The main difference is that 'calendardata' is optional. + * + * @param string $principalUri + * + * @return array + */ + public function getSchedulingObjects($principalUri) + { + $stmt = $this->pdo->prepare('SELECT id, calendardata, uri, lastmodified, etag, size FROM '.$this->schedulingObjectTableName.' WHERE principaluri = ?'); + $stmt->execute([$principalUri]); + + $result = []; + foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) { + $result[] = [ + 'calendardata' => $row['calendardata'], + 'uri' => $row['uri'], + 'lastmodified' => $row['lastmodified'], + 'etag' => '"'.$row['etag'].'"', + 'size' => (int) $row['size'], + ]; + } + + return $result; + } + + /** + * Deletes a scheduling object. + * + * @param string $principalUri + * @param string $objectUri + */ + public function deleteSchedulingObject($principalUri, $objectUri) + { + $stmt = $this->pdo->prepare('DELETE FROM '.$this->schedulingObjectTableName.' WHERE principaluri = ? AND uri = ?'); + $stmt->execute([$principalUri, $objectUri]); + } + + /** + * Creates a new scheduling object. This should land in a users' inbox. + * + * @param string $principalUri + * @param string $objectUri + * @param string|resource $objectData + */ + public function createSchedulingObject($principalUri, $objectUri, $objectData) + { + $stmt = $this->pdo->prepare('INSERT INTO '.$this->schedulingObjectTableName.' (principaluri, calendardata, uri, lastmodified, etag, size) VALUES (?, ?, ?, ?, ?, ?)'); + + if (is_resource($objectData)) { + $objectData = stream_get_contents($objectData); + } + + $stmt->execute([$principalUri, $objectData, $objectUri, time(), md5($objectData), strlen($objectData)]); + } + + /** + * Updates the list of shares. + * + * @param mixed $calendarId + * @param \Sabre\DAV\Xml\Element\Sharee[] $sharees + */ + public function updateInvites($calendarId, array $sharees) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + $currentInvites = $this->getInvites($calendarId); + list($calendarId, $instanceId) = $calendarId; + + $removeStmt = $this->pdo->prepare('DELETE FROM '.$this->calendarInstancesTableName.' WHERE calendarid = ? AND share_href = ? AND access IN (2,3)'); + $updateStmt = $this->pdo->prepare('UPDATE '.$this->calendarInstancesTableName.' SET access = ?, share_displayname = ?, share_invitestatus = ? WHERE calendarid = ? AND share_href = ?'); + + $insertStmt = $this->pdo->prepare(' +INSERT INTO '.$this->calendarInstancesTableName.' + ( + calendarid, + principaluri, + access, + displayname, + uri, + description, + calendarorder, + calendarcolor, + timezone, + transparent, + share_href, + share_displayname, + share_invitestatus + ) + SELECT + ?, + ?, + ?, + displayname, + ?, + description, + calendarorder, + calendarcolor, + timezone, + 1, + ?, + ?, + ? + FROM '.$this->calendarInstancesTableName.' WHERE id = ?'); + + foreach ($sharees as $sharee) { + if (\Sabre\DAV\Sharing\Plugin::ACCESS_NOACCESS === $sharee->access) { + // if access was set no NOACCESS, it means access for an + // existing sharee was removed. + $removeStmt->execute([$calendarId, $sharee->href]); + continue; + } + + if (is_null($sharee->principal)) { + // If the server could not determine the principal automatically, + // we will mark the invite status as invalid. + $sharee->inviteStatus = \Sabre\DAV\Sharing\Plugin::INVITE_INVALID; + } else { + // Because sabre/dav does not yet have an invitation system, + // every invite is automatically accepted for now. + $sharee->inviteStatus = \Sabre\DAV\Sharing\Plugin::INVITE_ACCEPTED; + } + + foreach ($currentInvites as $oldSharee) { + if ($oldSharee->href === $sharee->href) { + // This is an update + $sharee->properties = array_merge( + $oldSharee->properties, + $sharee->properties + ); + $updateStmt->execute([ + $sharee->access, + isset($sharee->properties['{DAV:}displayname']) ? $sharee->properties['{DAV:}displayname'] : null, + $sharee->inviteStatus ?: $oldSharee->inviteStatus, + $calendarId, + $sharee->href, + ]); + continue 2; + } + } + // If we got here, it means it was a new sharee + $insertStmt->execute([ + $calendarId, + $sharee->principal, + $sharee->access, + \Sabre\DAV\UUIDUtil::getUUID(), + $sharee->href, + isset($sharee->properties['{DAV:}displayname']) ? $sharee->properties['{DAV:}displayname'] : null, + $sharee->inviteStatus ?: \Sabre\DAV\Sharing\Plugin::INVITE_NORESPONSE, + $instanceId, + ]); + } + } + + /** + * Returns the list of people whom a calendar is shared with. + * + * Every item in the returned list must be a Sharee object with at + * least the following properties set: + * $href + * $shareAccess + * $inviteStatus + * + * and optionally: + * $properties + * + * @param mixed $calendarId + * + * @return \Sabre\DAV\Xml\Element\Sharee[] + */ + public function getInvites($calendarId) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to getInvites() is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $query = <<calendarInstancesTableName} +WHERE + calendarid = ? +SQL; + + $stmt = $this->pdo->prepare($query); + $stmt->execute([$calendarId]); + + $result = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $result[] = new Sharee([ + 'href' => isset($row['share_href']) ? $row['share_href'] : \Sabre\HTTP\encodePath($row['principaluri']), + 'access' => (int) $row['access'], + /// Everyone is always immediately accepted, for now. + 'inviteStatus' => (int) $row['share_invitestatus'], + 'properties' => !empty($row['share_displayname']) + ? ['{DAV:}displayname' => $row['share_displayname']] + : [], + 'principal' => $row['principaluri'], + ]); + } + + return $result; + } + + /** + * Publishes a calendar. + * + * @param mixed $calendarId + * @param bool $value + */ + public function setPublishStatus($calendarId, $value) + { + throw new DAV\Exception\NotImplemented('Not implemented'); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php new file mode 100644 index 0000000..69467e5 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php @@ -0,0 +1,66 @@ +pdo = $pdo; + } + + /** + * Returns a list of calendars for a principal. + * + * Every project is an array with the following keys: + * * id, a unique id that will be used by other functions to modify the + * calendar. This can be the same as the uri or a database key. + * * uri. This is just the 'base uri' or 'filename' of the calendar. + * * principaluri. The owner of the calendar. Almost always the same as + * principalUri passed to this method. + * + * Furthermore it can contain webdav properties in clark notation. A very + * common one is '{DAV:}displayname'. + * + * Many clients also require: + * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set + * For this property, you can just return an instance of + * Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet. + * + * If you return {http://sabredav.org/ns}read-only and set the value to 1, + * ACL will automatically be put in read-only mode. + * + * @param string $principalUri + * + * @return array + */ + public function getCalendarsForUser($principalUri) + { + // Making fields a comma-delimited list + $stmt = $this->pdo->prepare('SELECT id, uri FROM simple_calendars WHERE principaluri = ? ORDER BY id ASC'); + $stmt->execute([$principalUri]); + + $calendars = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $calendars[] = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'principaluri' => $principalUri, + ]; + } + + return $calendars; + } + + /** + * Creates a new calendar for a principal. + * + * If the creation was a success, an id must be returned that can be used + * to reference this calendar in other methods, such as updateCalendar. + * + * @param string $principalUri + * @param string $calendarUri + * + * @return string + */ + public function createCalendar($principalUri, $calendarUri, array $properties) + { + $stmt = $this->pdo->prepare('INSERT INTO simple_calendars (principaluri, uri) VALUES (?, ?)'); + $stmt->execute([$principalUri, $calendarUri]); + + return $this->pdo->lastInsertId(); + } + + /** + * Delete a calendar and all it's objects. + * + * @param string $calendarId + */ + public function deleteCalendar($calendarId) + { + $stmt = $this->pdo->prepare('DELETE FROM simple_calendarobjects WHERE calendarid = ?'); + $stmt->execute([$calendarId]); + + $stmt = $this->pdo->prepare('DELETE FROM simple_calendars WHERE id = ?'); + $stmt->execute([$calendarId]); + } + + /** + * Returns all calendar objects within a calendar. + * + * Every item contains an array with the following keys: + * * calendardata - The iCalendar-compatible calendar data + * * uri - a unique key which will be used to construct the uri. This can + * be any arbitrary string, but making sure it ends with '.ics' is a + * good idea. This is only the basename, or filename, not the full + * path. + * * lastmodified - a timestamp of the last modification time + * * etag - An arbitrary string, surrounded by double-quotes. (e.g.: + * ' "abcdef"') + * * size - The size of the calendar objects, in bytes. + * * component - optional, a string containing the type of object, such + * as 'vevent' or 'vtodo'. If specified, this will be used to populate + * the Content-Type header. + * + * Note that the etag is optional, but it's highly encouraged to return for + * speed reasons. + * + * The calendardata is also optional. If it's not returned + * 'getCalendarObject' will be called later, which *is* expected to return + * calendardata. + * + * If neither etag or size are specified, the calendardata will be + * used/fetched to determine these numbers. If both are specified the + * amount of times this is needed is reduced by a great degree. + * + * @param string $calendarId + * + * @return array + */ + public function getCalendarObjects($calendarId) + { + $stmt = $this->pdo->prepare('SELECT id, uri, calendardata FROM simple_calendarobjects WHERE calendarid = ?'); + $stmt->execute([$calendarId]); + + $result = []; + foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) { + $result[] = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'etag' => '"'.md5($row['calendardata']).'"', + 'calendarid' => $calendarId, + 'size' => strlen($row['calendardata']), + 'calendardata' => $row['calendardata'], + ]; + } + + return $result; + } + + /** + * Returns information from a single calendar object, based on it's object + * uri. + * + * The object uri is only the basename, or filename and not a full path. + * + * The returned array must have the same keys as getCalendarObjects. The + * 'calendardata' object is required here though, while it's not required + * for getCalendarObjects. + * + * This method must return null if the object did not exist. + * + * @param string $calendarId + * @param string $objectUri + * + * @return array|null + */ + public function getCalendarObject($calendarId, $objectUri) + { + $stmt = $this->pdo->prepare('SELECT id, uri, calendardata FROM simple_calendarobjects WHERE calendarid = ? AND uri = ?'); + $stmt->execute([$calendarId, $objectUri]); + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (!$row) { + return null; + } + + return [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'etag' => '"'.md5($row['calendardata']).'"', + 'calendarid' => $calendarId, + 'size' => strlen($row['calendardata']), + 'calendardata' => $row['calendardata'], + ]; + } + + /** + * Creates a new calendar object. + * + * The object uri is only the basename, or filename and not a full path. + * + * It is possible return an etag from this function, which will be used in + * the response to this PUT request. Note that the ETag must be surrounded + * by double-quotes. + * + * However, you should only really return this ETag if you don't mangle the + * calendar-data. If the result of a subsequent GET to this object is not + * the exact same as this request body, you should omit the ETag. + * + * @param mixed $calendarId + * @param string $objectUri + * @param string $calendarData + * + * @return string|null + */ + public function createCalendarObject($calendarId, $objectUri, $calendarData) + { + $stmt = $this->pdo->prepare('INSERT INTO simple_calendarobjects (calendarid, uri, calendardata) VALUES (?,?,?)'); + $stmt->execute([ + $calendarId, + $objectUri, + $calendarData, + ]); + + return '"'.md5($calendarData).'"'; + } + + /** + * Updates an existing calendarobject, based on it's uri. + * + * The object uri is only the basename, or filename and not a full path. + * + * It is possible return an etag from this function, which will be used in + * the response to this PUT request. Note that the ETag must be surrounded + * by double-quotes. + * + * However, you should only really return this ETag if you don't mangle the + * calendar-data. If the result of a subsequent GET to this object is not + * the exact same as this request body, you should omit the ETag. + * + * @param mixed $calendarId + * @param string $objectUri + * @param string $calendarData + * + * @return string|null + */ + public function updateCalendarObject($calendarId, $objectUri, $calendarData) + { + $stmt = $this->pdo->prepare('UPDATE simple_calendarobjects SET calendardata = ? WHERE calendarid = ? AND uri = ?'); + $stmt->execute([$calendarData, $calendarId, $objectUri]); + + return '"'.md5($calendarData).'"'; + } + + /** + * Deletes an existing calendar object. + * + * The object uri is only the basename, or filename and not a full path. + * + * @param string $calendarId + * @param string $objectUri + */ + public function deleteCalendarObject($calendarId, $objectUri) + { + $stmt = $this->pdo->prepare('DELETE FROM simple_calendarobjects WHERE calendarid = ? AND uri = ?'); + $stmt->execute([$calendarId, $objectUri]); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php new file mode 100644 index 0000000..7655c2e --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php @@ -0,0 +1,89 @@ + 'The current synctoken', + * 'added' => [ + * 'new.txt', + * ], + * 'modified' => [ + * 'modified.txt', + * ], + * 'deleted' => [ + * 'foo.php.bak', + * 'old.txt' + * ] + * ); + * + * The returned syncToken property should reflect the *current* syncToken + * of the calendar, as reported in the {http://sabredav.org/ns}sync-token + * property This is * needed here too, to ensure the operation is atomic. + * + * If the $syncToken argument is specified as null, this is an initial + * sync, and all members should be reported. + * + * The modified property is an array of nodenames that have changed since + * the last token. + * + * The deleted property is an array with nodenames, that have been deleted + * from collection. + * + * The $syncLevel argument is basically the 'depth' of the report. If it's + * 1, you only have to report changes that happened only directly in + * immediate descendants. If it's 2, it should also include changes from + * the nodes below the child collections. (grandchildren) + * + * The $limit argument allows a client to specify how many results should + * be returned at most. If the limit is not specified, it should be treated + * as infinite. + * + * If the limit (infinite or not) is higher than you're willing to return, + * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. + * + * If the syncToken is expired (due to data cleanup) or unknown, you must + * return null. + * + * The limit is 'suggestive'. You are free to ignore it. + * + * @param string $calendarId + * @param string $syncToken + * @param int $syncLevel + * @param int $limit + * + * @return array|null + */ + public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null); +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Calendar.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Calendar.php new file mode 100644 index 0000000..ba8c704 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Calendar.php @@ -0,0 +1,460 @@ +caldavBackend = $caldavBackend; + $this->calendarInfo = $calendarInfo; + } + + /** + * Returns the name of the calendar. + * + * @return string + */ + public function getName() + { + return $this->calendarInfo['uri']; + } + + /** + * Updates properties on this node. + * + * This method received a PropPatch object, which contains all the + * information about the update. + * + * To update specific properties, call the 'handle' method on this object. + * Read the PropPatch documentation for more information. + */ + public function propPatch(PropPatch $propPatch) + { + return $this->caldavBackend->updateCalendar($this->calendarInfo['id'], $propPatch); + } + + /** + * Returns the list of properties. + * + * @param array $requestedProperties + * + * @return array + */ + public function getProperties($requestedProperties) + { + $response = []; + + foreach ($this->calendarInfo as $propName => $propValue) { + if (!is_null($propValue) && '{' === $propName[0]) { + $response[$propName] = $this->calendarInfo[$propName]; + } + } + + return $response; + } + + /** + * Returns a calendar object. + * + * The contained calendar objects are for example Events or Todo's. + * + * @param string $name + * + * @return \Sabre\CalDAV\ICalendarObject + */ + public function getChild($name) + { + $obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name); + + if (!$obj) { + throw new DAV\Exception\NotFound('Calendar object not found'); + } + $obj['acl'] = $this->getChildACL(); + + return new CalendarObject($this->caldavBackend, $this->calendarInfo, $obj); + } + + /** + * Returns the full list of calendar objects. + * + * @return array + */ + public function getChildren() + { + $objs = $this->caldavBackend->getCalendarObjects($this->calendarInfo['id']); + $children = []; + foreach ($objs as $obj) { + $obj['acl'] = $this->getChildACL(); + $children[] = new CalendarObject($this->caldavBackend, $this->calendarInfo, $obj); + } + + return $children; + } + + /** + * This method receives a list of paths in it's first argument. + * It must return an array with Node objects. + * + * If any children are not found, you do not have to return them. + * + * @param string[] $paths + * + * @return array + */ + public function getMultipleChildren(array $paths) + { + $objs = $this->caldavBackend->getMultipleCalendarObjects($this->calendarInfo['id'], $paths); + $children = []; + foreach ($objs as $obj) { + $obj['acl'] = $this->getChildACL(); + $children[] = new CalendarObject($this->caldavBackend, $this->calendarInfo, $obj); + } + + return $children; + } + + /** + * Checks if a child-node exists. + * + * @param string $name + * + * @return bool + */ + public function childExists($name) + { + $obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name); + if (!$obj) { + return false; + } else { + return true; + } + } + + /** + * Creates a new directory. + * + * We actually block this, as subdirectories are not allowed in calendars. + * + * @param string $name + */ + public function createDirectory($name) + { + throw new DAV\Exception\MethodNotAllowed('Creating collections in calendar objects is not allowed'); + } + + /** + * Creates a new file. + * + * The contents of the new file must be a valid ICalendar string. + * + * @param string $name + * @param resource $data + * + * @return string|null + */ + public function createFile($name, $data = null) + { + if (is_resource($data)) { + $data = stream_get_contents($data); + } + + return $this->caldavBackend->createCalendarObject($this->calendarInfo['id'], $name, $data); + } + + /** + * Deletes the calendar. + */ + public function delete() + { + $this->caldavBackend->deleteCalendar($this->calendarInfo['id']); + } + + /** + * Renames the calendar. Note that most calendars use the + * {DAV:}displayname to display a name to display a name. + * + * @param string $newName + */ + public function setName($newName) + { + throw new DAV\Exception\MethodNotAllowed('Renaming calendars is not yet supported'); + } + + /** + * Returns the last modification date as a unix timestamp. + */ + public function getLastModified() + { + return null; + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->calendarInfo['principaluri']; + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + $acl = [ + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner().'/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner().'/calendar-proxy-read', + 'protected' => true, + ], + [ + 'privilege' => '{'.Plugin::NS_CALDAV.'}read-free-busy', + 'principal' => '{DAV:}authenticated', + 'protected' => true, + ], + ]; + if (empty($this->calendarInfo['{http://sabredav.org/ns}read-only'])) { + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->getOwner(), + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->getOwner().'/calendar-proxy-write', + 'protected' => true, + ]; + } + + return $acl; + } + + /** + * This method returns the ACL's for calendar objects in this calendar. + * The result of this method automatically gets passed to the + * calendar-object nodes in the calendar. + * + * @return array + */ + public function getChildACL() + { + $acl = [ + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner().'/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner().'/calendar-proxy-read', + 'protected' => true, + ], + ]; + if (empty($this->calendarInfo['{http://sabredav.org/ns}read-only'])) { + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->getOwner(), + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->getOwner().'/calendar-proxy-write', + 'protected' => true, + ]; + } + + return $acl; + } + + /** + * Performs a calendar-query on the contents of this calendar. + * + * The calendar-query is defined in RFC4791 : CalDAV. Using the + * calendar-query it is possible for a client to request a specific set of + * object, based on contents of iCalendar properties, date-ranges and + * iCalendar component types (VTODO, VEVENT). + * + * This method should just return a list of (relative) urls that match this + * query. + * + * The list of filters are specified as an array. The exact array is + * documented by Sabre\CalDAV\CalendarQueryParser. + * + * @return array + */ + public function calendarQuery(array $filters) + { + return $this->caldavBackend->calendarQuery($this->calendarInfo['id'], $filters); + } + + /** + * This method returns the current sync-token for this collection. + * This can be any string. + * + * If null is returned from this function, the plugin assumes there's no + * sync information available. + * + * @return string|null + */ + public function getSyncToken() + { + if ( + $this->caldavBackend instanceof Backend\SyncSupport && + isset($this->calendarInfo['{DAV:}sync-token']) + ) { + return $this->calendarInfo['{DAV:}sync-token']; + } + if ( + $this->caldavBackend instanceof Backend\SyncSupport && + isset($this->calendarInfo['{http://sabredav.org/ns}sync-token']) + ) { + return $this->calendarInfo['{http://sabredav.org/ns}sync-token']; + } + } + + /** + * The getChanges method returns all the changes that have happened, since + * the specified syncToken and the current collection. + * + * This function should return an array, such as the following: + * + * [ + * 'syncToken' => 'The current synctoken', + * 'added' => [ + * 'new.txt', + * ], + * 'modified' => [ + * 'modified.txt', + * ], + * 'deleted' => [ + * 'foo.php.bak', + * 'old.txt' + * ], + * 'result_truncated' : true + * ]; + * + * The syncToken property should reflect the *current* syncToken of the + * collection, as reported getSyncToken(). This is needed here too, to + * ensure the operation is atomic. + * + * If the syncToken is specified as null, this is an initial sync, and all + * members should be reported. + * + * If result is truncated due to server limitation or limit by client, + * set result_truncated to true, otherwise set to false or do not add the key. + * + * The modified property is an array of nodenames that have changed since + * the last token. + * + * The deleted property is an array with nodenames, that have been deleted + * from collection. + * + * The second argument is basically the 'depth' of the report. If it's 1, + * you only have to report changes that happened only directly in immediate + * descendants. If it's 2, it should also include changes from the nodes + * below the child collections. (grandchildren) + * + * The third (optional) argument allows a client to specify how many + * results should be returned at most. If the limit is not specified, it + * should be treated as infinite. + * + * If the limit (infinite or not) is higher than you're willing to return, + * the result should be truncated to fit the limit. + * Note that even when the result is truncated, syncToken must be consistent + * with the truncated result, not the result before truncation. + * (See RFC6578 Section 3.6 for detail) + * + * If the syncToken is expired (due to data cleanup) or unknown, you must + * return null. + * + * The limit is 'suggestive'. You are free to ignore it. + * TODO: RFC6578 Section 3.7 says that the server must fail when the server + * cannot truncate according to the limit, so it may not be just suggestive. + * + * @param string $syncToken + * @param int $syncLevel + * @param int $limit + * + * @return array|null + */ + public function getChanges($syncToken, $syncLevel, $limit = null) + { + if (!$this->caldavBackend instanceof Backend\SyncSupport) { + return null; + } + + return $this->caldavBackend->getChangesForCalendar( + $this->calendarInfo['id'], + $syncToken, + $syncLevel, + $limit + ); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/CalendarHome.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/CalendarHome.php new file mode 100644 index 0000000..49c54a3 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/CalendarHome.php @@ -0,0 +1,356 @@ +caldavBackend = $caldavBackend; + $this->principalInfo = $principalInfo; + } + + /** + * Returns the name of this object. + * + * @return string + */ + public function getName() + { + list(, $name) = Uri\split($this->principalInfo['uri']); + + return $name; + } + + /** + * Updates the name of this object. + * + * @param string $name + */ + public function setName($name) + { + throw new DAV\Exception\Forbidden(); + } + + /** + * Deletes this object. + */ + public function delete() + { + throw new DAV\Exception\Forbidden(); + } + + /** + * Returns the last modification date. + * + * @return int + */ + public function getLastModified() + { + return null; + } + + /** + * Creates a new file under this object. + * + * This is currently not allowed + * + * @param string $name + * @param resource $data + */ + public function createFile($name, $data = null) + { + throw new DAV\Exception\MethodNotAllowed('Creating new files in this collection is not supported'); + } + + /** + * Creates a new directory under this object. + * + * This is currently not allowed. + * + * @param string $filename + */ + public function createDirectory($filename) + { + throw new DAV\Exception\MethodNotAllowed('Creating new collections in this collection is not supported'); + } + + /** + * Returns a single calendar, by name. + * + * @param string $name + * + * @return Calendar + */ + public function getChild($name) + { + // Special nodes + if ('inbox' === $name && $this->caldavBackend instanceof Backend\SchedulingSupport) { + return new Schedule\Inbox($this->caldavBackend, $this->principalInfo['uri']); + } + if ('outbox' === $name && $this->caldavBackend instanceof Backend\SchedulingSupport) { + return new Schedule\Outbox($this->principalInfo['uri']); + } + if ('notifications' === $name && $this->caldavBackend instanceof Backend\NotificationSupport) { + return new Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']); + } + + // Calendars + foreach ($this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']) as $calendar) { + if ($calendar['uri'] === $name) { + if ($this->caldavBackend instanceof Backend\SharingSupport) { + return new SharedCalendar($this->caldavBackend, $calendar); + } else { + return new Calendar($this->caldavBackend, $calendar); + } + } + } + + if ($this->caldavBackend instanceof Backend\SubscriptionSupport) { + foreach ($this->caldavBackend->getSubscriptionsForUser($this->principalInfo['uri']) as $subscription) { + if ($subscription['uri'] === $name) { + return new Subscriptions\Subscription($this->caldavBackend, $subscription); + } + } + } + + throw new NotFound('Node with name \''.$name.'\' could not be found'); + } + + /** + * Checks if a calendar exists. + * + * @param string $name + * + * @return bool + */ + public function childExists($name) + { + try { + return (bool) $this->getChild($name); + } catch (NotFound $e) { + return false; + } + } + + /** + * Returns a list of calendars. + * + * @return array + */ + public function getChildren() + { + $calendars = $this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']); + $objs = []; + foreach ($calendars as $calendar) { + if ($this->caldavBackend instanceof Backend\SharingSupport) { + $objs[] = new SharedCalendar($this->caldavBackend, $calendar); + } else { + $objs[] = new Calendar($this->caldavBackend, $calendar); + } + } + + if ($this->caldavBackend instanceof Backend\SchedulingSupport) { + $objs[] = new Schedule\Inbox($this->caldavBackend, $this->principalInfo['uri']); + $objs[] = new Schedule\Outbox($this->principalInfo['uri']); + } + + // We're adding a notifications node, if it's supported by the backend. + if ($this->caldavBackend instanceof Backend\NotificationSupport) { + $objs[] = new Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']); + } + + // If the backend supports subscriptions, we'll add those as well, + if ($this->caldavBackend instanceof Backend\SubscriptionSupport) { + foreach ($this->caldavBackend->getSubscriptionsForUser($this->principalInfo['uri']) as $subscription) { + $objs[] = new Subscriptions\Subscription($this->caldavBackend, $subscription); + } + } + + return $objs; + } + + /** + * Creates a new calendar or subscription. + * + * @param string $name + * + * @throws DAV\Exception\InvalidResourceType + */ + public function createExtendedCollection($name, MkCol $mkCol) + { + $isCalendar = false; + $isSubscription = false; + foreach ($mkCol->getResourceType() as $rt) { + switch ($rt) { + case '{DAV:}collection': + case '{http://calendarserver.org/ns/}shared-owner': + // ignore + break; + case '{urn:ietf:params:xml:ns:caldav}calendar': + $isCalendar = true; + break; + case '{http://calendarserver.org/ns/}subscribed': + $isSubscription = true; + break; + default: + throw new DAV\Exception\InvalidResourceType('Unknown resourceType: '.$rt); + } + } + + $properties = $mkCol->getRemainingValues(); + $mkCol->setRemainingResultCode(201); + + if ($isSubscription) { + if (!$this->caldavBackend instanceof Backend\SubscriptionSupport) { + throw new DAV\Exception\InvalidResourceType('This backend does not support subscriptions'); + } + $this->caldavBackend->createSubscription($this->principalInfo['uri'], $name, $properties); + } elseif ($isCalendar) { + $this->caldavBackend->createCalendar($this->principalInfo['uri'], $name, $properties); + } else { + throw new DAV\Exception\InvalidResourceType('You can only create calendars and subscriptions in this collection'); + } + } + + /** + * Returns the owner of the calendar home. + * + * @return string + */ + public function getOwner() + { + return $this->principalInfo['uri']; + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + return [ + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->principalInfo['uri'], + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write', + 'principal' => $this->principalInfo['uri'], + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->principalInfo['uri'].'/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write', + 'principal' => $this->principalInfo['uri'].'/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->principalInfo['uri'].'/calendar-proxy-read', + 'protected' => true, + ], + ]; + } + + /** + * This method is called when a user replied to a request to share. + * + * This method should return the url of the newly created calendar if the + * share was accepted. + * + * @param string $href The sharee who is replying (often a mailto: address) + * @param int $status One of the SharingPlugin::STATUS_* constants + * @param string $calendarUri The url to the calendar thats being shared + * @param string $inReplyTo The unique id this message is a response to + * @param string $summary A description of the reply + * + * @return string|null + */ + public function shareReply($href, $status, $calendarUri, $inReplyTo, $summary = null) + { + if (!$this->caldavBackend instanceof Backend\SharingSupport) { + throw new DAV\Exception\NotImplemented('Sharing support is not implemented by this backend.'); + } + + return $this->caldavBackend->shareReply($href, $status, $calendarUri, $inReplyTo, $summary); + } + + /** + * Searches through all of a users calendars and calendar objects to find + * an object with a specific UID. + * + * This method should return the path to this object, relative to the + * calendar home, so this path usually only contains two parts: + * + * calendarpath/objectpath.ics + * + * If the uid is not found, return null. + * + * This method should only consider * objects that the principal owns, so + * any calendars owned by other principals that also appear in this + * collection should be ignored. + * + * @param string $uid + * + * @return string|null + */ + public function getCalendarObjectByUID($uid) + { + return $this->caldavBackend->getCalendarObjectByUID($this->principalInfo['uri'], $uid); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/CalendarObject.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/CalendarObject.php new file mode 100644 index 0000000..671f4b5 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/CalendarObject.php @@ -0,0 +1,223 @@ +caldavBackend = $caldavBackend; + + if (!isset($objectData['uri'])) { + throw new \InvalidArgumentException('The objectData argument must contain an \'uri\' property'); + } + + $this->calendarInfo = $calendarInfo; + $this->objectData = $objectData; + } + + /** + * Returns the uri for this object. + * + * @return string + */ + public function getName() + { + return $this->objectData['uri']; + } + + /** + * Returns the ICalendar-formatted object. + * + * @return string + */ + public function get() + { + // Pre-populating the 'calendardata' is optional, if we don't have it + // already we fetch it from the backend. + if (!isset($this->objectData['calendardata'])) { + $this->objectData = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $this->objectData['uri']); + } + + return $this->objectData['calendardata']; + } + + /** + * Updates the ICalendar-formatted object. + * + * @param string|resource $calendarData + * + * @return string + */ + public function put($calendarData) + { + if (is_resource($calendarData)) { + $calendarData = stream_get_contents($calendarData); + } + $etag = $this->caldavBackend->updateCalendarObject($this->calendarInfo['id'], $this->objectData['uri'], $calendarData); + $this->objectData['calendardata'] = $calendarData; + $this->objectData['etag'] = $etag; + + return $etag; + } + + /** + * Deletes the calendar object. + */ + public function delete() + { + $this->caldavBackend->deleteCalendarObject($this->calendarInfo['id'], $this->objectData['uri']); + } + + /** + * Returns the mime content-type. + * + * @return string + */ + public function getContentType() + { + $mime = 'text/calendar; charset=utf-8'; + if (isset($this->objectData['component']) && $this->objectData['component']) { + $mime .= '; component='.$this->objectData['component']; + } + + return $mime; + } + + /** + * Returns an ETag for this object. + * + * The ETag is an arbitrary string, but MUST be surrounded by double-quotes. + * + * @return string + */ + public function getETag() + { + if (isset($this->objectData['etag'])) { + return $this->objectData['etag']; + } else { + return '"'.md5($this->get()).'"'; + } + } + + /** + * Returns the last modification date as a unix timestamp. + * + * @return int + */ + public function getLastModified() + { + return $this->objectData['lastmodified']; + } + + /** + * Returns the size of this object in bytes. + * + * @return int + */ + public function getSize() + { + if (array_key_exists('size', $this->objectData)) { + return $this->objectData['size']; + } else { + return strlen($this->get()); + } + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->calendarInfo['principaluri']; + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + // An alternative acl may be specified in the object data. + if (isset($this->objectData['acl'])) { + return $this->objectData['acl']; + } + + // The default ACL + return [ + [ + 'privilege' => '{DAV:}all', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}all', + 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-read', + 'protected' => true, + ], + ]; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/CalendarQueryValidator.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/CalendarQueryValidator.php new file mode 100644 index 0000000..ee525da --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/CalendarQueryValidator.php @@ -0,0 +1,354 @@ +name !== $filters['name']) { + return false; + } + + return + $this->validateCompFilters($vObject, $filters['comp-filters']) && + $this->validatePropFilters($vObject, $filters['prop-filters']); + } + + /** + * This method checks the validity of comp-filters. + * + * A list of comp-filters needs to be specified. Also the parent of the + * component we're checking should be specified, not the component to check + * itself. + * + * @return bool + */ + protected function validateCompFilters(VObject\Component $parent, array $filters) + { + foreach ($filters as $filter) { + $isDefined = isset($parent->{$filter['name']}); + + if ($filter['is-not-defined']) { + if ($isDefined) { + return false; + } else { + continue; + } + } + if (!$isDefined) { + return false; + } + + if (array_key_exists('time-range', $filter) && $filter['time-range']) { + foreach ($parent->{$filter['name']} as $subComponent) { + $start = null; + $end = null; + if (array_key_exists('start', $filter['time-range'])) { + $start = $filter['time-range']['start']; + } + if (array_key_exists('end', $filter['time-range'])) { + $end = $filter['time-range']['end']; + } + if ($this->validateTimeRange($subComponent, $start, $end)) { + continue 2; + } + } + + return false; + } + + if (!$filter['comp-filters'] && !$filter['prop-filters']) { + continue; + } + + // If there are sub-filters, we need to find at least one component + // for which the subfilters hold true. + foreach ($parent->{$filter['name']} as $subComponent) { + if ( + $this->validateCompFilters($subComponent, $filter['comp-filters']) && + $this->validatePropFilters($subComponent, $filter['prop-filters'])) { + // We had a match, so this comp-filter succeeds + continue 2; + } + } + + // If we got here it means there were sub-comp-filters or + // sub-prop-filters and there was no match. This means this filter + // needs to return false. + return false; + } + + // If we got here it means we got through all comp-filters alive so the + // filters were all true. + return true; + } + + /** + * This method checks the validity of prop-filters. + * + * A list of prop-filters needs to be specified. Also the parent of the + * property we're checking should be specified, not the property to check + * itself. + * + * @return bool + */ + protected function validatePropFilters(VObject\Component $parent, array $filters) + { + foreach ($filters as $filter) { + $isDefined = isset($parent->{$filter['name']}); + + if ($filter['is-not-defined']) { + if ($isDefined) { + return false; + } else { + continue; + } + } + if (!$isDefined) { + return false; + } + + if (array_key_exists('time-range', $filter) && $filter['time-range']) { + foreach ($parent->{$filter['name']} as $subComponent) { + $start = null; + $end = null; + if (array_key_exists('start', $filter['time-range'])) { + $start = $filter['time-range']['start']; + } + if (array_key_exists('end', $filter['time-range'])) { + $end = $filter['time-range']['end']; + } + if ($this->validateTimeRange($subComponent, $start, $end)) { + continue 2; + } + } + + return false; + } + + if (!$filter['param-filters'] && !$filter['text-match']) { + continue; + } + + // If there are sub-filters, we need to find at least one property + // for which the subfilters hold true. + foreach ($parent->{$filter['name']} as $subComponent) { + if ( + $this->validateParamFilters($subComponent, $filter['param-filters']) && + (!$filter['text-match'] || $this->validateTextMatch($subComponent, $filter['text-match'])) + ) { + // We had a match, so this prop-filter succeeds + continue 2; + } + } + + // If we got here it means there were sub-param-filters or + // text-match filters and there was no match. This means the + // filter needs to return false. + return false; + } + + // If we got here it means we got through all prop-filters alive so the + // filters were all true. + return true; + } + + /** + * This method checks the validity of param-filters. + * + * A list of param-filters needs to be specified. Also the parent of the + * parameter we're checking should be specified, not the parameter to check + * itself. + * + * @return bool + */ + protected function validateParamFilters(VObject\Property $parent, array $filters) + { + foreach ($filters as $filter) { + $isDefined = isset($parent[$filter['name']]); + + if ($filter['is-not-defined']) { + if ($isDefined) { + return false; + } else { + continue; + } + } + if (!$isDefined) { + return false; + } + + if (!$filter['text-match']) { + continue; + } + + // If there are sub-filters, we need to find at least one parameter + // for which the subfilters hold true. + foreach ($parent[$filter['name']]->getParts() as $paramPart) { + if ($this->validateTextMatch($paramPart, $filter['text-match'])) { + // We had a match, so this param-filter succeeds + continue 2; + } + } + + // If we got here it means there was a text-match filter and there + // were no matches. This means the filter needs to return false. + return false; + } + + // If we got here it means we got through all param-filters alive so the + // filters were all true. + return true; + } + + /** + * This method checks the validity of a text-match. + * + * A single text-match should be specified as well as the specific property + * or parameter we need to validate. + * + * @param VObject\Node|string $check value to check against + * + * @return bool + */ + protected function validateTextMatch($check, array $textMatch) + { + if ($check instanceof VObject\Node) { + $check = $check->getValue(); + } + + $isMatching = \Sabre\DAV\StringUtil::textMatch($check, $textMatch['value'], $textMatch['collation']); + + return $textMatch['negate-condition'] xor $isMatching; + } + + /** + * Validates if a component matches the given time range. + * + * This is all based on the rules specified in rfc4791, which are quite + * complex. + * + * @param DateTime $start + * @param DateTime $end + * + * @return bool + */ + protected function validateTimeRange(VObject\Node $component, $start, $end) + { + if (is_null($start)) { + $start = new DateTime('1900-01-01'); + } + if (is_null($end)) { + $end = new DateTime('3000-01-01'); + } + + switch ($component->name) { + case 'VEVENT': + case 'VTODO': + case 'VJOURNAL': + return $component->isInTimeRange($start, $end); + + case 'VALARM': + // If the valarm is wrapped in a recurring event, we need to + // expand the recursions, and validate each. + // + // Our datamodel doesn't easily allow us to do this straight + // in the VALARM component code, so this is a hack, and an + // expensive one too. + if ('VEVENT' === $component->parent->name && $component->parent->RRULE) { + // Fire up the iterator! + $it = new VObject\Recur\EventIterator($component->parent->parent, (string) $component->parent->UID); + while ($it->valid()) { + $expandedEvent = $it->getEventObject(); + + // We need to check from these expanded alarms, which + // one is the first to trigger. Based on this, we can + // determine if we can 'give up' expanding events. + $firstAlarm = null; + if (null !== $expandedEvent->VALARM) { + foreach ($expandedEvent->VALARM as $expandedAlarm) { + $effectiveTrigger = $expandedAlarm->getEffectiveTriggerTime(); + if ($expandedAlarm->isInTimeRange($start, $end)) { + return true; + } + + if ('DATE-TIME' === (string) $expandedAlarm->TRIGGER['VALUE']) { + // This is an alarm with a non-relative trigger + // time, likely created by a buggy client. The + // implication is that every alarm in this + // recurring event trigger at the exact same + // time. It doesn't make sense to traverse + // further. + } else { + // We store the first alarm as a means to + // figure out when we can stop traversing. + if (!$firstAlarm || $effectiveTrigger < $firstAlarm) { + $firstAlarm = $effectiveTrigger; + } + } + } + } + if (is_null($firstAlarm)) { + // No alarm was found. + // + // Or technically: No alarm that will change for + // every instance of the recurrence was found, + // which means we can assume there was no match. + return false; + } + if ($firstAlarm > $end) { + return false; + } + $it->next(); + } + + return false; + } else { + return $component->isInTimeRange($start, $end); + } + + // no break + case 'VFREEBUSY': + throw new \Sabre\DAV\Exception\NotImplemented('time-range filters are currently not supported on '.$component->name.' components'); + case 'COMPLETED': + case 'CREATED': + case 'DTEND': + case 'DTSTAMP': + case 'DTSTART': + case 'DUE': + case 'LAST-MODIFIED': + return $start <= $component->getDateTime() && $end >= $component->getDateTime(); + + default: + throw new \Sabre\DAV\Exception\BadRequest('You cannot create a time-range filter on a '.$component->name.' component'); + } + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/CalendarRoot.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/CalendarRoot.php new file mode 100644 index 0000000..3038d21 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/CalendarRoot.php @@ -0,0 +1,75 @@ +caldavBackend = $caldavBackend; + } + + /** + * Returns the nodename. + * + * We're overriding this, because the default will be the 'principalPrefix', + * and we want it to be Sabre\CalDAV\Plugin::CALENDAR_ROOT + * + * @return string + */ + public function getName() + { + return Plugin::CALENDAR_ROOT; + } + + /** + * This method returns a node for a principal. + * + * The passed array contains principal information, and is guaranteed to + * at least contain a uri item. Other properties may or may not be + * supplied by the authentication backend. + * + * @return \Sabre\DAV\INode + */ + public function getChildForPrincipal(array $principal) + { + return new CalendarHome($this->caldavBackend, $principal); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php new file mode 100644 index 0000000..e94378a --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php @@ -0,0 +1,31 @@ +ownerDocument; + + $np = $doc->createElementNS(CalDAV\Plugin::NS_CALDAV, 'cal:supported-calendar-component'); + $errorNode->appendChild($np); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/ICSExportPlugin.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/ICSExportPlugin.php new file mode 100644 index 0000000..9171e36 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/ICSExportPlugin.php @@ -0,0 +1,377 @@ +server = $server; + $server->on('method:GET', [$this, 'httpGet'], 90); + $server->on('browserButtonActions', function ($path, $node, &$actions) { + if ($node instanceof ICalendar) { + $actions .= ''; + } + }); + } + + /** + * Intercepts GET requests on calendar urls ending with ?export. + * + * @throws BadRequest + * @throws DAV\Exception\NotFound + * @throws VObject\InvalidDataException + * + * @return bool + */ + public function httpGet(RequestInterface $request, ResponseInterface $response) + { + $queryParams = $request->getQueryParameters(); + if (!array_key_exists('export', $queryParams)) { + return; + } + + $path = $request->getPath(); + + $node = $this->server->getProperties($path, [ + '{DAV:}resourcetype', + '{DAV:}displayname', + '{http://sabredav.org/ns}sync-token', + '{DAV:}sync-token', + '{http://apple.com/ns/ical/}calendar-color', + ]); + + if (!isset($node['{DAV:}resourcetype']) || !$node['{DAV:}resourcetype']->is('{'.Plugin::NS_CALDAV.'}calendar')) { + return; + } + // Marking the transactionType, for logging purposes. + $this->server->transactionType = 'get-calendar-export'; + + $properties = $node; + + $start = null; + $end = null; + $expand = false; + $componentType = false; + if (isset($queryParams['start'])) { + if (!ctype_digit($queryParams['start'])) { + throw new BadRequest('The start= parameter must contain a unix timestamp'); + } + $start = DateTime::createFromFormat('U', $queryParams['start']); + } + if (isset($queryParams['end'])) { + if (!ctype_digit($queryParams['end'])) { + throw new BadRequest('The end= parameter must contain a unix timestamp'); + } + $end = DateTime::createFromFormat('U', $queryParams['end']); + } + if (isset($queryParams['expand']) && (bool) $queryParams['expand']) { + if (!$start || !$end) { + throw new BadRequest('If you\'d like to expand recurrences, you must specify both a start= and end= parameter.'); + } + $expand = true; + $componentType = 'VEVENT'; + } + if (isset($queryParams['componentType'])) { + if (!in_array($queryParams['componentType'], ['VEVENT', 'VTODO', 'VJOURNAL'])) { + throw new BadRequest('You are not allowed to search for components of type: '.$queryParams['componentType'].' here'); + } + $componentType = $queryParams['componentType']; + } + + $format = \Sabre\HTTP\negotiateContentType( + $request->getHeader('Accept'), + [ + 'text/calendar', + 'application/calendar+json', + ] + ); + + if (isset($queryParams['accept'])) { + if ('application/calendar+json' === $queryParams['accept'] || 'jcal' === $queryParams['accept']) { + $format = 'application/calendar+json'; + } + } + if (!$format) { + $format = 'text/calendar'; + } + + $this->generateResponse($path, $start, $end, $expand, $componentType, $format, $properties, $response); + + // Returning false to break the event chain + return false; + } + + /** + * This method is responsible for generating the actual, full response. + * + * @param string $path + * @param DateTime|null $start + * @param DateTime|null $end + * @param bool $expand + * @param string $componentType + * @param string $format + * @param array $properties + * + * @throws DAV\Exception\NotFound + * @throws VObject\InvalidDataException + */ + protected function generateResponse($path, $start, $end, $expand, $componentType, $format, $properties, ResponseInterface $response) + { + $calDataProp = '{'.Plugin::NS_CALDAV.'}calendar-data'; + $calendarNode = $this->server->tree->getNodeForPath($path); + + $blobs = []; + if ($start || $end || $componentType) { + // If there was a start or end filter, we need to enlist + // calendarQuery for speed. + $queryResult = $calendarNode->calendarQuery([ + 'name' => 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => $componentType, + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => [ + 'start' => $start, + 'end' => $end, + ], + ], + ], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]); + + // queryResult is just a list of base urls. We need to prefix the + // calendar path. + $queryResult = array_map( + function ($item) use ($path) { + return $path.'/'.$item; + }, + $queryResult + ); + $nodes = $this->server->getPropertiesForMultiplePaths($queryResult, [$calDataProp]); + unset($queryResult); + } else { + $nodes = $this->server->getPropertiesForPath($path, [$calDataProp], 1); + } + + // Flattening the arrays + foreach ($nodes as $node) { + if (isset($node[200][$calDataProp])) { + $blobs[$node['href']] = $node[200][$calDataProp]; + } + } + unset($nodes); + + $mergedCalendar = $this->mergeObjects( + $properties, + $blobs + ); + + if ($expand) { + $calendarTimeZone = null; + // We're expanding, and for that we need to figure out the + // calendar's timezone. + $tzProp = '{'.Plugin::NS_CALDAV.'}calendar-timezone'; + $tzResult = $this->server->getProperties($path, [$tzProp]); + if (isset($tzResult[$tzProp])) { + // This property contains a VCALENDAR with a single + // VTIMEZONE. + $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]); + $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone(); + // Destroy circular references to PHP will GC the object. + $vtimezoneObj->destroy(); + unset($vtimezoneObj); + } else { + // Defaulting to UTC. + $calendarTimeZone = new DateTimeZone('UTC'); + } + + $mergedCalendar = $mergedCalendar->expand($start, $end, $calendarTimeZone); + } + + $filenameExtension = '.ics'; + + switch ($format) { + case 'text/calendar': + $mergedCalendar = $mergedCalendar->serialize(); + $filenameExtension = '.ics'; + break; + case 'application/calendar+json': + $mergedCalendar = json_encode($mergedCalendar->jsonSerialize()); + $filenameExtension = '.json'; + break; + } + + $filename = preg_replace( + '/[^a-zA-Z0-9-_ ]/um', + '', + $calendarNode->getName() + ); + $filename .= '-'.date('Y-m-d').$filenameExtension; + + $response->setHeader('Content-Disposition', 'attachment; filename="'.$filename.'"'); + $response->setHeader('Content-Type', $format); + + $response->setStatus(200); + $response->setBody($mergedCalendar); + } + + /** + * Merges all calendar objects, and builds one big iCalendar blob. + * + * @param array $properties Some CalDAV properties + * + * @return VObject\Component\VCalendar + */ + public function mergeObjects(array $properties, array $inputObjects) + { + $calendar = new VObject\Component\VCalendar(); + $calendar->VERSION = '2.0'; + if (DAV\Server::$exposeVersion) { + $calendar->PRODID = '-//SabreDAV//SabreDAV '.DAV\Version::VERSION.'//EN'; + } else { + $calendar->PRODID = '-//SabreDAV//SabreDAV//EN'; + } + if (isset($properties['{DAV:}displayname'])) { + $calendar->{'X-WR-CALNAME'} = $properties['{DAV:}displayname']; + } + if (isset($properties['{http://apple.com/ns/ical/}calendar-color'])) { + $calendar->{'X-APPLE-CALENDAR-COLOR'} = $properties['{http://apple.com/ns/ical/}calendar-color']; + } + + $collectedTimezones = []; + + $timezones = []; + $objects = []; + + foreach ($inputObjects as $href => $inputObject) { + $nodeComp = VObject\Reader::read($inputObject); + + foreach ($nodeComp->children() as $child) { + switch ($child->name) { + case 'VEVENT': + case 'VTODO': + case 'VJOURNAL': + $objects[] = clone $child; + break; + + // VTIMEZONE is special, because we need to filter out the duplicates + case 'VTIMEZONE': + // Naively just checking tzid. + if (in_array((string) $child->TZID, $collectedTimezones)) { + break; + } + + $timezones[] = clone $child; + $collectedTimezones[] = $child->TZID; + break; + } + } + // Destroy circular references to PHP will GC the object. + $nodeComp->destroy(); + unset($nodeComp); + } + + foreach ($timezones as $tz) { + $calendar->add($tz); + } + foreach ($objects as $obj) { + $calendar->add($obj); + } + + return $calendar; + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using \Sabre\DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'ics-export'; + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds the ability to export CalDAV calendars as a single iCalendar file.', + 'link' => 'http://sabre.io/dav/ics-export-plugin/', + ]; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/ICalendar.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/ICalendar.php new file mode 100644 index 0000000..8636e0b --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/ICalendar.php @@ -0,0 +1,20 @@ +caldavBackend = $caldavBackend; + $this->principalUri = $principalUri; + } + + /** + * Returns all notifications for a principal. + * + * @return array + */ + public function getChildren() + { + $children = []; + $notifications = $this->caldavBackend->getNotificationsForPrincipal($this->principalUri); + + foreach ($notifications as $notification) { + $children[] = new Node( + $this->caldavBackend, + $this->principalUri, + $notification + ); + } + + return $children; + } + + /** + * Returns the name of this object. + * + * @return string + */ + public function getName() + { + return 'notifications'; + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->principalUri; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Notifications/ICollection.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Notifications/ICollection.php new file mode 100644 index 0000000..b12fb39 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Notifications/ICollection.php @@ -0,0 +1,25 @@ +caldavBackend = $caldavBackend; + $this->principalUri = $principalUri; + $this->notification = $notification; + } + + /** + * Returns the path name for this notification. + * + * @return string + */ + public function getName() + { + return $this->notification->getId().'.xml'; + } + + /** + * Returns the etag for the notification. + * + * The etag must be surrounded by literal double-quotes. + * + * @return string + */ + public function getETag() + { + return $this->notification->getETag(); + } + + /** + * This method must return an xml element, using the + * Sabre\CalDAV\Xml\Notification\NotificationInterface classes. + * + * @return NotificationInterface + */ + public function getNotificationType() + { + return $this->notification; + } + + /** + * Deletes this notification. + */ + public function delete() + { + $this->caldavBackend->deleteNotification($this->getOwner(), $this->notification); + } + + /** + * Returns the owner principal. + * + * This must be an url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->principalUri; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Notifications/Plugin.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Notifications/Plugin.php new file mode 100644 index 0000000..56b2fe9 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Notifications/Plugin.php @@ -0,0 +1,161 @@ +server = $server; + $server->on('method:GET', [$this, 'httpGet'], 90); + $server->on('propFind', [$this, 'propFind']); + + $server->xml->namespaceMap[self::NS_CALENDARSERVER] = 'cs'; + $server->resourceTypeMapping['\\Sabre\\CalDAV\\Notifications\\ICollection'] = '{'.self::NS_CALENDARSERVER.'}notification'; + + array_push($server->protectedProperties, + '{'.self::NS_CALENDARSERVER.'}notification-URL', + '{'.self::NS_CALENDARSERVER.'}notificationtype' + ); + } + + /** + * PropFind. + */ + public function propFind(PropFind $propFind, BaseINode $node) + { + $caldavPlugin = $this->server->getPlugin('caldav'); + + if ($node instanceof DAVACL\IPrincipal) { + $principalUrl = $node->getPrincipalUrl(); + + // notification-URL property + $propFind->handle('{'.self::NS_CALENDARSERVER.'}notification-URL', function () use ($principalUrl, $caldavPlugin) { + $notificationPath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl).'/notifications/'; + + return new DAV\Xml\Property\Href($notificationPath); + }); + } + + if ($node instanceof INode) { + $propFind->handle( + '{'.self::NS_CALENDARSERVER.'}notificationtype', + [$node, 'getNotificationType'] + ); + } + } + + /** + * This event is triggered before the usual GET request handler. + * + * We use this to intercept GET calls to notification nodes, and return the + * proper response. + */ + public function httpGet(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + + try { + $node = $this->server->tree->getNodeForPath($path); + } catch (DAV\Exception\NotFound $e) { + return; + } + + if (!$node instanceof INode) { + return; + } + + $writer = $this->server->xml->getWriter(); + $writer->contextUri = $this->server->getBaseUri(); + $writer->openMemory(); + $writer->startDocument('1.0', 'UTF-8'); + $writer->startElement('{http://calendarserver.org/ns/}notification'); + $node->getNotificationType()->xmlSerializeFull($writer); + $writer->endElement(); + + $response->setHeader('Content-Type', 'application/xml'); + $response->setHeader('ETag', $node->getETag()); + $response->setStatus(200); + $response->setBody($writer->outputMemory()); + + // Return false to break the event chain. + return false; + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds support for caldav-notifications, which is required to enable caldav-sharing.', + 'link' => 'http://sabre.io/dav/caldav-sharing/', + ]; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Plugin.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Plugin.php new file mode 100644 index 0000000..ccb722f --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Plugin.php @@ -0,0 +1,1011 @@ +server->tree->getNodeForPath($parent); + + if ($node instanceof DAV\IExtendedCollection) { + try { + $node->getChild($name); + } catch (DAV\Exception\NotFound $e) { + return ['MKCALENDAR']; + } + } + + return []; + } + + /** + * Returns the path to a principal's calendar home. + * + * The return url must not end with a slash. + * This function should return null in case a principal did not have + * a calendar home. + * + * @param string $principalUrl + * + * @return string + */ + public function getCalendarHomeForPrincipal($principalUrl) + { + // The default behavior for most sabre/dav servers is that there is a + // principals root node, which contains users directly under it. + // + // This function assumes that there are two components in a principal + // path. If there's more, we don't return a calendar home. This + // excludes things like the calendar-proxy-read principal (which it + // should). + $parts = explode('/', trim($principalUrl, '/')); + if (2 !== count($parts)) { + return; + } + if ('principals' !== $parts[0]) { + return; + } + + return self::CALENDAR_ROOT.'/'.$parts[1]; + } + + /** + * Returns a list of features for the DAV: HTTP header. + * + * @return array + */ + public function getFeatures() + { + return ['calendar-access', 'calendar-proxy']; + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'caldav'; + } + + /** + * Returns a list of reports this plugin supports. + * + * This will be used in the {DAV:}supported-report-set property. + * Note that you still need to subscribe to the 'report' event to actually + * implement them + * + * @param string $uri + * + * @return array + */ + public function getSupportedReportSet($uri) + { + $node = $this->server->tree->getNodeForPath($uri); + + $reports = []; + if ($node instanceof ICalendarObjectContainer || $node instanceof ICalendarObject) { + $reports[] = '{'.self::NS_CALDAV.'}calendar-multiget'; + $reports[] = '{'.self::NS_CALDAV.'}calendar-query'; + } + if ($node instanceof ICalendar) { + $reports[] = '{'.self::NS_CALDAV.'}free-busy-query'; + } + // iCal has a bug where it assumes that sync support is enabled, only + // if we say we support it on the calendar-home, even though this is + // not actually the case. + if ($node instanceof CalendarHome && $this->server->getPlugin('sync')) { + $reports[] = '{DAV:}sync-collection'; + } + + return $reports; + } + + /** + * Initializes the plugin. + */ + public function initialize(DAV\Server $server) + { + $this->server = $server; + + $server->on('method:MKCALENDAR', [$this, 'httpMkCalendar']); + $server->on('report', [$this, 'report']); + $server->on('propFind', [$this, 'propFind']); + $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']); + $server->on('beforeCreateFile', [$this, 'beforeCreateFile']); + $server->on('beforeWriteContent', [$this, 'beforeWriteContent']); + $server->on('afterMethod:GET', [$this, 'httpAfterGET']); + $server->on('getSupportedPrivilegeSet', [$this, 'getSupportedPrivilegeSet']); + + $server->xml->namespaceMap[self::NS_CALDAV] = 'cal'; + $server->xml->namespaceMap[self::NS_CALENDARSERVER] = 'cs'; + + $server->xml->elementMap['{'.self::NS_CALDAV.'}supported-calendar-component-set'] = 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet'; + $server->xml->elementMap['{'.self::NS_CALDAV.'}calendar-query'] = 'Sabre\\CalDAV\\Xml\\Request\\CalendarQueryReport'; + $server->xml->elementMap['{'.self::NS_CALDAV.'}calendar-multiget'] = 'Sabre\\CalDAV\\Xml\\Request\\CalendarMultiGetReport'; + $server->xml->elementMap['{'.self::NS_CALDAV.'}free-busy-query'] = 'Sabre\\CalDAV\\Xml\\Request\\FreeBusyQueryReport'; + $server->xml->elementMap['{'.self::NS_CALDAV.'}mkcalendar'] = 'Sabre\\CalDAV\\Xml\\Request\\MkCalendar'; + $server->xml->elementMap['{'.self::NS_CALDAV.'}schedule-calendar-transp'] = 'Sabre\\CalDAV\\Xml\\Property\\ScheduleCalendarTransp'; + $server->xml->elementMap['{'.self::NS_CALDAV.'}supported-calendar-component-set'] = 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet'; + + $server->resourceTypeMapping['\\Sabre\\CalDAV\\ICalendar'] = '{urn:ietf:params:xml:ns:caldav}calendar'; + + $server->resourceTypeMapping['\\Sabre\\CalDAV\\Principal\\IProxyRead'] = '{http://calendarserver.org/ns/}calendar-proxy-read'; + $server->resourceTypeMapping['\\Sabre\\CalDAV\\Principal\\IProxyWrite'] = '{http://calendarserver.org/ns/}calendar-proxy-write'; + + array_push($server->protectedProperties, + '{'.self::NS_CALDAV.'}supported-calendar-component-set', + '{'.self::NS_CALDAV.'}supported-calendar-data', + '{'.self::NS_CALDAV.'}max-resource-size', + '{'.self::NS_CALDAV.'}min-date-time', + '{'.self::NS_CALDAV.'}max-date-time', + '{'.self::NS_CALDAV.'}max-instances', + '{'.self::NS_CALDAV.'}max-attendees-per-instance', + '{'.self::NS_CALDAV.'}calendar-home-set', + '{'.self::NS_CALDAV.'}supported-collation-set', + '{'.self::NS_CALDAV.'}calendar-data', + + // CalendarServer extensions + '{'.self::NS_CALENDARSERVER.'}getctag', + '{'.self::NS_CALENDARSERVER.'}calendar-proxy-read-for', + '{'.self::NS_CALENDARSERVER.'}calendar-proxy-write-for' + ); + + if ($aclPlugin = $server->getPlugin('acl')) { + $aclPlugin->principalSearchPropertySet['{'.self::NS_CALDAV.'}calendar-user-address-set'] = 'Calendar address'; + } + } + + /** + * This functions handles REPORT requests specific to CalDAV. + * + * @param string $reportName + * @param mixed $report + * @param mixed $path + * + * @return bool|null + */ + public function report($reportName, $report, $path) + { + switch ($reportName) { + case '{'.self::NS_CALDAV.'}calendar-multiget': + $this->server->transactionType = 'report-calendar-multiget'; + $this->calendarMultiGetReport($report); + + return false; + case '{'.self::NS_CALDAV.'}calendar-query': + $this->server->transactionType = 'report-calendar-query'; + $this->calendarQueryReport($report); + + return false; + case '{'.self::NS_CALDAV.'}free-busy-query': + $this->server->transactionType = 'report-free-busy-query'; + $this->freeBusyQueryReport($report); + + return false; + } + } + + /** + * This function handles the MKCALENDAR HTTP method, which creates + * a new calendar. + * + * @return bool + */ + public function httpMkCalendar(RequestInterface $request, ResponseInterface $response) + { + $body = $request->getBodyAsString(); + $path = $request->getPath(); + + $properties = []; + + if ($body) { + try { + $mkcalendar = $this->server->xml->expect( + '{urn:ietf:params:xml:ns:caldav}mkcalendar', + $body + ); + } catch (\Sabre\Xml\ParseException $e) { + throw new BadRequest($e->getMessage(), 0, $e); + } + $properties = $mkcalendar->getProperties(); + } + + // iCal abuses MKCALENDAR since iCal 10.9.2 to create server-stored + // subscriptions. Before that it used MKCOL which was the correct way + // to do this. + // + // If the body had a {DAV:}resourcetype, it means we stumbled upon this + // request, and we simply use it instead of the pre-defined list. + if (isset($properties['{DAV:}resourcetype'])) { + $resourceType = $properties['{DAV:}resourcetype']->getValue(); + } else { + $resourceType = ['{DAV:}collection', '{urn:ietf:params:xml:ns:caldav}calendar']; + } + + $this->server->createCollection($path, new MkCol($resourceType, $properties)); + + $response->setStatus(201); + $response->setHeader('Content-Length', 0); + + // This breaks the method chain. + return false; + } + + /** + * PropFind. + * + * This method handler is invoked before any after properties for a + * resource are fetched. This allows us to add in any CalDAV specific + * properties. + */ + public function propFind(DAV\PropFind $propFind, DAV\INode $node) + { + $ns = '{'.self::NS_CALDAV.'}'; + + if ($node instanceof ICalendarObjectContainer) { + $propFind->handle($ns.'max-resource-size', $this->maxResourceSize); + $propFind->handle($ns.'supported-calendar-data', function () { + return new Xml\Property\SupportedCalendarData(); + }); + $propFind->handle($ns.'supported-collation-set', function () { + return new Xml\Property\SupportedCollationSet(); + }); + } + + if ($node instanceof DAVACL\IPrincipal) { + $principalUrl = $node->getPrincipalUrl(); + + $propFind->handle('{'.self::NS_CALDAV.'}calendar-home-set', function () use ($principalUrl) { + $calendarHomePath = $this->getCalendarHomeForPrincipal($principalUrl); + if (is_null($calendarHomePath)) { + return null; + } + + return new LocalHref($calendarHomePath.'/'); + }); + // The calendar-user-address-set property is basically mapped to + // the {DAV:}alternate-URI-set property. + $propFind->handle('{'.self::NS_CALDAV.'}calendar-user-address-set', function () use ($node) { + $addresses = $node->getAlternateUriSet(); + $addresses[] = $this->server->getBaseUri().$node->getPrincipalUrl().'/'; + + return new LocalHref($addresses); + }); + // For some reason somebody thought it was a good idea to add + // another one of these properties. We're supporting it too. + $propFind->handle('{'.self::NS_CALENDARSERVER.'}email-address-set', function () use ($node) { + $addresses = $node->getAlternateUriSet(); + $emails = []; + foreach ($addresses as $address) { + if ('mailto:' === substr($address, 0, 7)) { + $emails[] = substr($address, 7); + } + } + + return new Xml\Property\EmailAddressSet($emails); + }); + + // These two properties are shortcuts for ical to easily find + // other principals this principal has access to. + $propRead = '{'.self::NS_CALENDARSERVER.'}calendar-proxy-read-for'; + $propWrite = '{'.self::NS_CALENDARSERVER.'}calendar-proxy-write-for'; + + if (404 === $propFind->getStatus($propRead) || 404 === $propFind->getStatus($propWrite)) { + $aclPlugin = $this->server->getPlugin('acl'); + $membership = $aclPlugin->getPrincipalMembership($propFind->getPath()); + $readList = []; + $writeList = []; + + foreach ($membership as $group) { + $groupNode = $this->server->tree->getNodeForPath($group); + + $listItem = Uri\split($group)[0].'/'; + + // If the node is either ap proxy-read or proxy-write + // group, we grab the parent principal and add it to the + // list. + if ($groupNode instanceof Principal\IProxyRead) { + $readList[] = $listItem; + } + if ($groupNode instanceof Principal\IProxyWrite) { + $writeList[] = $listItem; + } + } + + $propFind->set($propRead, new LocalHref($readList)); + $propFind->set($propWrite, new LocalHref($writeList)); + } + } // instanceof IPrincipal + + if ($node instanceof ICalendarObject) { + // The calendar-data property is not supposed to be a 'real' + // property, but in large chunks of the spec it does act as such. + // Therefore we simply expose it as a property. + $propFind->handle('{'.self::NS_CALDAV.'}calendar-data', function () use ($node) { + $val = $node->get(); + if (is_resource($val)) { + $val = stream_get_contents($val); + } + + // Taking out \r to not screw up the xml output + return str_replace("\r", '', $val); + }); + } + } + + /** + * This function handles the calendar-multiget REPORT. + * + * This report is used by the client to fetch the content of a series + * of urls. Effectively avoiding a lot of redundant requests. + * + * @param CalendarMultiGetReport $report + */ + public function calendarMultiGetReport($report) + { + $needsJson = 'application/calendar+json' === $report->contentType; + + $timeZones = []; + $propertyList = []; + + $paths = array_map( + [$this->server, 'calculateUri'], + $report->hrefs + ); + + foreach ($this->server->getPropertiesForMultiplePaths($paths, $report->properties) as $uri => $objProps) { + if (($needsJson || $report->expand) && isset($objProps[200]['{'.self::NS_CALDAV.'}calendar-data'])) { + $vObject = VObject\Reader::read($objProps[200]['{'.self::NS_CALDAV.'}calendar-data']); + + if ($report->expand) { + // We're expanding, and for that we need to figure out the + // calendar's timezone. + list($calendarPath) = Uri\split($uri); + if (!isset($timeZones[$calendarPath])) { + // Checking the calendar-timezone property. + $tzProp = '{'.self::NS_CALDAV.'}calendar-timezone'; + $tzResult = $this->server->getProperties($calendarPath, [$tzProp]); + if (isset($tzResult[$tzProp])) { + // This property contains a VCALENDAR with a single + // VTIMEZONE. + $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]); + $timeZone = $vtimezoneObj->VTIMEZONE->getTimeZone(); + } else { + // Defaulting to UTC. + $timeZone = new DateTimeZone('UTC'); + } + $timeZones[$calendarPath] = $timeZone; + } + + $vObject = $vObject->expand($report->expand['start'], $report->expand['end'], $timeZones[$calendarPath]); + } + if ($needsJson) { + $objProps[200]['{'.self::NS_CALDAV.'}calendar-data'] = json_encode($vObject->jsonSerialize()); + } else { + $objProps[200]['{'.self::NS_CALDAV.'}calendar-data'] = $vObject->serialize(); + } + // Destroy circular references so PHP will garbage collect the + // object. + $vObject->destroy(); + } + + $propertyList[] = $objProps; + } + + $prefer = $this->server->getHTTPPrefer(); + + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer'); + $this->server->httpResponse->setBody($this->server->generateMultiStatus($propertyList, 'minimal' === $prefer['return'])); + } + + /** + * This function handles the calendar-query REPORT. + * + * This report is used by clients to request calendar objects based on + * complex conditions. + * + * @param Xml\Request\CalendarQueryReport $report + */ + public function calendarQueryReport($report) + { + $path = $this->server->getRequestUri(); + + $needsJson = 'application/calendar+json' === $report->contentType; + + $node = $this->server->tree->getNodeForPath($this->server->getRequestUri()); + $depth = $this->server->getHTTPDepth(0); + + // The default result is an empty array + $result = []; + + $calendarTimeZone = null; + if ($report->expand) { + // We're expanding, and for that we need to figure out the + // calendar's timezone. + $tzProp = '{'.self::NS_CALDAV.'}calendar-timezone'; + $tzResult = $this->server->getProperties($path, [$tzProp]); + if (isset($tzResult[$tzProp])) { + // This property contains a VCALENDAR with a single + // VTIMEZONE. + $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]); + $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone(); + + // Destroy circular references so PHP will garbage collect the + // object. + $vtimezoneObj->destroy(); + } else { + // Defaulting to UTC. + $calendarTimeZone = new DateTimeZone('UTC'); + } + } + + // The calendarobject was requested directly. In this case we handle + // this locally. + if (0 == $depth && $node instanceof ICalendarObject) { + $requestedCalendarData = true; + $requestedProperties = $report->properties; + + if (!in_array('{urn:ietf:params:xml:ns:caldav}calendar-data', $requestedProperties)) { + // We always retrieve calendar-data, as we need it for filtering. + $requestedProperties[] = '{urn:ietf:params:xml:ns:caldav}calendar-data'; + + // If calendar-data wasn't explicitly requested, we need to remove + // it after processing. + $requestedCalendarData = false; + } + + $properties = $this->server->getPropertiesForPath( + $path, + $requestedProperties, + 0 + ); + + // This array should have only 1 element, the first calendar + // object. + $properties = current($properties); + + // If there wasn't any calendar-data returned somehow, we ignore + // this. + if (isset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data'])) { + $validator = new CalendarQueryValidator(); + + $vObject = VObject\Reader::read($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']); + if ($validator->validate($vObject, $report->filters)) { + // If the client didn't require the calendar-data property, + // we won't give it back. + if (!$requestedCalendarData) { + unset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']); + } else { + if ($report->expand) { + $vObject = $vObject->expand($report->expand['start'], $report->expand['end'], $calendarTimeZone); + } + if ($needsJson) { + $properties[200]['{'.self::NS_CALDAV.'}calendar-data'] = json_encode($vObject->jsonSerialize()); + } elseif ($report->expand) { + $properties[200]['{'.self::NS_CALDAV.'}calendar-data'] = $vObject->serialize(); + } + } + + $result = [$properties]; + } + // Destroy circular references so PHP will garbage collect the + // object. + $vObject->destroy(); + } + } + + if ($node instanceof ICalendarObjectContainer && 0 === $depth) { + if (0 === strpos((string) $this->server->httpRequest->getHeader('User-Agent'), 'MSFT-')) { + // Microsoft clients incorrectly supplied depth as 0, when it actually + // should have set depth to 1. We're implementing a workaround here + // to deal with this. + // + // This targets at least the following clients: + // Windows 10 + // Windows Phone 8, 10 + $depth = 1; + } else { + throw new BadRequest('A calendar-query REPORT on a calendar with a Depth: 0 is undefined. Set Depth to 1'); + } + } + + // If we're dealing with a calendar, the calendar itself is responsible + // for the calendar-query. + if ($node instanceof ICalendarObjectContainer && 1 == $depth) { + $nodePaths = $node->calendarQuery($report->filters); + + foreach ($nodePaths as $path) { + list($properties) = + $this->server->getPropertiesForPath($this->server->getRequestUri().'/'.$path, $report->properties); + + if (($needsJson || $report->expand)) { + $vObject = VObject\Reader::read($properties[200]['{'.self::NS_CALDAV.'}calendar-data']); + + if ($report->expand) { + $vObject = $vObject->expand($report->expand['start'], $report->expand['end'], $calendarTimeZone); + } + + if ($needsJson) { + $properties[200]['{'.self::NS_CALDAV.'}calendar-data'] = json_encode($vObject->jsonSerialize()); + } else { + $properties[200]['{'.self::NS_CALDAV.'}calendar-data'] = $vObject->serialize(); + } + + // Destroy circular references so PHP will garbage collect the + // object. + $vObject->destroy(); + } + $result[] = $properties; + } + } + + $prefer = $this->server->getHTTPPrefer(); + + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer'); + $this->server->httpResponse->setBody($this->server->generateMultiStatus($result, 'minimal' === $prefer['return'])); + } + + /** + * This method is responsible for parsing the request and generating the + * response for the CALDAV:free-busy-query REPORT. + */ + protected function freeBusyQueryReport(Xml\Request\FreeBusyQueryReport $report) + { + $uri = $this->server->getRequestUri(); + + $acl = $this->server->getPlugin('acl'); + if ($acl) { + $acl->checkPrivileges($uri, '{'.self::NS_CALDAV.'}read-free-busy'); + } + + $calendar = $this->server->tree->getNodeForPath($uri); + if (!$calendar instanceof ICalendar) { + throw new DAV\Exception\NotImplemented('The free-busy-query REPORT is only implemented on calendars'); + } + + $tzProp = '{'.self::NS_CALDAV.'}calendar-timezone'; + + // Figuring out the default timezone for the calendar, for floating + // times. + $calendarProps = $this->server->getProperties($uri, [$tzProp]); + + if (isset($calendarProps[$tzProp])) { + $vtimezoneObj = VObject\Reader::read($calendarProps[$tzProp]); + $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone(); + // Destroy circular references so PHP will garbage collect the object. + $vtimezoneObj->destroy(); + } else { + $calendarTimeZone = new DateTimeZone('UTC'); + } + + // Doing a calendar-query first, to make sure we get the most + // performance. + $urls = $calendar->calendarQuery([ + 'name' => 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => 'VEVENT', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => [ + 'start' => $report->start, + 'end' => $report->end, + ], + ], + ], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]); + + $objects = array_map(function ($url) use ($calendar) { + $obj = $calendar->getChild($url)->get(); + + return $obj; + }, $urls); + + $generator = new VObject\FreeBusyGenerator(); + $generator->setObjects($objects); + $generator->setTimeRange($report->start, $report->end); + $generator->setTimeZone($calendarTimeZone); + $result = $generator->getResult(); + $result = $result->serialize(); + + $this->server->httpResponse->setStatus(200); + $this->server->httpResponse->setHeader('Content-Type', 'text/calendar'); + $this->server->httpResponse->setHeader('Content-Length', strlen($result)); + $this->server->httpResponse->setBody($result); + } + + /** + * This method is triggered before a file gets updated with new content. + * + * This plugin uses this method to ensure that CalDAV objects receive + * valid calendar data. + * + * @param string $path + * @param resource $data + * @param bool $modified should be set to true, if this event handler + * changed &$data + */ + public function beforeWriteContent($path, DAV\IFile $node, &$data, &$modified) + { + if (!$node instanceof ICalendarObject) { + return; + } + + // We're only interested in ICalendarObject nodes that are inside of a + // real calendar. This is to avoid triggering validation and scheduling + // for non-calendars (such as an inbox). + list($parent) = Uri\split($path); + $parentNode = $this->server->tree->getNodeForPath($parent); + + if (!$parentNode instanceof ICalendar) { + return; + } + + $this->validateICalendar( + $data, + $path, + $modified, + $this->server->httpRequest, + $this->server->httpResponse, + false + ); + } + + /** + * This method is triggered before a new file is created. + * + * This plugin uses this method to ensure that newly created calendar + * objects contain valid calendar data. + * + * @param string $path + * @param resource $data + * @param bool $modified should be set to true, if this event handler + * changed &$data + */ + public function beforeCreateFile($path, &$data, DAV\ICollection $parentNode, &$modified) + { + if (!$parentNode instanceof ICalendar) { + return; + } + + $this->validateICalendar( + $data, + $path, + $modified, + $this->server->httpRequest, + $this->server->httpResponse, + true + ); + } + + /** + * Checks if the submitted iCalendar data is in fact, valid. + * + * An exception is thrown if it's not. + * + * @param resource|string $data + * @param string $path + * @param bool $modified should be set to true, if this event handler + * changed &$data + * @param RequestInterface $request the http request + * @param ResponseInterface $response the http response + * @param bool $isNew is the item a new one, or an update + */ + protected function validateICalendar(&$data, $path, &$modified, RequestInterface $request, ResponseInterface $response, $isNew) + { + // If it's a stream, we convert it to a string first. + if (is_resource($data)) { + $data = stream_get_contents($data); + } + + $before = $data; + + try { + // If the data starts with a [, we can reasonably assume we're dealing + // with a jCal object. + if ('[' === substr($data, 0, 1)) { + $vobj = VObject\Reader::readJson($data); + + // Converting $data back to iCalendar, as that's what we + // technically support everywhere. + $data = $vobj->serialize(); + $modified = true; + } else { + $vobj = VObject\Reader::read($data); + } + } catch (VObject\ParseException $e) { + throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid iCalendar 2.0 data. Parse error: '.$e->getMessage()); + } + + if ('VCALENDAR' !== $vobj->name) { + throw new DAV\Exception\UnsupportedMediaType('This collection can only support iCalendar objects.'); + } + + $sCCS = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'; + + // Get the Supported Components for the target calendar + list($parentPath) = Uri\split($path); + $calendarProperties = $this->server->getProperties($parentPath, [$sCCS]); + + if (isset($calendarProperties[$sCCS])) { + $supportedComponents = $calendarProperties[$sCCS]->getValue(); + } else { + $supportedComponents = ['VJOURNAL', 'VTODO', 'VEVENT']; + } + + $foundType = null; + + foreach ($vobj->getComponents() as $component) { + switch ($component->name) { + case 'VTIMEZONE': + continue 2; + case 'VEVENT': + case 'VTODO': + case 'VJOURNAL': + $foundType = $component->name; + break; + } + } + + if (!$foundType || !in_array($foundType, $supportedComponents)) { + throw new Exception\InvalidComponentType('iCalendar objects must at least have a component of type '.implode(', ', $supportedComponents)); + } + + $options = VObject\Node::PROFILE_CALDAV; + $prefer = $this->server->getHTTPPrefer(); + + if ('strict' !== $prefer['handling']) { + $options |= VObject\Node::REPAIR; + } + + $messages = $vobj->validate($options); + + $highestLevel = 0; + $warningMessage = null; + + // $messages contains a list of problems with the vcard, along with + // their severity. + foreach ($messages as $message) { + if ($message['level'] > $highestLevel) { + // Recording the highest reported error level. + $highestLevel = $message['level']; + $warningMessage = $message['message']; + } + switch ($message['level']) { + case 1: + // Level 1 means that there was a problem, but it was repaired. + $modified = true; + break; + case 2: + // Level 2 means a warning, but not critical + break; + case 3: + // Level 3 means a critical error + throw new DAV\Exception\UnsupportedMediaType('Validation error in iCalendar: '.$message['message']); + } + } + if ($warningMessage) { + $response->setHeader( + 'X-Sabre-Ew-Gross', + 'iCalendar validation warning: '.$warningMessage + ); + } + + // We use an extra variable to allow event handles to tell us whether + // the object was modified or not. + // + // This helps us determine if we need to re-serialize the object. + $subModified = false; + + $this->server->emit( + 'calendarObjectChange', + [ + $request, + $response, + $vobj, + $parentPath, + &$subModified, + $isNew, + ] + ); + + if ($modified || $subModified) { + // An event handler told us that it modified the object. + $data = $vobj->serialize(); + + // Using md5 to figure out if there was an *actual* change. + if (!$modified && 0 !== strcmp($data, $before)) { + $modified = true; + } + } + + // Destroy circular references so PHP will garbage collect the object. + $vobj->destroy(); + } + + /** + * This method is triggered whenever a subsystem requests the privileges + * that are supported on a particular node. + */ + public function getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet) + { + if ($node instanceof ICalendar) { + $supportedPrivilegeSet['{DAV:}read']['aggregates']['{'.self::NS_CALDAV.'}read-free-busy'] = [ + 'abstract' => false, + 'aggregates' => [], + ]; + } + } + + /** + * This method is used to generate HTML output for the + * DAV\Browser\Plugin. This allows us to generate an interface users + * can use to create new calendars. + * + * @param string $output + * + * @return bool + */ + public function htmlActionsPanel(DAV\INode $node, &$output) + { + if (!$node instanceof CalendarHome) { + return; + } + + $output .= '
+

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/composer/vendor/sabre/dav/lib/CalDAV/Principal/Collection.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/Collection.php new file mode 100644 index 0000000..6d02303 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/Collection.php @@ -0,0 +1,32 @@ +principalBackend, $principalInfo); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/IProxyRead.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/IProxyRead.php new file mode 100644 index 0000000..96e6991 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/IProxyRead.php @@ -0,0 +1,21 @@ +principalInfo = $principalInfo; + $this->principalBackend = $principalBackend; + } + + /** + * Returns this principals name. + * + * @return string + */ + public function getName() + { + return 'calendar-proxy-read'; + } + + /** + * Returns the last modification time. + */ + public function getLastModified() + { + return null; + } + + /** + * Deletes the current node. + * + * @throws DAV\Exception\Forbidden + */ + public function delete() + { + throw new DAV\Exception\Forbidden('Permission denied to delete node'); + } + + /** + * Renames the node. + * + * @param string $name The new name + * + * @throws DAV\Exception\Forbidden + */ + public function setName($name) + { + throw new DAV\Exception\Forbidden('Permission denied to rename file'); + } + + /** + * Returns a list of alternative urls for a principal. + * + * This can for example be an email address, or ldap url. + * + * @return array + */ + public function getAlternateUriSet() + { + return []; + } + + /** + * Returns the full principal url. + * + * @return string + */ + public function getPrincipalUrl() + { + return $this->principalInfo['uri'].'/'.$this->getName(); + } + + /** + * Returns the list of group members. + * + * If this principal is a group, this function should return + * all member principal uri's for the group. + * + * @return array + */ + public function getGroupMemberSet() + { + return $this->principalBackend->getGroupMemberSet($this->getPrincipalUrl()); + } + + /** + * Returns the list of groups this principal is member of. + * + * If this principal is a member of a (list of) groups, this function + * should return a list of principal uri's for it's members. + * + * @return array + */ + public function getGroupMembership() + { + return $this->principalBackend->getGroupMembership($this->getPrincipalUrl()); + } + + /** + * Sets a list of group members. + * + * If this principal is a group, this method sets all the group members. + * The list of members is always overwritten, never appended to. + * + * This method should throw an exception if the members could not be set. + */ + public function setGroupMemberSet(array $principals) + { + $this->principalBackend->setGroupMemberSet($this->getPrincipalUrl(), $principals); + } + + /** + * Returns the displayname. + * + * This should be a human readable name for the principal. + * If none is available, return the nodename. + * + * @return string + */ + public function getDisplayName() + { + return $this->getName(); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/ProxyWrite.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/ProxyWrite.php new file mode 100644 index 0000000..2d1ce7c --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/ProxyWrite.php @@ -0,0 +1,161 @@ +principalInfo = $principalInfo; + $this->principalBackend = $principalBackend; + } + + /** + * Returns this principals name. + * + * @return string + */ + public function getName() + { + return 'calendar-proxy-write'; + } + + /** + * Returns the last modification time. + */ + public function getLastModified() + { + return null; + } + + /** + * Deletes the current node. + * + * @throws DAV\Exception\Forbidden + */ + public function delete() + { + throw new DAV\Exception\Forbidden('Permission denied to delete node'); + } + + /** + * Renames the node. + * + * @param string $name The new name + * + * @throws DAV\Exception\Forbidden + */ + public function setName($name) + { + throw new DAV\Exception\Forbidden('Permission denied to rename file'); + } + + /** + * Returns a list of alternative urls for a principal. + * + * This can for example be an email address, or ldap url. + * + * @return array + */ + public function getAlternateUriSet() + { + return []; + } + + /** + * Returns the full principal url. + * + * @return string + */ + public function getPrincipalUrl() + { + return $this->principalInfo['uri'].'/'.$this->getName(); + } + + /** + * Returns the list of group members. + * + * If this principal is a group, this function should return + * all member principal uri's for the group. + * + * @return array + */ + public function getGroupMemberSet() + { + return $this->principalBackend->getGroupMemberSet($this->getPrincipalUrl()); + } + + /** + * Returns the list of groups this principal is member of. + * + * If this principal is a member of a (list of) groups, this function + * should return a list of principal uri's for it's members. + * + * @return array + */ + public function getGroupMembership() + { + return $this->principalBackend->getGroupMembership($this->getPrincipalUrl()); + } + + /** + * Sets a list of group members. + * + * If this principal is a group, this method sets all the group members. + * The list of members is always overwritten, never appended to. + * + * This method should throw an exception if the members could not be set. + */ + public function setGroupMemberSet(array $principals) + { + $this->principalBackend->setGroupMemberSet($this->getPrincipalUrl(), $principals); + } + + /** + * Returns the displayname. + * + * This should be a human readable name for the principal. + * If none is available, return the nodename. + * + * @return string + */ + public function getDisplayName() + { + return $this->getName(); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/User.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/User.php new file mode 100644 index 0000000..88bf4b4 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Principal/User.php @@ -0,0 +1,136 @@ +principalBackend->getPrincipalByPath($this->getPrincipalURL().'/'.$name); + if (!$principal) { + throw new DAV\Exception\NotFound('Node with name '.$name.' was not found'); + } + if ('calendar-proxy-read' === $name) { + return new ProxyRead($this->principalBackend, $this->principalProperties); + } + + if ('calendar-proxy-write' === $name) { + return new ProxyWrite($this->principalBackend, $this->principalProperties); + } + + throw new DAV\Exception\NotFound('Node with name '.$name.' was not found'); + } + + /** + * Returns an array with all the child nodes. + * + * @return DAV\INode[] + */ + public function getChildren() + { + $r = []; + if ($this->principalBackend->getPrincipalByPath($this->getPrincipalURL().'/calendar-proxy-read')) { + $r[] = new ProxyRead($this->principalBackend, $this->principalProperties); + } + if ($this->principalBackend->getPrincipalByPath($this->getPrincipalURL().'/calendar-proxy-write')) { + $r[] = new ProxyWrite($this->principalBackend, $this->principalProperties); + } + + return $r; + } + + /** + * Returns whether or not the child node exists. + * + * @param string $name + * + * @return bool + */ + public function childExists($name) + { + try { + $this->getChild($name); + + return true; + } catch (DAV\Exception\NotFound $e) { + return false; + } + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + $acl = parent::getACL(); + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => $this->principalProperties['uri'].'/calendar-proxy-read', + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => $this->principalProperties['uri'].'/calendar-proxy-write', + 'protected' => true, + ]; + + return $acl; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/IInbox.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/IInbox.php new file mode 100644 index 0000000..64a94be --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/IInbox.php @@ -0,0 +1,17 @@ +senderEmail = $senderEmail; + } + + /* + * This initializes the plugin. + * + * This function is called by Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + * + * @param DAV\Server $server + * @return void + */ + public function initialize(DAV\Server $server) + { + $server->on('schedule', [$this, 'schedule'], 120); + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using \Sabre\DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'imip'; + } + + /** + * Event handler for the 'schedule' event. + */ + public function schedule(ITip\Message $iTipMessage) + { + // Not sending any emails if the system considers the update + // insignificant. + if (!$iTipMessage->significantChange) { + if (!$iTipMessage->scheduleStatus) { + $iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email'; + } + + return; + } + + $summary = $iTipMessage->message->VEVENT->SUMMARY; + + if ('mailto' !== parse_url($iTipMessage->sender, PHP_URL_SCHEME)) { + return; + } + + if ('mailto' !== parse_url($iTipMessage->recipient, PHP_URL_SCHEME)) { + return; + } + + $sender = substr($iTipMessage->sender, 7); + $recipient = substr($iTipMessage->recipient, 7); + + if ($iTipMessage->senderName) { + $sender = $iTipMessage->senderName.' <'.$sender.'>'; + } + if ($iTipMessage->recipientName && $iTipMessage->recipientName != $recipient) { + $recipient = $iTipMessage->recipientName.' <'.$recipient.'>'; + } + + $subject = 'SabreDAV iTIP message'; + switch (strtoupper($iTipMessage->method)) { + case 'REPLY': + $subject = 'Re: '.$summary; + break; + case 'REQUEST': + $subject = 'Invitation: '.$summary; + break; + case 'CANCEL': + $subject = 'Cancelled: '.$summary; + break; + } + + $headers = [ + 'Reply-To: '.$sender, + 'From: '.$iTipMessage->senderName.' <'.$this->senderEmail.'>', + 'MIME-Version: 1.0', + 'Content-Type: text/calendar; charset=UTF-8; method='.$iTipMessage->method, + ]; + if (DAV\Server::$exposeVersion) { + $headers[] = 'X-Sabre-Version: '.DAV\Version::VERSION; + } + $this->mail( + $recipient, + $subject, + $iTipMessage->message->serialize(), + $headers + ); + $iTipMessage->scheduleStatus = '1.1; Scheduling message is sent via iMip'; + } + + // @codeCoverageIgnoreStart + // This is deemed untestable in a reasonable manner + + /** + * This function is responsible for sending the actual email. + * + * @param string $to Recipient email address + * @param string $subject Subject of the email + * @param string $body iCalendar body + * @param array $headers List of headers + */ + protected function mail($to, $subject, $body, array $headers) + { + mail($to, $subject, $body, implode("\r\n", $headers)); + } + + // @codeCoverageIgnoreEnd + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Email delivery (rfc6047) for CalDAV scheduling', + 'link' => 'http://sabre.io/dav/scheduling/', + ]; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/IOutbox.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/IOutbox.php new file mode 100644 index 0000000..384b503 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/IOutbox.php @@ -0,0 +1,17 @@ +caldavBackend = $caldavBackend; + $this->principalUri = $principalUri; + } + + /** + * Returns the name of the node. + * + * This is used to generate the url. + * + * @return string + */ + public function getName() + { + return 'inbox'; + } + + /** + * Returns an array with all the child nodes. + * + * @return \Sabre\DAV\INode[] + */ + public function getChildren() + { + $objs = $this->caldavBackend->getSchedulingObjects($this->principalUri); + $children = []; + foreach ($objs as $obj) { + //$obj['acl'] = $this->getACL(); + $obj['principaluri'] = $this->principalUri; + $children[] = new SchedulingObject($this->caldavBackend, $obj); + } + + return $children; + } + + /** + * Creates a new file in the directory. + * + * Data will either be supplied as a stream resource, or in certain cases + * as a string. Keep in mind that you may have to support either. + * + * After successful creation of the file, you may choose to return the ETag + * of the new file here. + * + * The returned ETag must be surrounded by double-quotes (The quotes should + * be part of the actual string). + * + * If you cannot accurately determine the ETag, you should not return it. + * If you don't store the file exactly as-is (you're transforming it + * somehow) you should also not return an ETag. + * + * This means that if a subsequent GET to this new file does not exactly + * return the same contents of what was submitted here, you are strongly + * recommended to omit the ETag. + * + * @param string $name Name of the file + * @param resource|string $data Initial payload + * + * @return string|null + */ + public function createFile($name, $data = null) + { + $this->caldavBackend->createSchedulingObject($this->principalUri, $name, $data); + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->principalUri; + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + return [ + [ + 'privilege' => '{DAV:}read', + 'principal' => '{DAV:}authenticated', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write-properties', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}unbind', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}unbind', + 'principal' => $this->getOwner().'/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-deliver', + 'principal' => '{DAV:}authenticated', + 'protected' => true, + ], + ]; + } + + /** + * Performs a calendar-query on the contents of this calendar. + * + * The calendar-query is defined in RFC4791 : CalDAV. Using the + * calendar-query it is possible for a client to request a specific set of + * object, based on contents of iCalendar properties, date-ranges and + * iCalendar component types (VTODO, VEVENT). + * + * This method should just return a list of (relative) urls that match this + * query. + * + * The list of filters are specified as an array. The exact array is + * documented by \Sabre\CalDAV\CalendarQueryParser. + * + * @return array + */ + public function calendarQuery(array $filters) + { + $result = []; + $validator = new CalDAV\CalendarQueryValidator(); + + $objects = $this->caldavBackend->getSchedulingObjects($this->principalUri); + foreach ($objects as $object) { + $vObject = VObject\Reader::read($object['calendardata']); + if ($validator->validate($vObject, $filters)) { + $result[] = $object['uri']; + } + + // Destroy circular references to PHP will GC the object. + $vObject->destroy(); + } + + return $result; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/Outbox.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/Outbox.php new file mode 100644 index 0000000..1442c4c --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/Outbox.php @@ -0,0 +1,119 @@ +principalUri = $principalUri; + } + + /** + * Returns the name of the node. + * + * This is used to generate the url. + * + * @return string + */ + public function getName() + { + return 'outbox'; + } + + /** + * Returns an array with all the child nodes. + * + * @return \Sabre\DAV\INode[] + */ + public function getChildren() + { + return []; + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->principalUri; + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + return [ + [ + 'privilege' => '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-send', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + [ + 'privilege' => '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-send', + 'principal' => $this->getOwner().'/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner().'/calendar-proxy-read', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner().'/calendar-proxy-write', + 'protected' => true, + ], + ]; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/Plugin.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/Plugin.php new file mode 100644 index 0000000..5bca56d --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/Plugin.php @@ -0,0 +1,1006 @@ +server = $server; + $server->on('method:POST', [$this, 'httpPost']); + $server->on('propFind', [$this, 'propFind']); + $server->on('propPatch', [$this, 'propPatch']); + $server->on('calendarObjectChange', [$this, 'calendarObjectChange']); + $server->on('beforeUnbind', [$this, 'beforeUnbind']); + $server->on('schedule', [$this, 'scheduleLocalDelivery']); + $server->on('getSupportedPrivilegeSet', [$this, 'getSupportedPrivilegeSet']); + + $ns = '{'.self::NS_CALDAV.'}'; + + /* + * This information ensures that the {DAV:}resourcetype property has + * the correct values. + */ + $server->resourceTypeMapping['\\Sabre\\CalDAV\\Schedule\\IOutbox'] = $ns.'schedule-outbox'; + $server->resourceTypeMapping['\\Sabre\\CalDAV\\Schedule\\IInbox'] = $ns.'schedule-inbox'; + + /* + * Properties we protect are made read-only by the server. + */ + array_push($server->protectedProperties, + $ns.'schedule-inbox-URL', + $ns.'schedule-outbox-URL', + $ns.'calendar-user-address-set', + $ns.'calendar-user-type', + $ns.'schedule-default-calendar-URL' + ); + } + + /** + * Use this method to tell the server this plugin defines additional + * HTTP methods. + * + * This method is passed a uri. It should only return HTTP methods that are + * available for the specified uri. + * + * @param string $uri + * + * @return array + */ + public function getHTTPMethods($uri) + { + try { + $node = $this->server->tree->getNodeForPath($uri); + } catch (NotFound $e) { + return []; + } + + if ($node instanceof IOutbox) { + return ['POST']; + } + + return []; + } + + /** + * This method handles POST request for the outbox. + * + * @return bool + */ + public function httpPost(RequestInterface $request, ResponseInterface $response) + { + // Checking if this is a text/calendar content type + $contentType = $request->getHeader('Content-Type'); + if (!$contentType || 0 !== strpos($contentType, 'text/calendar')) { + return; + } + + $path = $request->getPath(); + + // Checking if we're talking to an outbox + try { + $node = $this->server->tree->getNodeForPath($path); + } catch (NotFound $e) { + return; + } + if (!$node instanceof IOutbox) { + return; + } + + $this->server->transactionType = 'post-caldav-outbox'; + $this->outboxRequest($node, $request, $response); + + // Returning false breaks the event chain and tells the server we've + // handled the request. + return false; + } + + /** + * This method handler is invoked during fetching of properties. + * + * We use this event to add calendar-auto-schedule-specific properties. + */ + public function propFind(PropFind $propFind, INode $node) + { + if ($node instanceof DAVACL\IPrincipal) { + $caldavPlugin = $this->server->getPlugin('caldav'); + $principalUrl = $node->getPrincipalUrl(); + + // schedule-outbox-URL property + $propFind->handle('{'.self::NS_CALDAV.'}schedule-outbox-URL', function () use ($principalUrl, $caldavPlugin) { + $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl); + if (!$calendarHomePath) { + return null; + } + $outboxPath = $calendarHomePath.'/outbox/'; + + return new LocalHref($outboxPath); + }); + // schedule-inbox-URL property + $propFind->handle('{'.self::NS_CALDAV.'}schedule-inbox-URL', function () use ($principalUrl, $caldavPlugin) { + $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl); + if (!$calendarHomePath) { + return null; + } + $inboxPath = $calendarHomePath.'/inbox/'; + + return new LocalHref($inboxPath); + }); + + $propFind->handle('{'.self::NS_CALDAV.'}schedule-default-calendar-URL', function () use ($principalUrl, $caldavPlugin) { + // We don't support customizing this property yet, so in the + // meantime we just grab the first calendar in the home-set. + $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl); + + if (!$calendarHomePath) { + return null; + } + + $sccs = '{'.self::NS_CALDAV.'}supported-calendar-component-set'; + + $result = $this->server->getPropertiesForPath($calendarHomePath, [ + '{DAV:}resourcetype', + '{DAV:}share-access', + $sccs, + ], 1); + + foreach ($result as $child) { + if (!isset($child[200]['{DAV:}resourcetype']) || !$child[200]['{DAV:}resourcetype']->is('{'.self::NS_CALDAV.'}calendar')) { + // Node is either not a calendar + continue; + } + if (isset($child[200]['{DAV:}share-access'])) { + $shareAccess = $child[200]['{DAV:}share-access']->getValue(); + if (Sharing\Plugin::ACCESS_NOTSHARED !== $shareAccess && Sharing\Plugin::ACCESS_SHAREDOWNER !== $shareAccess) { + // Node is a shared node, not owned by the relevant + // user. + continue; + } + } + if (!isset($child[200][$sccs]) || in_array('VEVENT', $child[200][$sccs]->getValue())) { + // Either there is no supported-calendar-component-set + // (which is fine) or we found one that supports VEVENT. + return new LocalHref($child['href']); + } + } + }); + + // The server currently reports every principal to be of type + // 'INDIVIDUAL' + $propFind->handle('{'.self::NS_CALDAV.'}calendar-user-type', function () { + return 'INDIVIDUAL'; + }); + } + + // Mapping the old property to the new property. + $propFind->handle('{http://calendarserver.org/ns/}calendar-availability', function () use ($propFind, $node) { + // In case it wasn't clear, the only difference is that we map the + // old property to a different namespace. + $availProp = '{'.self::NS_CALDAV.'}calendar-availability'; + $subPropFind = new PropFind( + $propFind->getPath(), + [$availProp] + ); + + $this->server->getPropertiesByNode( + $subPropFind, + $node + ); + + $propFind->set( + '{http://calendarserver.org/ns/}calendar-availability', + $subPropFind->get($availProp), + $subPropFind->getStatus($availProp) + ); + }); + } + + /** + * This method is called during property updates. + * + * @param string $path + */ + public function propPatch($path, PropPatch $propPatch) + { + // Mapping the old property to the new property. + $propPatch->handle('{http://calendarserver.org/ns/}calendar-availability', function ($value) use ($path) { + $availProp = '{'.self::NS_CALDAV.'}calendar-availability'; + $subPropPatch = new PropPatch([$availProp => $value]); + $this->server->emit('propPatch', [$path, $subPropPatch]); + $subPropPatch->commit(); + + return $subPropPatch->getResult()[$availProp]; + }); + } + + /** + * This method is triggered whenever there was a calendar object gets + * created or updated. + * + * @param RequestInterface $request HTTP request + * @param ResponseInterface $response HTTP Response + * @param VCalendar $vCal Parsed iCalendar object + * @param mixed $calendarPath Path to calendar collection + * @param mixed $modified the iCalendar object has been touched + * @param mixed $isNew Whether this was a new item or we're updating one + */ + public function calendarObjectChange(RequestInterface $request, ResponseInterface $response, VCalendar $vCal, $calendarPath, &$modified, $isNew) + { + if (!$this->scheduleReply($this->server->httpRequest)) { + return; + } + + $calendarNode = $this->server->tree->getNodeForPath($calendarPath); + + $addresses = $this->getAddressesForPrincipal( + $calendarNode->getOwner() + ); + + if (!$isNew) { + $node = $this->server->tree->getNodeForPath($request->getPath()); + $oldObj = Reader::read($node->get()); + } else { + $oldObj = null; + } + + $this->processICalendarChange($oldObj, $vCal, $addresses, [], $modified); + + if ($oldObj) { + // Destroy circular references so PHP will GC the object. + $oldObj->destroy(); + } + } + + /** + * This method is responsible for delivering the ITip message. + */ + public function deliver(ITip\Message $iTipMessage) + { + $this->server->emit('schedule', [$iTipMessage]); + if (!$iTipMessage->scheduleStatus) { + $iTipMessage->scheduleStatus = '5.2;There was no system capable of delivering the scheduling message'; + } + // In case the change was considered 'insignificant', we are going to + // remove any error statuses, if any. See ticket #525. + list($baseCode) = explode('.', $iTipMessage->scheduleStatus); + if (!$iTipMessage->significantChange && in_array($baseCode, ['3', '5'])) { + $iTipMessage->scheduleStatus = null; + } + } + + /** + * This method is triggered before a file gets deleted. + * + * We use this event to make sure that when this happens, attendees get + * cancellations, and organizers get 'DECLINED' statuses. + * + * @param string $path + */ + public function beforeUnbind($path) + { + // FIXME: We shouldn't trigger this functionality when we're issuing a + // MOVE. This is a hack. + if ('MOVE' === $this->server->httpRequest->getMethod()) { + return; + } + + $node = $this->server->tree->getNodeForPath($path); + + if (!$node instanceof ICalendarObject || $node instanceof ISchedulingObject) { + return; + } + + if (!$this->scheduleReply($this->server->httpRequest)) { + return; + } + + $addresses = $this->getAddressesForPrincipal( + $node->getOwner() + ); + + $broker = $this->createITipBroker(); + $messages = $broker->parseEvent(null, $addresses, $node->get()); + + foreach ($messages as $message) { + $this->deliver($message); + } + } + + /** + * Event handler for the 'schedule' event. + * + * This handler attempts to look at local accounts to deliver the + * scheduling object. + */ + public function scheduleLocalDelivery(ITip\Message $iTipMessage) + { + $aclPlugin = $this->server->getPlugin('acl'); + + // Local delivery is not available if the ACL plugin is not loaded. + if (!$aclPlugin) { + return; + } + + $caldavNS = '{'.self::NS_CALDAV.'}'; + + $principalUri = $aclPlugin->getPrincipalByUri($iTipMessage->recipient); + if (!$principalUri) { + $iTipMessage->scheduleStatus = '3.7;Could not find principal.'; + + return; + } + + // We found a principal URL, now we need to find its inbox. + // Unfortunately we may not have sufficient privileges to find this, so + // we are temporarily turning off ACL to let this come through. + // + // Once we support PHP 5.5, this should be wrapped in a try..finally + // block so we can ensure that this privilege gets added again after. + $this->server->removeListener('propFind', [$aclPlugin, 'propFind']); + + $result = $this->server->getProperties( + $principalUri, + [ + '{DAV:}principal-URL', + $caldavNS.'calendar-home-set', + $caldavNS.'schedule-inbox-URL', + $caldavNS.'schedule-default-calendar-URL', + '{http://sabredav.org/ns}email-address', + ] + ); + + // Re-registering the ACL event + $this->server->on('propFind', [$aclPlugin, 'propFind'], 20); + + if (!isset($result[$caldavNS.'schedule-inbox-URL'])) { + $iTipMessage->scheduleStatus = '5.2;Could not find local inbox'; + + return; + } + if (!isset($result[$caldavNS.'calendar-home-set'])) { + $iTipMessage->scheduleStatus = '5.2;Could not locate a calendar-home-set'; + + return; + } + if (!isset($result[$caldavNS.'schedule-default-calendar-URL'])) { + $iTipMessage->scheduleStatus = '5.2;Could not find a schedule-default-calendar-URL property'; + + return; + } + + $calendarPath = $result[$caldavNS.'schedule-default-calendar-URL']->getHref(); + $homePath = $result[$caldavNS.'calendar-home-set']->getHref(); + $inboxPath = $result[$caldavNS.'schedule-inbox-URL']->getHref(); + + if ('REPLY' === $iTipMessage->method) { + $privilege = 'schedule-deliver-reply'; + } else { + $privilege = 'schedule-deliver-invite'; + } + + if (!$aclPlugin->checkPrivileges($inboxPath, $caldavNS.$privilege, DAVACL\Plugin::R_PARENT, false)) { + $iTipMessage->scheduleStatus = '3.8;insufficient privileges: '.$privilege.' is required on the recipient schedule inbox.'; + + return; + } + + // Next, we're going to find out if the item already exits in one of + // the users' calendars. + $uid = $iTipMessage->uid; + + $newFileName = 'sabredav-'.\Sabre\DAV\UUIDUtil::getUUID().'.ics'; + + $home = $this->server->tree->getNodeForPath($homePath); + $inbox = $this->server->tree->getNodeForPath($inboxPath); + + $currentObject = null; + $objectNode = null; + $oldICalendarData = null; + $isNewNode = false; + + $result = $home->getCalendarObjectByUID($uid); + if ($result) { + // There was an existing object, we need to update probably. + $objectPath = $homePath.'/'.$result; + $objectNode = $this->server->tree->getNodeForPath($objectPath); + $oldICalendarData = $objectNode->get(); + $currentObject = Reader::read($oldICalendarData); + } else { + $isNewNode = true; + } + + $broker = $this->createITipBroker(); + $newObject = $broker->processMessage($iTipMessage, $currentObject); + + $inbox->createFile($newFileName, $iTipMessage->message->serialize()); + + if (!$newObject) { + // We received an iTip message referring to a UID that we don't + // have in any calendars yet, and processMessage did not give us a + // calendarobject back. + // + // The implication is that processMessage did not understand the + // iTip message. + $iTipMessage->scheduleStatus = '5.0;iTip message was not processed by the server, likely because we didn\'t understand it.'; + + return; + } + + // Note that we are bypassing ACL on purpose by calling this directly. + // We may need to look a bit deeper into this later. Supporting ACL + // here would be nice. + if ($isNewNode) { + $calendar = $this->server->tree->getNodeForPath($calendarPath); + $calendar->createFile($newFileName, $newObject->serialize()); + } else { + // If the message was a reply, we may have to inform other + // attendees of this attendees status. Therefore we're shooting off + // another itipMessage. + if ('REPLY' === $iTipMessage->method) { + $this->processICalendarChange( + $oldICalendarData, + $newObject, + [$iTipMessage->recipient], + [$iTipMessage->sender] + ); + } + $objectNode->put($newObject->serialize()); + } + $iTipMessage->scheduleStatus = '1.2;Message delivered locally'; + } + + /** + * This method is triggered whenever a subsystem requests the privileges + * that are supported on a particular node. + * + * We need to add a number of privileges for scheduling purposes. + */ + public function getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet) + { + $ns = '{'.self::NS_CALDAV.'}'; + if ($node instanceof IOutbox) { + $supportedPrivilegeSet[$ns.'schedule-send'] = [ + 'abstract' => false, + 'aggregates' => [ + $ns.'schedule-send-invite' => [ + 'abstract' => false, + 'aggregates' => [], + ], + $ns.'schedule-send-reply' => [ + 'abstract' => false, + 'aggregates' => [], + ], + $ns.'schedule-send-freebusy' => [ + 'abstract' => false, + 'aggregates' => [], + ], + // Privilege from an earlier scheduling draft, but still + // used by some clients. + $ns.'schedule-post-vevent' => [ + 'abstract' => false, + 'aggregates' => [], + ], + ], + ]; + } + if ($node instanceof IInbox) { + $supportedPrivilegeSet[$ns.'schedule-deliver'] = [ + 'abstract' => false, + 'aggregates' => [ + $ns.'schedule-deliver-invite' => [ + 'abstract' => false, + 'aggregates' => [], + ], + $ns.'schedule-deliver-reply' => [ + 'abstract' => false, + 'aggregates' => [], + ], + $ns.'schedule-query-freebusy' => [ + 'abstract' => false, + 'aggregates' => [], + ], + ], + ]; + } + } + + /** + * This method looks at an old iCalendar object, a new iCalendar object and + * starts sending scheduling messages based on the changes. + * + * A list of addresses needs to be specified, so the system knows who made + * the update, because the behavior may be different based on if it's an + * attendee or an organizer. + * + * This method may update $newObject to add any status changes. + * + * @param VCalendar|string|null $oldObject + * @param array $ignore any addresses to not send messages to + * @param bool $modified a marker to indicate that the original object modified by this process + */ + protected function processICalendarChange($oldObject, VCalendar $newObject, array $addresses, array $ignore = [], &$modified = false) + { + $broker = $this->createITipBroker(); + $messages = $broker->parseEvent($newObject, $addresses, $oldObject); + + if ($messages) { + $modified = true; + } + + foreach ($messages as $message) { + if (in_array($message->recipient, $ignore)) { + continue; + } + + $this->deliver($message); + + if (isset($newObject->VEVENT->ORGANIZER) && ($newObject->VEVENT->ORGANIZER->getNormalizedValue() === $message->recipient)) { + if ($message->scheduleStatus) { + $newObject->VEVENT->ORGANIZER['SCHEDULE-STATUS'] = $message->getScheduleStatus(); + } + unset($newObject->VEVENT->ORGANIZER['SCHEDULE-FORCE-SEND']); + } else { + if (isset($newObject->VEVENT->ATTENDEE)) { + foreach ($newObject->VEVENT->ATTENDEE as $attendee) { + if ($attendee->getNormalizedValue() === $message->recipient) { + if ($message->scheduleStatus) { + $attendee['SCHEDULE-STATUS'] = $message->getScheduleStatus(); + } + unset($attendee['SCHEDULE-FORCE-SEND']); + break; + } + } + } + } + } + } + + /** + * Returns a list of addresses that are associated with a principal. + * + * @param string $principal + * + * @return array + */ + protected function getAddressesForPrincipal($principal) + { + $CUAS = '{'.self::NS_CALDAV.'}calendar-user-address-set'; + + $properties = $this->server->getProperties( + $principal, + [$CUAS] + ); + + // If we can't find this information, we'll stop processing + if (!isset($properties[$CUAS])) { + return []; + } + + $addresses = $properties[$CUAS]->getHrefs(); + + return $addresses; + } + + /** + * This method handles POST requests to the schedule-outbox. + * + * Currently, two types of requests are supported: + * * FREEBUSY requests from RFC 6638 + * * Simple iTIP messages from draft-desruisseaux-caldav-sched-04 + * + * The latter is from an expired early draft of the CalDAV scheduling + * extensions, but iCal depends on a feature from that spec, so we + * implement it. + */ + public function outboxRequest(IOutbox $outboxNode, RequestInterface $request, ResponseInterface $response) + { + $outboxPath = $request->getPath(); + + // Parsing the request body + try { + $vObject = VObject\Reader::read($request->getBody()); + } catch (VObject\ParseException $e) { + throw new BadRequest('The request body must be a valid iCalendar object. Parse error: '.$e->getMessage()); + } + + // The incoming iCalendar object must have a METHOD property, and a + // component. The combination of both determines what type of request + // this is. + $componentType = null; + foreach ($vObject->getComponents() as $component) { + if ('VTIMEZONE' !== $component->name) { + $componentType = $component->name; + break; + } + } + if (is_null($componentType)) { + throw new BadRequest('We expected at least one VTODO, VJOURNAL, VFREEBUSY or VEVENT component'); + } + + // Validating the METHOD + $method = strtoupper((string) $vObject->METHOD); + if (!$method) { + throw new BadRequest('A METHOD property must be specified in iTIP messages'); + } + + // So we support one type of request: + // + // REQUEST with a VFREEBUSY component + + $acl = $this->server->getPlugin('acl'); + + if ('VFREEBUSY' === $componentType && 'REQUEST' === $method) { + $acl && $acl->checkPrivileges($outboxPath, '{'.self::NS_CALDAV.'}schedule-send-freebusy'); + $this->handleFreeBusyRequest($outboxNode, $vObject, $request, $response); + + // Destroy circular references so PHP can GC the object. + $vObject->destroy(); + unset($vObject); + } else { + throw new NotImplemented('We only support VFREEBUSY (REQUEST) on this endpoint'); + } + } + + /** + * This method is responsible for parsing a free-busy query request and + * returning its result in $response. + */ + protected function handleFreeBusyRequest(IOutbox $outbox, VObject\Component $vObject, RequestInterface $request, ResponseInterface $response) + { + $vFreeBusy = $vObject->VFREEBUSY; + $organizer = $vFreeBusy->ORGANIZER; + + $organizer = (string) $organizer; + + // Validating if the organizer matches the owner of the inbox. + $owner = $outbox->getOwner(); + + $caldavNS = '{'.self::NS_CALDAV.'}'; + + $uas = $caldavNS.'calendar-user-address-set'; + $props = $this->server->getProperties($owner, [$uas]); + + if (empty($props[$uas]) || !in_array($organizer, $props[$uas]->getHrefs())) { + throw new Forbidden('The organizer in the request did not match any of the addresses for the owner of this inbox'); + } + + if (!isset($vFreeBusy->ATTENDEE)) { + throw new BadRequest('You must at least specify 1 attendee'); + } + + $attendees = []; + foreach ($vFreeBusy->ATTENDEE as $attendee) { + $attendees[] = (string) $attendee; + } + + if (!isset($vFreeBusy->DTSTART) || !isset($vFreeBusy->DTEND)) { + throw new BadRequest('DTSTART and DTEND must both be specified'); + } + + $startRange = $vFreeBusy->DTSTART->getDateTime(); + $endRange = $vFreeBusy->DTEND->getDateTime(); + + $results = []; + foreach ($attendees as $attendee) { + $results[] = $this->getFreeBusyForEmail($attendee, $startRange, $endRange, $vObject); + } + + $dom = new \DOMDocument('1.0', 'utf-8'); + $dom->formatOutput = true; + $scheduleResponse = $dom->createElement('cal:schedule-response'); + foreach ($this->server->xml->namespaceMap as $namespace => $prefix) { + $scheduleResponse->setAttribute('xmlns:'.$prefix, $namespace); + } + $dom->appendChild($scheduleResponse); + + foreach ($results as $result) { + $xresponse = $dom->createElement('cal:response'); + + $recipient = $dom->createElement('cal:recipient'); + $recipientHref = $dom->createElement('d:href'); + + $recipientHref->appendChild($dom->createTextNode($result['href'])); + $recipient->appendChild($recipientHref); + $xresponse->appendChild($recipient); + + $reqStatus = $dom->createElement('cal:request-status'); + $reqStatus->appendChild($dom->createTextNode($result['request-status'])); + $xresponse->appendChild($reqStatus); + + if (isset($result['calendar-data'])) { + $calendardata = $dom->createElement('cal:calendar-data'); + $calendardata->appendChild($dom->createTextNode(str_replace("\r\n", "\n", $result['calendar-data']->serialize()))); + $xresponse->appendChild($calendardata); + } + $scheduleResponse->appendChild($xresponse); + } + + $response->setStatus(200); + $response->setHeader('Content-Type', 'application/xml'); + $response->setBody($dom->saveXML()); + } + + /** + * Returns free-busy information for a specific address. The returned + * data is an array containing the following properties:. + * + * calendar-data : A VFREEBUSY VObject + * request-status : an iTip status code. + * href: The principal's email address, as requested + * + * The following request status codes may be returned: + * * 2.0;description + * * 3.7;description + * + * @param string $email address + * + * @return array + */ + protected function getFreeBusyForEmail($email, \DateTimeInterface $start, \DateTimeInterface $end, VObject\Component $request) + { + $caldavNS = '{'.self::NS_CALDAV.'}'; + + $aclPlugin = $this->server->getPlugin('acl'); + if ('mailto:' === substr($email, 0, 7)) { + $email = substr($email, 7); + } + + $result = $aclPlugin->principalSearch( + ['{http://sabredav.org/ns}email-address' => $email], + [ + '{DAV:}principal-URL', + $caldavNS.'calendar-home-set', + $caldavNS.'schedule-inbox-URL', + '{http://sabredav.org/ns}email-address', + ] + ); + + if (!count($result)) { + return [ + 'request-status' => '3.7;Could not find principal', + 'href' => 'mailto:'.$email, + ]; + } + + if (!isset($result[0][200][$caldavNS.'calendar-home-set'])) { + return [ + 'request-status' => '3.7;No calendar-home-set property found', + 'href' => 'mailto:'.$email, + ]; + } + if (!isset($result[0][200][$caldavNS.'schedule-inbox-URL'])) { + return [ + 'request-status' => '3.7;No schedule-inbox-URL property found', + 'href' => 'mailto:'.$email, + ]; + } + $homeSet = $result[0][200][$caldavNS.'calendar-home-set']->getHref(); + $inboxUrl = $result[0][200][$caldavNS.'schedule-inbox-URL']->getHref(); + + // Do we have permission? + $aclPlugin->checkPrivileges($inboxUrl, $caldavNS.'schedule-query-freebusy'); + + // Grabbing the calendar list + $objects = []; + $calendarTimeZone = new DateTimeZone('UTC'); + + foreach ($this->server->tree->getNodeForPath($homeSet)->getChildren() as $node) { + if (!$node instanceof ICalendar) { + continue; + } + + $sct = $caldavNS.'schedule-calendar-transp'; + $ctz = $caldavNS.'calendar-timezone'; + $props = $node->getProperties([$sct, $ctz]); + + if (isset($props[$sct]) && ScheduleCalendarTransp::TRANSPARENT == $props[$sct]->getValue()) { + // If a calendar is marked as 'transparent', it means we must + // ignore it for free-busy purposes. + continue; + } + + if (isset($props[$ctz])) { + $vtimezoneObj = VObject\Reader::read($props[$ctz]); + $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone(); + + // Destroy circular references so PHP can garbage collect the object. + $vtimezoneObj->destroy(); + } + + // Getting the list of object uris within the time-range + $urls = $node->calendarQuery([ + 'name' => 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => 'VEVENT', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => [ + 'start' => $start, + 'end' => $end, + ], + ], + ], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]); + + $calObjects = array_map(function ($url) use ($node) { + $obj = $node->getChild($url)->get(); + + return $obj; + }, $urls); + + $objects = array_merge($objects, $calObjects); + } + + $inboxProps = $this->server->getProperties( + $inboxUrl, + $caldavNS.'calendar-availability' + ); + + $vcalendar = new VObject\Component\VCalendar(); + $vcalendar->METHOD = 'REPLY'; + + $generator = new VObject\FreeBusyGenerator(); + $generator->setObjects($objects); + $generator->setTimeRange($start, $end); + $generator->setBaseObject($vcalendar); + $generator->setTimeZone($calendarTimeZone); + + if ($inboxProps) { + $generator->setVAvailability( + VObject\Reader::read( + $inboxProps[$caldavNS.'calendar-availability'] + ) + ); + } + + $result = $generator->getResult(); + + $vcalendar->VFREEBUSY->ATTENDEE = 'mailto:'.$email; + $vcalendar->VFREEBUSY->UID = (string) $request->VFREEBUSY->UID; + $vcalendar->VFREEBUSY->ORGANIZER = clone $request->VFREEBUSY->ORGANIZER; + + return [ + 'calendar-data' => $result, + 'request-status' => '2.0;Success', + 'href' => 'mailto:'.$email, + ]; + } + + /** + * This method checks the 'Schedule-Reply' header + * and returns false if it's 'F', otherwise true. + * + * @return bool + */ + protected function scheduleReply(RequestInterface $request) + { + $scheduleReply = $request->getHeader('Schedule-Reply'); + + return 'F' !== $scheduleReply; + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds calendar-auto-schedule, as defined in rfc6638', + 'link' => 'http://sabre.io/dav/scheduling/', + ]; + } + + /** + * Returns an instance of the iTip\Broker. + */ + protected function createITipBroker(): Broker + { + return new Broker(); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php new file mode 100644 index 0000000..b40f28a --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php @@ -0,0 +1,130 @@ +objectData['calendardata'])) { + $this->objectData = $this->caldavBackend->getSchedulingObject($this->objectData['principaluri'], $this->objectData['uri']); + } + + return $this->objectData['calendardata']; + } + + /** + * Updates the ICalendar-formatted object. + * + * @param string|resource $calendarData + * + * @return string + */ + public function put($calendarData) + { + throw new MethodNotAllowed('Updating scheduling objects is not supported'); + } + + /** + * Deletes the scheduling message. + */ + public function delete() + { + $this->caldavBackend->deleteSchedulingObject($this->objectData['principaluri'], $this->objectData['uri']); + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->objectData['principaluri']; + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + // An alternative acl may be specified in the object data. + // + + if (isset($this->objectData['acl'])) { + return $this->objectData['acl']; + } + + // The default ACL + return [ + [ + 'privilege' => '{DAV:}all', + 'principal' => '{DAV:}owner', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}all', + 'principal' => $this->objectData['principaluri'].'/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->objectData['principaluri'].'/calendar-proxy-read', + 'protected' => true, + ], + ]; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/SharedCalendar.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/SharedCalendar.php new file mode 100644 index 0000000..818392f --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/SharedCalendar.php @@ -0,0 +1,219 @@ +calendarInfo['share-access']) ? $this->calendarInfo['share-access'] : SPlugin::ACCESS_NOTSHARED; + } + + /** + * This function must return a URI that uniquely identifies the shared + * resource. This URI should be identical across instances, and is + * also used in several other XML bodies to connect invites to + * resources. + * + * This may simply be a relative reference to the original shared instance, + * but it could also be a urn. As long as it's a valid URI and unique. + * + * @return string + */ + public function getShareResourceUri() + { + return $this->calendarInfo['share-resource-uri']; + } + + /** + * Updates the list of sharees. + * + * Every item must be a Sharee object. + * + * @param \Sabre\DAV\Xml\Element\Sharee[] $sharees + */ + public function updateInvites(array $sharees) + { + $this->caldavBackend->updateInvites($this->calendarInfo['id'], $sharees); + } + + /** + * Returns the list of people whom this resource is shared with. + * + * Every item in the returned array must be a Sharee object with + * at least the following properties set: + * + * * $href + * * $shareAccess + * * $inviteStatus + * + * and optionally: + * + * * $properties + * + * @return \Sabre\DAV\Xml\Element\Sharee[] + */ + public function getInvites() + { + return $this->caldavBackend->getInvites($this->calendarInfo['id']); + } + + /** + * Marks this calendar as published. + * + * Publishing a calendar should automatically create a read-only, public, + * subscribable calendar. + * + * @param bool $value + */ + public function setPublishStatus($value) + { + $this->caldavBackend->setPublishStatus($this->calendarInfo['id'], $value); + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + $acl = []; + + switch ($this->getShareAccess()) { + case SPlugin::ACCESS_NOTSHARED: + case SPlugin::ACCESS_SHAREDOWNER: + $acl[] = [ + 'privilege' => '{DAV:}share', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}share', + 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write', + 'protected' => true, + ]; + // no break intentional! + case SPlugin::ACCESS_READWRITE: + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write', + 'protected' => true, + ]; + // no break intentional! + case SPlugin::ACCESS_READ: + $acl[] = [ + 'privilege' => '{DAV:}write-properties', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}write-properties', + 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write', + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-read', + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write', + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{'.Plugin::NS_CALDAV.'}read-free-busy', + 'principal' => '{DAV:}authenticated', + 'protected' => true, + ]; + break; + } + + return $acl; + } + + /** + * This method returns the ACL's for calendar objects in this calendar. + * The result of this method automatically gets passed to the + * calendar-object nodes in the calendar. + * + * @return array + */ + public function getChildACL() + { + $acl = []; + + switch ($this->getShareAccess()) { + case SPlugin::ACCESS_NOTSHARED: + case SPlugin::ACCESS_SHAREDOWNER: + case SPlugin::ACCESS_READWRITE: + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write', + 'protected' => true, + ]; + // no break intentional + case SPlugin::ACCESS_READ: + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write', + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-read', + 'protected' => true, + ]; + break; + } + + return $acl; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/SharingPlugin.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/SharingPlugin.php new file mode 100644 index 0000000..bacfe04 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/SharingPlugin.php @@ -0,0 +1,346 @@ +server = $server; + + if (is_null($this->server->getPlugin('sharing'))) { + throw new \LogicException('The generic "sharing" plugin must be loaded before the caldav sharing plugin. Call $server->addPlugin(new \Sabre\DAV\Sharing\Plugin()); before this one.'); + } + + array_push( + $this->server->protectedProperties, + '{'.Plugin::NS_CALENDARSERVER.'}invite', + '{'.Plugin::NS_CALENDARSERVER.'}allowed-sharing-modes', + '{'.Plugin::NS_CALENDARSERVER.'}shared-url' + ); + + $this->server->xml->elementMap['{'.Plugin::NS_CALENDARSERVER.'}share'] = 'Sabre\\CalDAV\\Xml\\Request\\Share'; + $this->server->xml->elementMap['{'.Plugin::NS_CALENDARSERVER.'}invite-reply'] = 'Sabre\\CalDAV\\Xml\\Request\\InviteReply'; + + $this->server->on('propFind', [$this, 'propFindEarly']); + $this->server->on('propFind', [$this, 'propFindLate'], 150); + $this->server->on('propPatch', [$this, 'propPatch'], 40); + $this->server->on('method:POST', [$this, 'httpPost']); + } + + /** + * This event is triggered when properties are requested for a certain + * node. + * + * This allows us to inject any properties early. + */ + public function propFindEarly(DAV\PropFind $propFind, DAV\INode $node) + { + if ($node instanceof ISharedCalendar) { + $propFind->handle('{'.Plugin::NS_CALENDARSERVER.'}invite', function () use ($node) { + return new Xml\Property\Invite( + $node->getInvites() + ); + }); + } + } + + /** + * This method is triggered *after* all properties have been retrieved. + * This allows us to inject the correct resourcetype for calendars that + * have been shared. + */ + public function propFindLate(DAV\PropFind $propFind, DAV\INode $node) + { + if ($node instanceof ISharedCalendar) { + $shareAccess = $node->getShareAccess(); + if ($rt = $propFind->get('{DAV:}resourcetype')) { + switch ($shareAccess) { + case \Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER: + $rt->add('{'.Plugin::NS_CALENDARSERVER.'}shared-owner'); + break; + case \Sabre\DAV\Sharing\Plugin::ACCESS_READ: + case \Sabre\DAV\Sharing\Plugin::ACCESS_READWRITE: + $rt->add('{'.Plugin::NS_CALENDARSERVER.'}shared'); + break; + } + } + $propFind->handle('{'.Plugin::NS_CALENDARSERVER.'}allowed-sharing-modes', function () { + return new Xml\Property\AllowedSharingModes(true, false); + }); + } + } + + /** + * This method is triggered when a user attempts to update a node's + * properties. + * + * A previous draft of the sharing spec stated that it was possible to use + * PROPPATCH to remove 'shared-owner' from the resourcetype, thus unsharing + * the calendar. + * + * Even though this is no longer in the current spec, we keep this around + * because OS X 10.7 may still make use of this feature. + * + * @param string $path + */ + public function propPatch($path, DAV\PropPatch $propPatch) + { + $node = $this->server->tree->getNodeForPath($path); + if (!$node instanceof ISharedCalendar) { + return; + } + + if (\Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER === $node->getShareAccess() || \Sabre\DAV\Sharing\Plugin::ACCESS_NOTSHARED === $node->getShareAccess()) { + $propPatch->handle('{DAV:}resourcetype', function ($value) use ($node) { + if ($value->is('{'.Plugin::NS_CALENDARSERVER.'}shared-owner')) { + return false; + } + $shares = $node->getInvites(); + foreach ($shares as $share) { + $share->access = DAV\Sharing\Plugin::ACCESS_NOACCESS; + } + $node->updateInvites($shares); + + return true; + }); + } + } + + /** + * We intercept this to handle POST requests on calendars. + * + * @return bool|null + */ + public function httpPost(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + + // Only handling xml + $contentType = $request->getHeader('Content-Type'); + if (null === $contentType) { + return; + } + if (false === strpos($contentType, 'application/xml') && false === strpos($contentType, 'text/xml')) { + return; + } + + // Making sure the node exists + try { + $node = $this->server->tree->getNodeForPath($path); + } catch (DAV\Exception\NotFound $e) { + return; + } + + $requestBody = $request->getBodyAsString(); + + // If this request handler could not deal with this POST request, it + // will return 'null' and other plugins get a chance to handle the + // request. + // + // However, we already requested the full body. This is a problem, + // because a body can only be read once. This is why we preemptively + // re-populated the request body with the existing data. + $request->setBody($requestBody); + + $message = $this->server->xml->parse($requestBody, $request->getUrl(), $documentType); + + switch ($documentType) { + // Both the DAV:share-resource and CALENDARSERVER:share requests + // behave identically. + case '{'.Plugin::NS_CALENDARSERVER.'}share': + $sharingPlugin = $this->server->getPlugin('sharing'); + $sharingPlugin->shareResource($path, $message->sharees); + + $response->setStatus(200); + // Adding this because sending a response body may cause issues, + // and I wanted some type of indicator the response was handled. + $response->setHeader('X-Sabre-Status', 'everything-went-well'); + + // Breaking the event chain + return false; + + // The invite-reply document is sent when the user replies to an + // invitation of a calendar share. + case '{'.Plugin::NS_CALENDARSERVER.'}invite-reply': + // This only works on the calendar-home-root node. + if (!$node instanceof CalendarHome) { + return; + } + $this->server->transactionType = 'post-invite-reply'; + + // Getting ACL info + $acl = $this->server->getPlugin('acl'); + + // If there's no ACL support, we allow everything + if ($acl) { + $acl->checkPrivileges($path, '{DAV:}write'); + } + + $url = $node->shareReply( + $message->href, + $message->status, + $message->calendarUri, + $message->inReplyTo, + $message->summary + ); + + $response->setStatus(200); + // Adding this because sending a response body may cause issues, + // and I wanted some type of indicator the response was handled. + $response->setHeader('X-Sabre-Status', 'everything-went-well'); + + if ($url) { + $writer = $this->server->xml->getWriter(); + $writer->contextUri = $request->getUrl(); + $writer->openMemory(); + $writer->startDocument(); + $writer->startElement('{'.Plugin::NS_CALENDARSERVER.'}shared-as'); + $writer->write(new LocalHref($url)); + $writer->endElement(); + $response->setHeader('Content-Type', 'application/xml'); + $response->setBody($writer->outputMemory()); + } + + // Breaking the event chain + return false; + + case '{'.Plugin::NS_CALENDARSERVER.'}publish-calendar': + // We can only deal with IShareableCalendar objects + if (!$node instanceof ISharedCalendar) { + return; + } + $this->server->transactionType = 'post-publish-calendar'; + + // Getting ACL info + $acl = $this->server->getPlugin('acl'); + + // If there's no ACL support, we allow everything + if ($acl) { + $acl->checkPrivileges($path, '{DAV:}share'); + } + + $node->setPublishStatus(true); + + // iCloud sends back the 202, so we will too. + $response->setStatus(202); + + // Adding this because sending a response body may cause issues, + // and I wanted some type of indicator the response was handled. + $response->setHeader('X-Sabre-Status', 'everything-went-well'); + + // Breaking the event chain + return false; + + case '{'.Plugin::NS_CALENDARSERVER.'}unpublish-calendar': + // We can only deal with IShareableCalendar objects + if (!$node instanceof ISharedCalendar) { + return; + } + $this->server->transactionType = 'post-unpublish-calendar'; + + // Getting ACL info + $acl = $this->server->getPlugin('acl'); + + // If there's no ACL support, we allow everything + if ($acl) { + $acl->checkPrivileges($path, '{DAV:}share'); + } + + $node->setPublishStatus(false); + + $response->setStatus(200); + + // Adding this because sending a response body may cause issues, + // and I wanted some type of indicator the response was handled. + $response->setHeader('X-Sabre-Status', 'everything-went-well'); + + // Breaking the event chain + return false; + } + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds support for caldav-sharing.', + 'link' => 'http://sabre.io/dav/caldav-sharing/', + ]; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Subscriptions/ISubscription.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Subscriptions/ISubscription.php new file mode 100644 index 0000000..e83082c --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Subscriptions/ISubscription.php @@ -0,0 +1,41 @@ +resourceTypeMapping['Sabre\\CalDAV\\Subscriptions\\ISubscription'] = + '{http://calendarserver.org/ns/}subscribed'; + + $server->xml->elementMap['{http://calendarserver.org/ns/}source'] = + 'Sabre\\DAV\\Xml\\Property\\Href'; + + $server->on('propFind', [$this, 'propFind'], 150); + } + + /** + * This method should return a list of server-features. + * + * This is for example 'versioning' and is added to the DAV: header + * in an OPTIONS response. + * + * @return array + */ + public function getFeatures() + { + return ['calendarserver-subscribed']; + } + + /** + * Triggered after properties have been fetched. + */ + public function propFind(PropFind $propFind, INode $node) + { + // There's a bunch of properties that must appear as a self-closing + // xml-element. This event handler ensures that this will be the case. + $props = [ + '{http://calendarserver.org/ns/}subscribed-strip-alarms', + '{http://calendarserver.org/ns/}subscribed-strip-attachments', + '{http://calendarserver.org/ns/}subscribed-strip-todos', + ]; + + foreach ($props as $prop) { + if (200 === $propFind->getStatus($prop)) { + $propFind->set($prop, '', 200); + } + } + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using \Sabre\DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'subscriptions'; + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'This plugin allows users to store iCalendar subscriptions in their calendar-home.', + 'link' => null, + ]; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php new file mode 100644 index 0000000..8d56e64 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php @@ -0,0 +1,204 @@ +caldavBackend = $caldavBackend; + $this->subscriptionInfo = $subscriptionInfo; + + $required = [ + 'id', + 'uri', + 'principaluri', + 'source', + ]; + + foreach ($required as $r) { + if (!isset($subscriptionInfo[$r])) { + throw new \InvalidArgumentException('The '.$r.' field is required when creating a subscription node'); + } + } + } + + /** + * Returns the name of the node. + * + * This is used to generate the url. + * + * @return string + */ + public function getName() + { + return $this->subscriptionInfo['uri']; + } + + /** + * Returns the last modification time. + * + * @return int|null + */ + public function getLastModified() + { + if (isset($this->subscriptionInfo['lastmodified'])) { + return $this->subscriptionInfo['lastmodified']; + } + } + + /** + * Deletes the current node. + */ + public function delete() + { + $this->caldavBackend->deleteSubscription( + $this->subscriptionInfo['id'] + ); + } + + /** + * Returns an array with all the child nodes. + * + * @return \Sabre\DAV\INode[] + */ + public function getChildren() + { + return []; + } + + /** + * Updates properties on this node. + * + * This method received a PropPatch object, which contains all the + * information about the update. + * + * To update specific properties, call the 'handle' method on this object. + * Read the PropPatch documentation for more information. + */ + public function propPatch(PropPatch $propPatch) + { + return $this->caldavBackend->updateSubscription( + $this->subscriptionInfo['id'], + $propPatch + ); + } + + /** + * Returns a list of properties for this nodes. + * + * The properties list is a list of propertynames the client requested, + * encoded in clark-notation {xmlnamespace}tagname. + * + * If the array is empty, it means 'all properties' were requested. + * + * Note that it's fine to liberally give properties back, instead of + * conforming to the list of requested properties. + * The Server class will filter out the extra. + * + * @param array $properties + * + * @return array + */ + public function getProperties($properties) + { + $r = []; + + foreach ($properties as $prop) { + switch ($prop) { + case '{http://calendarserver.org/ns/}source': + $r[$prop] = new Href($this->subscriptionInfo['source']); + break; + default: + if (array_key_exists($prop, $this->subscriptionInfo)) { + $r[$prop] = $this->subscriptionInfo[$prop]; + } + break; + } + } + + return $r; + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->subscriptionInfo['principaluri']; + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + return [ + [ + 'privilege' => '{DAV:}all', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}all', + 'principal' => $this->getOwner().'/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner().'/calendar-proxy-read', + 'protected' => true, + ], + ]; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Filter/CalendarData.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Filter/CalendarData.php new file mode 100644 index 0000000..c9656d8 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Filter/CalendarData.php @@ -0,0 +1,80 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $result = [ + 'contentType' => $reader->getAttribute('content-type') ?: 'text/calendar', + 'version' => $reader->getAttribute('version') ?: '2.0', + ]; + + $elems = (array) $reader->parseInnerTree(); + foreach ($elems as $elem) { + switch ($elem['name']) { + case '{'.Plugin::NS_CALDAV.'}expand': + $result['expand'] = [ + 'start' => isset($elem['attributes']['start']) ? DateTimeParser::parseDateTime($elem['attributes']['start']) : null, + 'end' => isset($elem['attributes']['end']) ? DateTimeParser::parseDateTime($elem['attributes']['end']) : null, + ]; + + if (!$result['expand']['start'] || !$result['expand']['end']) { + throw new BadRequest('The "start" and "end" attributes are required when expanding calendar-data'); + } + if ($result['expand']['end'] <= $result['expand']['start']) { + throw new BadRequest('The end-date must be larger than the start-date when expanding calendar-data'); + } + break; + } + } + + return $result; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Filter/CompFilter.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Filter/CompFilter.php new file mode 100644 index 0000000..929000b --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Filter/CompFilter.php @@ -0,0 +1,94 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $result = [ + 'name' => null, + 'is-not-defined' => false, + 'comp-filters' => [], + 'prop-filters' => [], + 'time-range' => false, + ]; + + $att = $reader->parseAttributes(); + $result['name'] = $att['name']; + + $elems = $reader->parseInnerTree(); + + if (is_array($elems)) { + foreach ($elems as $elem) { + switch ($elem['name']) { + case '{'.Plugin::NS_CALDAV.'}comp-filter': + $result['comp-filters'][] = $elem['value']; + break; + case '{'.Plugin::NS_CALDAV.'}prop-filter': + $result['prop-filters'][] = $elem['value']; + break; + case '{'.Plugin::NS_CALDAV.'}is-not-defined': + $result['is-not-defined'] = true; + break; + case '{'.Plugin::NS_CALDAV.'}time-range': + if ('VCALENDAR' === $result['name']) { + throw new BadRequest('You cannot add time-range filters on the VCALENDAR component'); + } + $result['time-range'] = [ + 'start' => isset($elem['attributes']['start']) ? DateTimeParser::parseDateTime($elem['attributes']['start']) : null, + 'end' => isset($elem['attributes']['end']) ? DateTimeParser::parseDateTime($elem['attributes']['end']) : null, + ]; + if ($result['time-range']['start'] && $result['time-range']['end'] && $result['time-range']['end'] <= $result['time-range']['start']) { + throw new BadRequest('The end-date must be larger than the start-date'); + } + break; + } + } + } + + return $result; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Filter/ParamFilter.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Filter/ParamFilter.php new file mode 100644 index 0000000..1e6dd59 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Filter/ParamFilter.php @@ -0,0 +1,79 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $result = [ + 'name' => null, + 'is-not-defined' => false, + 'text-match' => null, + ]; + + $att = $reader->parseAttributes(); + $result['name'] = $att['name']; + + $elems = $reader->parseInnerTree(); + + if (is_array($elems)) { + foreach ($elems as $elem) { + switch ($elem['name']) { + case '{'.Plugin::NS_CALDAV.'}is-not-defined': + $result['is-not-defined'] = true; + break; + case '{'.Plugin::NS_CALDAV.'}text-match': + $result['text-match'] = [ + 'negate-condition' => isset($elem['attributes']['negate-condition']) && 'yes' === $elem['attributes']['negate-condition'], + 'collation' => isset($elem['attributes']['collation']) ? $elem['attributes']['collation'] : 'i;ascii-casemap', + 'value' => $elem['value'], + ]; + break; + } + } + } + + return $result; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Filter/PropFilter.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Filter/PropFilter.php new file mode 100644 index 0000000..f1d66cc --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Filter/PropFilter.php @@ -0,0 +1,95 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $result = [ + 'name' => null, + 'is-not-defined' => false, + 'param-filters' => [], + 'text-match' => null, + 'time-range' => [], + ]; + + $att = $reader->parseAttributes(); + $result['name'] = $att['name']; + + $elems = $reader->parseInnerTree(); + + if (is_array($elems)) { + foreach ($elems as $elem) { + switch ($elem['name']) { + case '{'.Plugin::NS_CALDAV.'}param-filter': + $result['param-filters'][] = $elem['value']; + break; + case '{'.Plugin::NS_CALDAV.'}is-not-defined': + $result['is-not-defined'] = true; + break; + case '{'.Plugin::NS_CALDAV.'}time-range': + $result['time-range'] = [ + 'start' => isset($elem['attributes']['start']) ? DateTimeParser::parseDateTime($elem['attributes']['start']) : null, + 'end' => isset($elem['attributes']['end']) ? DateTimeParser::parseDateTime($elem['attributes']['end']) : null, + ]; + if ($result['time-range']['start'] && $result['time-range']['end'] && $result['time-range']['end'] <= $result['time-range']['start']) { + throw new BadRequest('The end-date must be larger than the start-date'); + } + break; + case '{'.Plugin::NS_CALDAV.'}text-match': + $result['text-match'] = [ + 'negate-condition' => isset($elem['attributes']['negate-condition']) && 'yes' === $elem['attributes']['negate-condition'], + 'collation' => isset($elem['attributes']['collation']) ? $elem['attributes']['collation'] : 'i;ascii-casemap', + 'value' => $elem['value'], + ]; + break; + } + } + } + + return $result; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php new file mode 100644 index 0000000..2dbb0f4 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php @@ -0,0 +1,290 @@ + $value) { + if (!property_exists($this, $key)) { + throw new \InvalidArgumentException('Unknown option: '.$key); + } + $this->$key = $value; + } + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + $writer->writeElement('{'.CalDAV\Plugin::NS_CALENDARSERVER.'}invite-notification'); + } + + /** + * This method serializes the entire notification, as it is used in the + * response body. + */ + public function xmlSerializeFull(Writer $writer) + { + $cs = '{'.CalDAV\Plugin::NS_CALENDARSERVER.'}'; + + $this->dtStamp->setTimezone(new \DateTimeZone('GMT')); + $writer->writeElement($cs.'dtstamp', $this->dtStamp->format('Ymd\\THis\\Z')); + + $writer->startElement($cs.'invite-notification'); + + $writer->writeElement($cs.'uid', $this->id); + $writer->writeElement('{DAV:}href', $this->href); + + switch ($this->type) { + case DAV\Sharing\Plugin::INVITE_ACCEPTED: + $writer->writeElement($cs.'invite-accepted'); + break; + case DAV\Sharing\Plugin::INVITE_NORESPONSE: + $writer->writeElement($cs.'invite-noresponse'); + break; + } + + $writer->writeElement($cs.'hosturl', [ + '{DAV:}href' => $writer->contextUri.$this->hostUrl, + ]); + + if ($this->summary) { + $writer->writeElement($cs.'summary', $this->summary); + } + + $writer->startElement($cs.'access'); + if ($this->readOnly) { + $writer->writeElement($cs.'read'); + } else { + $writer->writeElement($cs.'read-write'); + } + $writer->endElement(); // access + + $writer->startElement($cs.'organizer'); + // If the organizer contains a 'mailto:' part, it means it should be + // treated as absolute. + if ('mailto:' === strtolower(substr($this->organizer, 0, 7))) { + $writer->writeElement('{DAV:}href', $this->organizer); + } else { + $writer->writeElement('{DAV:}href', $writer->contextUri.$this->organizer); + } + if ($this->commonName) { + $writer->writeElement($cs.'common-name', $this->commonName); + } + if ($this->firstName) { + $writer->writeElement($cs.'first-name', $this->firstName); + } + if ($this->lastName) { + $writer->writeElement($cs.'last-name', $this->lastName); + } + $writer->endElement(); // organizer + + if ($this->commonName) { + $writer->writeElement($cs.'organizer-cn', $this->commonName); + } + if ($this->firstName) { + $writer->writeElement($cs.'organizer-first', $this->firstName); + } + if ($this->lastName) { + $writer->writeElement($cs.'organizer-last', $this->lastName); + } + if ($this->supportedComponents) { + $writer->writeElement('{'.CalDAV\Plugin::NS_CALDAV.'}supported-calendar-component-set', $this->supportedComponents); + } + + $writer->endElement(); // invite-notification + } + + /** + * Returns a unique id for this notification. + * + * This is just the base url. This should generally be some kind of unique + * id. + * + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * Returns the ETag for this notification. + * + * The ETag must be surrounded by literal double-quotes. + * + * @return string + */ + public function getETag() + { + return $this->etag; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php new file mode 100644 index 0000000..dbdba3b --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php @@ -0,0 +1,199 @@ + $value) { + if (!property_exists($this, $key)) { + throw new \InvalidArgumentException('Unknown option: '.$key); + } + $this->$key = $value; + } + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + $writer->writeElement('{'.CalDAV\Plugin::NS_CALENDARSERVER.'}invite-reply'); + } + + /** + * This method serializes the entire notification, as it is used in the + * response body. + */ + public function xmlSerializeFull(Writer $writer) + { + $cs = '{'.CalDAV\Plugin::NS_CALENDARSERVER.'}'; + + $this->dtStamp->setTimezone(new \DateTimeZone('GMT')); + $writer->writeElement($cs.'dtstamp', $this->dtStamp->format('Ymd\\THis\\Z')); + + $writer->startElement($cs.'invite-reply'); + + $writer->writeElement($cs.'uid', $this->id); + $writer->writeElement($cs.'in-reply-to', $this->inReplyTo); + $writer->writeElement('{DAV:}href', $this->href); + + switch ($this->type) { + case DAV\Sharing\Plugin::INVITE_ACCEPTED: + $writer->writeElement($cs.'invite-accepted'); + break; + case DAV\Sharing\Plugin::INVITE_DECLINED: + $writer->writeElement($cs.'invite-declined'); + break; + } + + $writer->writeElement($cs.'hosturl', [ + '{DAV:}href' => $writer->contextUri.$this->hostUrl, + ]); + + if ($this->summary) { + $writer->writeElement($cs.'summary', $this->summary); + } + $writer->endElement(); // invite-reply + } + + /** + * Returns a unique id for this notification. + * + * This is just the base url. This should generally be some kind of unique + * id. + * + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * Returns the ETag for this notification. + * + * The ETag must be surrounded by literal double-quotes. + * + * @return string + */ + public function getETag() + { + return $this->etag; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Notification/NotificationInterface.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Notification/NotificationInterface.php new file mode 100644 index 0000000..e1b393f --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Notification/NotificationInterface.php @@ -0,0 +1,43 @@ +id = $id; + $this->type = $type; + $this->description = $description; + $this->href = $href; + $this->etag = $etag; + } + + /** + * The serialize method is called during xml writing. + * + * It should use the $writer argument to encode this object into XML. + * + * Important note: it is not needed to create the parent element. The + * parent element is already created, and we only have to worry about + * attributes, child elements and text (if any). + * + * Important note 2: If you are writing any new elements, you are also + * responsible for closing them. + */ + public function xmlSerialize(Writer $writer) + { + switch ($this->type) { + case self::TYPE_LOW: + $type = 'low'; + break; + case self::TYPE_MEDIUM: + $type = 'medium'; + break; + default: + case self::TYPE_HIGH: + $type = 'high'; + break; + } + + $writer->startElement('{'.Plugin::NS_CALENDARSERVER.'}systemstatus'); + $writer->writeAttribute('type', $type); + $writer->endElement(); + } + + /** + * This method serializes the entire notification, as it is used in the + * response body. + */ + public function xmlSerializeFull(Writer $writer) + { + $cs = '{'.Plugin::NS_CALENDARSERVER.'}'; + switch ($this->type) { + case self::TYPE_LOW: + $type = 'low'; + break; + case self::TYPE_MEDIUM: + $type = 'medium'; + break; + default: + case self::TYPE_HIGH: + $type = 'high'; + break; + } + + $writer->startElement($cs.'systemstatus'); + $writer->writeAttribute('type', $type); + + if ($this->description) { + $writer->writeElement($cs.'description', $this->description); + } + if ($this->href) { + $writer->writeElement('{DAV:}href', $this->href); + } + + $writer->endElement(); // systemstatus + } + + /** + * Returns a unique id for this notification. + * + * This is just the base url. This should generally be some kind of unique + * id. + * + * @return string + */ + public function getId() + { + return $this->id; + } + + /* + * Returns the ETag for this notification. + * + * The ETag must be surrounded by literal double-quotes. + * + * @return string + */ + public function getETag() + { + return $this->etag; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/AllowedSharingModes.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/AllowedSharingModes.php new file mode 100644 index 0000000..58acb6d --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/AllowedSharingModes.php @@ -0,0 +1,81 @@ +canBeShared = $canBeShared; + $this->canBePublished = $canBePublished; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + if ($this->canBeShared) { + $writer->writeElement('{'.Plugin::NS_CALENDARSERVER.'}can-be-shared'); + } + if ($this->canBePublished) { + $writer->writeElement('{'.Plugin::NS_CALENDARSERVER.'}can-be-published'); + } + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/EmailAddressSet.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/EmailAddressSet.php new file mode 100644 index 0000000..84f7ae0 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/EmailAddressSet.php @@ -0,0 +1,71 @@ +emails = $emails; + } + + /** + * Returns the email addresses. + * + * @return array + */ + public function getValue() + { + return $this->emails; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + foreach ($this->emails as $email) { + $writer->writeElement('{http://calendarserver.org/ns/}email-address', $email); + } + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/Invite.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/Invite.php new file mode 100644 index 0000000..c389ca8 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/Invite.php @@ -0,0 +1,120 @@ +sharees = $sharees; + } + + /** + * Returns the list of users, as it was passed to the constructor. + * + * @return array + */ + public function getValue() + { + return $this->sharees; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + $cs = '{'.Plugin::NS_CALENDARSERVER.'}'; + + foreach ($this->sharees as $sharee) { + if (DAV\Sharing\Plugin::ACCESS_SHAREDOWNER === $sharee->access) { + $writer->startElement($cs.'organizer'); + } else { + $writer->startElement($cs.'user'); + + switch ($sharee->inviteStatus) { + case DAV\Sharing\Plugin::INVITE_ACCEPTED: + $writer->writeElement($cs.'invite-accepted'); + break; + case DAV\Sharing\Plugin::INVITE_DECLINED: + $writer->writeElement($cs.'invite-declined'); + break; + case DAV\Sharing\Plugin::INVITE_NORESPONSE: + $writer->writeElement($cs.'invite-noresponse'); + break; + case DAV\Sharing\Plugin::INVITE_INVALID: + $writer->writeElement($cs.'invite-invalid'); + break; + } + + $writer->startElement($cs.'access'); + switch ($sharee->access) { + case DAV\Sharing\Plugin::ACCESS_READWRITE: + $writer->writeElement($cs.'read-write'); + break; + case DAV\Sharing\Plugin::ACCESS_READ: + $writer->writeElement($cs.'read'); + break; + } + $writer->endElement(); // access + } + + $href = new DAV\Xml\Property\Href($sharee->href); + $href->xmlSerialize($writer); + + if (isset($sharee->properties['{DAV:}displayname'])) { + $writer->writeElement($cs.'common-name', $sharee->properties['{DAV:}displayname']); + } + if ($sharee->comment) { + $writer->writeElement($cs.'summary', $sharee->comment); + } + $writer->endElement(); // organizer or user + } + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/ScheduleCalendarTransp.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/ScheduleCalendarTransp.php new file mode 100644 index 0000000..1595220 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/ScheduleCalendarTransp.php @@ -0,0 +1,124 @@ +value = $value; + } + + /** + * Returns the current value. + * + * @return string + */ + public function getValue() + { + return $this->value; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + switch ($this->value) { + case self::TRANSPARENT: + $writer->writeElement('{'.Plugin::NS_CALDAV.'}transparent'); + break; + case self::OPAQUE: + $writer->writeElement('{'.Plugin::NS_CALDAV.'}opaque'); + break; + } + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $elems = Deserializer\enum($reader, Plugin::NS_CALDAV); + + if (in_array('transparent', $elems)) { + $value = self::TRANSPARENT; + } else { + $value = self::OPAQUE; + } + + return new self($value); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarComponentSet.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarComponentSet.php new file mode 100644 index 0000000..d86e7b7 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarComponentSet.php @@ -0,0 +1,118 @@ +components = $components; + } + + /** + * Returns the list of supported components. + * + * @return array + */ + public function getValue() + { + return $this->components; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + foreach ($this->components as $component) { + $writer->startElement('{'.Plugin::NS_CALDAV.'}comp'); + $writer->writeAttributes(['name' => $component]); + $writer->endElement(); + } + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $elems = $reader->parseInnerTree(); + + $components = []; + + foreach ((array) $elems as $elem) { + if ($elem['name'] === '{'.Plugin::NS_CALDAV.'}comp') { + $components[] = $elem['attributes']['name']; + } + } + + if (!$components) { + throw new ParseException('supported-calendar-component-set must have at least one CALDAV:comp element'); + } + + return new self($components); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarData.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarData.php new file mode 100644 index 0000000..5b08933 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarData.php @@ -0,0 +1,57 @@ +startElement('{'.Plugin::NS_CALDAV.'}calendar-data'); + $writer->writeAttributes([ + 'content-type' => 'text/calendar', + 'version' => '2.0', + ]); + $writer->endElement(); // calendar-data + $writer->startElement('{'.Plugin::NS_CALDAV.'}calendar-data'); + $writer->writeAttributes([ + 'content-type' => 'application/calendar+json', + ]); + $writer->endElement(); // calendar-data + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCollationSet.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCollationSet.php new file mode 100644 index 0000000..c5ffeee --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCollationSet.php @@ -0,0 +1,54 @@ +writeElement('{'.Plugin::NS_CALDAV.'}supported-collation', $collation); + } + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/CalendarMultiGetReport.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/CalendarMultiGetReport.php new file mode 100644 index 0000000..4771a20 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/CalendarMultiGetReport.php @@ -0,0 +1,119 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $elems = $reader->parseInnerTree([ + '{urn:ietf:params:xml:ns:caldav}calendar-data' => 'Sabre\\CalDAV\\Xml\\Filter\\CalendarData', + '{DAV:}prop' => 'Sabre\\Xml\\Element\\KeyValue', + ]); + + $newProps = [ + 'hrefs' => [], + 'properties' => [], + ]; + + foreach ($elems as $elem) { + switch ($elem['name']) { + case '{DAV:}prop': + $newProps['properties'] = array_keys($elem['value']); + if (isset($elem['value']['{'.Plugin::NS_CALDAV.'}calendar-data'])) { + $newProps += $elem['value']['{'.Plugin::NS_CALDAV.'}calendar-data']; + } + break; + case '{DAV:}href': + $newProps['hrefs'][] = Uri\resolve($reader->contextUri, $elem['value']); + break; + } + } + + $obj = new self(); + foreach ($newProps as $key => $value) { + $obj->$key = $value; + } + + return $obj; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/CalendarQueryReport.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/CalendarQueryReport.php new file mode 100644 index 0000000..5a4df46 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/CalendarQueryReport.php @@ -0,0 +1,137 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $elems = $reader->parseInnerTree([ + '{urn:ietf:params:xml:ns:caldav}comp-filter' => 'Sabre\\CalDAV\\Xml\\Filter\\CompFilter', + '{urn:ietf:params:xml:ns:caldav}prop-filter' => 'Sabre\\CalDAV\\Xml\\Filter\\PropFilter', + '{urn:ietf:params:xml:ns:caldav}param-filter' => 'Sabre\\CalDAV\\Xml\\Filter\\ParamFilter', + '{urn:ietf:params:xml:ns:caldav}calendar-data' => 'Sabre\\CalDAV\\Xml\\Filter\\CalendarData', + '{DAV:}prop' => 'Sabre\\Xml\\Element\\KeyValue', + ]); + + $newProps = [ + 'filters' => null, + 'properties' => [], + ]; + + if (!is_array($elems)) { + $elems = []; + } + + foreach ($elems as $elem) { + switch ($elem['name']) { + case '{DAV:}prop': + $newProps['properties'] = array_keys($elem['value']); + if (isset($elem['value']['{'.Plugin::NS_CALDAV.'}calendar-data'])) { + $newProps += $elem['value']['{'.Plugin::NS_CALDAV.'}calendar-data']; + } + break; + case '{'.Plugin::NS_CALDAV.'}filter': + foreach ($elem['value'] as $subElem) { + if ($subElem['name'] === '{'.Plugin::NS_CALDAV.'}comp-filter') { + if (!is_null($newProps['filters'])) { + throw new BadRequest('Only one top-level comp-filter may be defined'); + } + $newProps['filters'] = $subElem['value']; + } + } + break; + } + } + + if (is_null($newProps['filters'])) { + throw new BadRequest('The {'.Plugin::NS_CALDAV.'}filter element is required for this request'); + } + + $obj = new self(); + foreach ($newProps as $key => $value) { + $obj->$key = $value; + } + + return $obj; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/FreeBusyQueryReport.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/FreeBusyQueryReport.php new file mode 100644 index 0000000..17df05a --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/FreeBusyQueryReport.php @@ -0,0 +1,90 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $timeRange = '{'.Plugin::NS_CALDAV.'}time-range'; + + $start = null; + $end = null; + + foreach ((array) $reader->parseInnerTree([]) as $elem) { + if ($elem['name'] !== $timeRange) { + continue; + } + + $start = empty($elem['attributes']['start']) ?: $elem['attributes']['start']; + $end = empty($elem['attributes']['end']) ?: $elem['attributes']['end']; + } + if (!$start && !$end) { + throw new BadRequest('The freebusy report must have a time-range element'); + } + if ($start) { + $start = DateTimeParser::parseDateTime($start); + } + if ($end) { + $end = DateTimeParser::parseDateTime($end); + } + $result = new self(); + $result->start = $start; + $result->end = $end; + + return $result; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php new file mode 100644 index 0000000..166721e --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php @@ -0,0 +1,145 @@ +href = $href; + $this->calendarUri = $calendarUri; + $this->inReplyTo = $inReplyTo; + $this->summary = $summary; + $this->status = $status; + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $elems = KeyValue::xmlDeserialize($reader); + + $href = null; + $calendarUri = null; + $inReplyTo = null; + $summary = null; + $status = null; + + foreach ($elems as $name => $value) { + switch ($name) { + case '{'.Plugin::NS_CALENDARSERVER.'}hosturl': + foreach ($value as $bla) { + if ('{DAV:}href' === $bla['name']) { + $calendarUri = $bla['value']; + } + } + break; + case '{'.Plugin::NS_CALENDARSERVER.'}invite-accepted': + $status = DAV\Sharing\Plugin::INVITE_ACCEPTED; + break; + case '{'.Plugin::NS_CALENDARSERVER.'}invite-declined': + $status = DAV\Sharing\Plugin::INVITE_DECLINED; + break; + case '{'.Plugin::NS_CALENDARSERVER.'}in-reply-to': + $inReplyTo = $value; + break; + case '{'.Plugin::NS_CALENDARSERVER.'}summary': + $summary = $value; + break; + case '{DAV:}href': + $href = $value; + break; + } + } + if (is_null($calendarUri)) { + throw new BadRequest('The {http://calendarserver.org/ns/}hosturl/{DAV:}href element must exist'); + } + + return new self($href, $calendarUri, $inReplyTo, $summary, $status); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/MkCalendar.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/MkCalendar.php new file mode 100644 index 0000000..b5701e2 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/MkCalendar.php @@ -0,0 +1,77 @@ +properties; + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $self = new self(); + + $elementMap = $reader->elementMap; + $elementMap['{DAV:}prop'] = 'Sabre\DAV\Xml\Element\Prop'; + $elementMap['{DAV:}set'] = 'Sabre\Xml\Element\KeyValue'; + $elems = $reader->parseInnerTree($elementMap); + + foreach ($elems as $elem) { + if ('{DAV:}set' === $elem['name']) { + $self->properties = array_merge($self->properties, $elem['value']['{DAV:}prop']); + } + } + + return $self; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/Share.php b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/Share.php new file mode 100644 index 0000000..d597b76 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CalDAV/Xml/Request/Share.php @@ -0,0 +1,107 @@ +sharees = $sharees; + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $elems = $reader->parseGetElements([ + '{'.Plugin::NS_CALENDARSERVER.'}set' => 'Sabre\\Xml\\Element\\KeyValue', + '{'.Plugin::NS_CALENDARSERVER.'}remove' => 'Sabre\\Xml\\Element\\KeyValue', + ]); + + $sharees = []; + + foreach ($elems as $elem) { + switch ($elem['name']) { + case '{'.Plugin::NS_CALENDARSERVER.'}set': + $sharee = $elem['value']; + + $sumElem = '{'.Plugin::NS_CALENDARSERVER.'}summary'; + $commonName = '{'.Plugin::NS_CALENDARSERVER.'}common-name'; + + $properties = []; + if (isset($sharee[$commonName])) { + $properties['{DAV:}displayname'] = $sharee[$commonName]; + } + + $access = array_key_exists('{'.Plugin::NS_CALENDARSERVER.'}read-write', $sharee) + ? \Sabre\DAV\Sharing\Plugin::ACCESS_READWRITE + : \Sabre\DAV\Sharing\Plugin::ACCESS_READ; + + $sharees[] = new Sharee([ + 'href' => $sharee['{DAV:}href'], + 'properties' => $properties, + 'access' => $access, + 'comment' => isset($sharee[$sumElem]) ? $sharee[$sumElem] : null, + ]); + break; + + case '{'.Plugin::NS_CALENDARSERVER.'}remove': + $sharees[] = new Sharee([ + 'href' => $elem['value']['{DAV:}href'], + 'access' => \Sabre\DAV\Sharing\Plugin::ACCESS_NOACCESS, + ]); + break; + } + } + + return new self($sharees); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/AddressBook.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/AddressBook.php new file mode 100644 index 0000000..f5744f6 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/AddressBook.php @@ -0,0 +1,335 @@ +carddavBackend = $carddavBackend; + $this->addressBookInfo = $addressBookInfo; + } + + /** + * Returns the name of the addressbook. + * + * @return string + */ + public function getName() + { + return $this->addressBookInfo['uri']; + } + + /** + * Returns a card. + * + * @param string $name + * + * @return Card + */ + public function getChild($name) + { + $obj = $this->carddavBackend->getCard($this->addressBookInfo['id'], $name); + if (!$obj) { + throw new DAV\Exception\NotFound('Card not found'); + } + + return new Card($this->carddavBackend, $this->addressBookInfo, $obj); + } + + /** + * Returns the full list of cards. + * + * @return array + */ + public function getChildren() + { + $objs = $this->carddavBackend->getCards($this->addressBookInfo['id']); + $children = []; + foreach ($objs as $obj) { + $obj['acl'] = $this->getChildACL(); + $children[] = new Card($this->carddavBackend, $this->addressBookInfo, $obj); + } + + return $children; + } + + /** + * This method receives a list of paths in it's first argument. + * It must return an array with Node objects. + * + * If any children are not found, you do not have to return them. + * + * @param string[] $paths + * + * @return array + */ + public function getMultipleChildren(array $paths) + { + $objs = $this->carddavBackend->getMultipleCards($this->addressBookInfo['id'], $paths); + $children = []; + foreach ($objs as $obj) { + $obj['acl'] = $this->getChildACL(); + $children[] = new Card($this->carddavBackend, $this->addressBookInfo, $obj); + } + + return $children; + } + + /** + * Creates a new directory. + * + * We actually block this, as subdirectories are not allowed in addressbooks. + * + * @param string $name + */ + public function createDirectory($name) + { + throw new DAV\Exception\MethodNotAllowed('Creating collections in addressbooks is not allowed'); + } + + /** + * Creates a new file. + * + * The contents of the new file must be a valid VCARD. + * + * This method may return an ETag. + * + * @param string $name + * @param resource $data + * + * @return string|null + */ + public function createFile($name, $data = null) + { + if (is_resource($data)) { + $data = stream_get_contents($data); + } + // Converting to UTF-8, if needed + $data = DAV\StringUtil::ensureUTF8($data); + + return $this->carddavBackend->createCard($this->addressBookInfo['id'], $name, $data); + } + + /** + * Deletes the entire addressbook. + */ + public function delete() + { + $this->carddavBackend->deleteAddressBook($this->addressBookInfo['id']); + } + + /** + * Renames the addressbook. + * + * @param string $newName + */ + public function setName($newName) + { + throw new DAV\Exception\MethodNotAllowed('Renaming addressbooks is not yet supported'); + } + + /** + * Returns the last modification date as a unix timestamp. + */ + public function getLastModified() + { + return null; + } + + /** + * Updates properties on this node. + * + * This method received a PropPatch object, which contains all the + * information about the update. + * + * To update specific properties, call the 'handle' method on this object. + * Read the PropPatch documentation for more information. + */ + public function propPatch(DAV\PropPatch $propPatch) + { + return $this->carddavBackend->updateAddressBook($this->addressBookInfo['id'], $propPatch); + } + + /** + * Returns a list of properties for this nodes. + * + * The properties list is a list of propertynames the client requested, + * encoded in clark-notation {xmlnamespace}tagname + * + * If the array is empty, it means 'all properties' were requested. + * + * @param array $properties + * + * @return array + */ + public function getProperties($properties) + { + $response = []; + foreach ($properties as $propertyName) { + if (isset($this->addressBookInfo[$propertyName])) { + $response[$propertyName] = $this->addressBookInfo[$propertyName]; + } + } + + return $response; + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->addressBookInfo['principaluri']; + } + + /** + * This method returns the ACL's for card nodes in this address book. + * The result of this method automatically gets passed to the + * card nodes in this address book. + * + * @return array + */ + public function getChildACL() + { + return [ + [ + 'privilege' => '{DAV:}all', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + ]; + } + + /** + * This method returns the current sync-token for this collection. + * This can be any string. + * + * If null is returned from this function, the plugin assumes there's no + * sync information available. + * + * @return string|null + */ + public function getSyncToken() + { + if ( + $this->carddavBackend instanceof Backend\SyncSupport && + isset($this->addressBookInfo['{DAV:}sync-token']) + ) { + return $this->addressBookInfo['{DAV:}sync-token']; + } + if ( + $this->carddavBackend instanceof Backend\SyncSupport && + isset($this->addressBookInfo['{http://sabredav.org/ns}sync-token']) + ) { + return $this->addressBookInfo['{http://sabredav.org/ns}sync-token']; + } + } + + /** + * The getChanges method returns all the changes that have happened, since + * the specified syncToken and the current collection. + * + * This function should return an array, such as the following: + * + * [ + * 'syncToken' => 'The current synctoken', + * 'added' => [ + * 'new.txt', + * ], + * 'modified' => [ + * 'modified.txt', + * ], + * 'deleted' => [ + * 'foo.php.bak', + * 'old.txt' + * ] + * ]; + * + * The syncToken property should reflect the *current* syncToken of the + * collection, as reported getSyncToken(). This is needed here too, to + * ensure the operation is atomic. + * + * If the syncToken is specified as null, this is an initial sync, and all + * members should be reported. + * + * The modified property is an array of nodenames that have changed since + * the last token. + * + * The deleted property is an array with nodenames, that have been deleted + * from collection. + * + * The second argument is basically the 'depth' of the report. If it's 1, + * you only have to report changes that happened only directly in immediate + * descendants. If it's 2, it should also include changes from the nodes + * below the child collections. (grandchildren) + * + * The third (optional) argument allows a client to specify how many + * results should be returned at most. If the limit is not specified, it + * should be treated as infinite. + * + * If the limit (infinite or not) is higher than you're willing to return, + * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. + * + * If the syncToken is expired (due to data cleanup) or unknown, you must + * return null. + * + * The limit is 'suggestive'. You are free to ignore it. + * + * @param string $syncToken + * @param int $syncLevel + * @param int $limit + * + * @return array|null + */ + public function getChanges($syncToken, $syncLevel, $limit = null) + { + if (!$this->carddavBackend instanceof Backend\SyncSupport) { + return null; + } + + return $this->carddavBackend->getChangesForAddressBook( + $this->addressBookInfo['id'], + $syncToken, + $syncLevel, + $limit + ); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/AddressBookHome.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/AddressBookHome.php new file mode 100644 index 0000000..d7365fb --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/AddressBookHome.php @@ -0,0 +1,178 @@ +carddavBackend = $carddavBackend; + $this->principalUri = $principalUri; + } + + /** + * Returns the name of this object. + * + * @return string + */ + public function getName() + { + list(, $name) = Uri\split($this->principalUri); + + return $name; + } + + /** + * Updates the name of this object. + * + * @param string $name + */ + public function setName($name) + { + throw new DAV\Exception\MethodNotAllowed(); + } + + /** + * Deletes this object. + */ + public function delete() + { + throw new DAV\Exception\MethodNotAllowed(); + } + + /** + * Returns the last modification date. + * + * @return int + */ + public function getLastModified() + { + return null; + } + + /** + * Creates a new file under this object. + * + * This is currently not allowed + * + * @param string $name + * @param resource $data + */ + public function createFile($name, $data = null) + { + throw new DAV\Exception\MethodNotAllowed('Creating new files in this collection is not supported'); + } + + /** + * Creates a new directory under this object. + * + * This is currently not allowed. + * + * @param string $filename + */ + public function createDirectory($filename) + { + throw new DAV\Exception\MethodNotAllowed('Creating new collections in this collection is not supported'); + } + + /** + * Returns a single addressbook, by name. + * + * @param string $name + * + * @todo needs optimizing + * + * @return AddressBook + */ + public function getChild($name) + { + foreach ($this->getChildren() as $child) { + if ($name == $child->getName()) { + return $child; + } + } + throw new DAV\Exception\NotFound('Addressbook with name \''.$name.'\' could not be found'); + } + + /** + * Returns a list of addressbooks. + * + * @return array + */ + public function getChildren() + { + $addressbooks = $this->carddavBackend->getAddressBooksForUser($this->principalUri); + $objs = []; + foreach ($addressbooks as $addressbook) { + $objs[] = new AddressBook($this->carddavBackend, $addressbook); + } + + return $objs; + } + + /** + * Creates a new address book. + * + * @param string $name + * + * @throws DAV\Exception\InvalidResourceType + */ + public function createExtendedCollection($name, MkCol $mkCol) + { + if (!$mkCol->hasResourceType('{'.Plugin::NS_CARDDAV.'}addressbook')) { + throw new DAV\Exception\InvalidResourceType('Unknown resourceType for this collection'); + } + $properties = $mkCol->getRemainingValues(); + $mkCol->setRemainingResultCode(201); + $this->carddavBackend->createAddressBook($this->principalUri, $name, $properties); + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->principalUri; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/AddressBookRoot.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/AddressBookRoot.php new file mode 100644 index 0000000..ee1721a --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/AddressBookRoot.php @@ -0,0 +1,75 @@ +carddavBackend = $carddavBackend; + parent::__construct($principalBackend, $principalPrefix); + } + + /** + * Returns the name of the node. + * + * @return string + */ + public function getName() + { + return Plugin::ADDRESSBOOK_ROOT; + } + + /** + * This method returns a node for a principal. + * + * The passed array contains principal information, and is guaranteed to + * at least contain a uri item. Other properties may or may not be + * supplied by the authentication backend. + * + * @return \Sabre\DAV\INode + */ + public function getChildForPrincipal(array $principal) + { + return new AddressBookHome($this->carddavBackend, $principal['uri']); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/Backend/AbstractBackend.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/Backend/AbstractBackend.php new file mode 100644 index 0000000..a900c62 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/Backend/AbstractBackend.php @@ -0,0 +1,38 @@ +getCard($addressBookId, $uri); + }, $uris); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/Backend/BackendInterface.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/Backend/BackendInterface.php new file mode 100644 index 0000000..f9955ac --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/Backend/BackendInterface.php @@ -0,0 +1,194 @@ +pdo = $pdo; + } + + /** + * Returns the list of addressbooks for a specific user. + * + * @param string $principalUri + * + * @return array + */ + public function getAddressBooksForUser($principalUri) + { + $stmt = $this->pdo->prepare('SELECT id, uri, displayname, principaluri, description, synctoken FROM '.$this->addressBooksTableName.' WHERE principaluri = ?'); + $stmt->execute([$principalUri]); + + $addressBooks = []; + + foreach ($stmt->fetchAll() as $row) { + $addressBooks[] = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'principaluri' => $row['principaluri'], + '{DAV:}displayname' => $row['displayname'], + '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description' => $row['description'], + '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0', + ]; + } + + return $addressBooks; + } + + /** + * Updates properties for an address book. + * + * The list of mutations is stored in a Sabre\DAV\PropPatch object. + * To do the actual updates, you must tell this object which properties + * you're going to process with the handle() method. + * + * Calling the handle method is like telling the PropPatch object "I + * promise I can handle updating this property". + * + * Read the PropPatch documentation for more info and examples. + * + * @param string $addressBookId + */ + public function updateAddressBook($addressBookId, PropPatch $propPatch) + { + $supportedProperties = [ + '{DAV:}displayname', + '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description', + ]; + + $propPatch->handle($supportedProperties, function ($mutations) use ($addressBookId) { + $updates = []; + foreach ($mutations as $property => $newValue) { + switch ($property) { + case '{DAV:}displayname': + $updates['displayname'] = $newValue; + break; + case '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description': + $updates['description'] = $newValue; + break; + } + } + $query = 'UPDATE '.$this->addressBooksTableName.' SET '; + $first = true; + foreach ($updates as $key => $value) { + if ($first) { + $first = false; + } else { + $query .= ', '; + } + $query .= ' '.$key.' = :'.$key.' '; + } + $query .= ' WHERE id = :addressbookid'; + + $stmt = $this->pdo->prepare($query); + $updates['addressbookid'] = $addressBookId; + + $stmt->execute($updates); + + $this->addChange($addressBookId, '', 2); + + return true; + }); + } + + /** + * Creates a new address book. + * + * @param string $principalUri + * @param string $url just the 'basename' of the url + * + * @return int Last insert id + */ + public function createAddressBook($principalUri, $url, array $properties) + { + $values = [ + 'displayname' => null, + 'description' => null, + 'principaluri' => $principalUri, + 'uri' => $url, + ]; + + foreach ($properties as $property => $newValue) { + switch ($property) { + case '{DAV:}displayname': + $values['displayname'] = $newValue; + break; + case '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description': + $values['description'] = $newValue; + break; + default: + throw new DAV\Exception\BadRequest('Unknown property: '.$property); + } + } + + $query = 'INSERT INTO '.$this->addressBooksTableName.' (uri, displayname, description, principaluri, synctoken) VALUES (:uri, :displayname, :description, :principaluri, 1)'; + $stmt = $this->pdo->prepare($query); + $stmt->execute($values); + + return $this->pdo->lastInsertId( + $this->addressBooksTableName.'_id_seq' + ); + } + + /** + * Deletes an entire addressbook and all its contents. + * + * @param int $addressBookId + */ + public function deleteAddressBook($addressBookId) + { + $stmt = $this->pdo->prepare('DELETE FROM '.$this->cardsTableName.' WHERE addressbookid = ?'); + $stmt->execute([$addressBookId]); + + $stmt = $this->pdo->prepare('DELETE FROM '.$this->addressBooksTableName.' WHERE id = ?'); + $stmt->execute([$addressBookId]); + + $stmt = $this->pdo->prepare('DELETE FROM '.$this->addressBookChangesTableName.' WHERE addressbookid = ?'); + $stmt->execute([$addressBookId]); + } + + /** + * Returns all cards for a specific addressbook id. + * + * This method should return the following properties for each card: + * * carddata - raw vcard data + * * uri - Some unique url + * * lastmodified - A unix timestamp + * + * It's recommended to also return the following properties: + * * etag - A unique etag. This must change every time the card changes. + * * size - The size of the card in bytes. + * + * If these last two properties are provided, less time will be spent + * calculating them. If they are specified, you can also omit carddata. + * This may speed up certain requests, especially with large cards. + * + * @param mixed $addressbookId + * + * @return array + */ + public function getCards($addressbookId) + { + $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, size FROM '.$this->cardsTableName.' WHERE addressbookid = ?'); + $stmt->execute([$addressbookId]); + + $result = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $row['etag'] = '"'.$row['etag'].'"'; + $row['lastmodified'] = (int) $row['lastmodified']; + $result[] = $row; + } + + return $result; + } + + /** + * Returns a specific card. + * + * The same set of properties must be returned as with getCards. The only + * exception is that 'carddata' is absolutely required. + * + * If the card does not exist, you must return false. + * + * @param mixed $addressBookId + * @param string $cardUri + * + * @return array + */ + public function getCard($addressBookId, $cardUri) + { + $stmt = $this->pdo->prepare('SELECT id, carddata, uri, lastmodified, etag, size FROM '.$this->cardsTableName.' WHERE addressbookid = ? AND uri = ? LIMIT 1'); + $stmt->execute([$addressBookId, $cardUri]); + + $result = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (!$result) { + return false; + } + + $result['etag'] = '"'.$result['etag'].'"'; + $result['lastmodified'] = (int) $result['lastmodified']; + + return $result; + } + + /** + * Returns a list of cards. + * + * This method should work identical to getCard, but instead return all the + * cards in the list as an array. + * + * If the backend supports this, it may allow for some speed-ups. + * + * @param mixed $addressBookId + * + * @return array + */ + public function getMultipleCards($addressBookId, array $uris) + { + $query = 'SELECT id, uri, lastmodified, etag, size, carddata FROM '.$this->cardsTableName.' WHERE addressbookid = ? AND uri IN ('; + // Inserting a whole bunch of question marks + $query .= implode(',', array_fill(0, count($uris), '?')); + $query .= ')'; + + $stmt = $this->pdo->prepare($query); + $stmt->execute(array_merge([$addressBookId], $uris)); + $result = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $row['etag'] = '"'.$row['etag'].'"'; + $row['lastmodified'] = (int) $row['lastmodified']; + $result[] = $row; + } + + return $result; + } + + /** + * Creates a new card. + * + * The addressbook id will be passed as the first argument. This is the + * same id as it is returned from the getAddressBooksForUser method. + * + * The cardUri is a base uri, and doesn't include the full path. The + * cardData argument is the vcard body, and is passed as a string. + * + * It is possible to return an ETag from this method. This ETag is for the + * newly created resource, and must be enclosed with double quotes (that + * is, the string itself must contain the double quotes). + * + * You should only return the ETag if you store the carddata as-is. If a + * subsequent GET request on the same card does not have the same body, + * byte-by-byte and you did return an ETag here, clients tend to get + * confused. + * + * If you don't return an ETag, you can just return null. + * + * @param mixed $addressBookId + * @param string $cardUri + * @param string $cardData + * + * @return string|null + */ + public function createCard($addressBookId, $cardUri, $cardData) + { + $stmt = $this->pdo->prepare('INSERT INTO '.$this->cardsTableName.' (carddata, uri, lastmodified, addressbookid, size, etag) VALUES (?, ?, ?, ?, ?, ?)'); + + $etag = md5($cardData); + + $stmt->execute([ + $cardData, + $cardUri, + time(), + $addressBookId, + strlen($cardData), + $etag, + ]); + + $this->addChange($addressBookId, $cardUri, 1); + + return '"'.$etag.'"'; + } + + /** + * Updates a card. + * + * The addressbook id will be passed as the first argument. This is the + * same id as it is returned from the getAddressBooksForUser method. + * + * The cardUri is a base uri, and doesn't include the full path. The + * cardData argument is the vcard body, and is passed as a string. + * + * It is possible to return an ETag from this method. This ETag should + * match that of the updated resource, and must be enclosed with double + * quotes (that is: the string itself must contain the actual quotes). + * + * You should only return the ETag if you store the carddata as-is. If a + * subsequent GET request on the same card does not have the same body, + * byte-by-byte and you did return an ETag here, clients tend to get + * confused. + * + * If you don't return an ETag, you can just return null. + * + * @param mixed $addressBookId + * @param string $cardUri + * @param string $cardData + * + * @return string|null + */ + public function updateCard($addressBookId, $cardUri, $cardData) + { + $stmt = $this->pdo->prepare('UPDATE '.$this->cardsTableName.' SET carddata = ?, lastmodified = ?, size = ?, etag = ? WHERE uri = ? AND addressbookid =?'); + + $etag = md5($cardData); + $stmt->execute([ + $cardData, + time(), + strlen($cardData), + $etag, + $cardUri, + $addressBookId, + ]); + + $this->addChange($addressBookId, $cardUri, 2); + + return '"'.$etag.'"'; + } + + /** + * Deletes a card. + * + * @param mixed $addressBookId + * @param string $cardUri + * + * @return bool + */ + public function deleteCard($addressBookId, $cardUri) + { + $stmt = $this->pdo->prepare('DELETE FROM '.$this->cardsTableName.' WHERE addressbookid = ? AND uri = ?'); + $stmt->execute([$addressBookId, $cardUri]); + + $this->addChange($addressBookId, $cardUri, 3); + + return 1 === $stmt->rowCount(); + } + + /** + * The getChanges method returns all the changes that have happened, since + * the specified syncToken in the specified address book. + * + * This function should return an array, such as the following: + * + * [ + * 'syncToken' => 'The current synctoken', + * 'added' => [ + * 'new.txt', + * ], + * 'modified' => [ + * 'updated.txt', + * ], + * 'deleted' => [ + * 'foo.php.bak', + * 'old.txt' + * ] + * ]; + * + * The returned syncToken property should reflect the *current* syncToken + * of the addressbook, as reported in the {http://sabredav.org/ns}sync-token + * property. This is needed here too, to ensure the operation is atomic. + * + * If the $syncToken argument is specified as null, this is an initial + * sync, and all members should be reported. + * + * The modified property is an array of nodenames that have changed since + * the last token. + * + * The deleted property is an array with nodenames, that have been deleted + * from collection. + * + * The $syncLevel argument is basically the 'depth' of the report. If it's + * 1, you only have to report changes that happened only directly in + * immediate descendants. If it's 2, it should also include changes from + * the nodes below the child collections. (grandchildren) + * + * The $limit argument allows a client to specify how many results should + * be returned at most. If the limit is not specified, it should be treated + * as infinite. + * + * If the limit (infinite or not) is higher than you're willing to return, + * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. + * + * If the syncToken is expired (due to data cleanup) or unknown, you must + * return null. + * + * The limit is 'suggestive'. You are free to ignore it. + * + * @param string $addressBookId + * @param string $syncToken + * @param int $syncLevel + * @param int $limit + * + * @return array|null + */ + public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) + { + // Current synctoken + $stmt = $this->pdo->prepare('SELECT synctoken FROM '.$this->addressBooksTableName.' WHERE id = ?'); + $stmt->execute([$addressBookId]); + $currentToken = $stmt->fetchColumn(0); + + if (is_null($currentToken)) { + return null; + } + + $result = [ + 'syncToken' => $currentToken, + 'added' => [], + 'modified' => [], + 'deleted' => [], + ]; + + if ($syncToken) { + $query = 'SELECT uri, operation FROM '.$this->addressBookChangesTableName.' WHERE synctoken >= ? AND synctoken < ? AND addressbookid = ? ORDER BY synctoken'; + if ($limit > 0) { + $query .= ' LIMIT '.(int) $limit; + } + + // Fetching all changes + $stmt = $this->pdo->prepare($query); + $stmt->execute([$syncToken, $currentToken, $addressBookId]); + + $changes = []; + + // This loop ensures that any duplicates are overwritten, only the + // last change on a node is relevant. + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $changes[$row['uri']] = $row['operation']; + } + + foreach ($changes as $uri => $operation) { + switch ($operation) { + case 1: + $result['added'][] = $uri; + break; + case 2: + $result['modified'][] = $uri; + break; + case 3: + $result['deleted'][] = $uri; + break; + } + } + } else { + // No synctoken supplied, this is the initial sync. + $query = 'SELECT uri FROM '.$this->cardsTableName.' WHERE addressbookid = ?'; + $stmt = $this->pdo->prepare($query); + $stmt->execute([$addressBookId]); + + $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN); + } + + return $result; + } + + /** + * Adds a change record to the addressbookchanges table. + * + * @param mixed $addressBookId + * @param string $objectUri + * @param int $operation 1 = add, 2 = modify, 3 = delete + */ + protected function addChange($addressBookId, $objectUri, $operation) + { + $stmt = $this->pdo->prepare('INSERT INTO '.$this->addressBookChangesTableName.' (uri, synctoken, addressbookid, operation) SELECT ?, synctoken, ?, ? FROM '.$this->addressBooksTableName.' WHERE id = ?'); + $stmt->execute([ + $objectUri, + $addressBookId, + $operation, + $addressBookId, + ]); + $stmt = $this->pdo->prepare('UPDATE '.$this->addressBooksTableName.' SET synctoken = synctoken + 1 WHERE id = ?'); + $stmt->execute([ + $addressBookId, + ]); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/Backend/SyncSupport.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/Backend/SyncSupport.php new file mode 100644 index 0000000..6aaad14 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/Backend/SyncSupport.php @@ -0,0 +1,83 @@ + 'The current synctoken', + * 'added' => [ + * 'new.txt', + * ], + * 'modified' => [ + * 'modified.txt', + * ], + * 'deleted' => [ + * 'foo.php.bak', + * 'old.txt' + * ] + * ]; + * + * The returned syncToken property should reflect the *current* syncToken + * of the calendar, as reported in the {http://sabredav.org/ns}sync-token + * property. This is needed here too, to ensure the operation is atomic. + * + * If the $syncToken argument is specified as null, this is an initial + * sync, and all members should be reported. + * + * The modified property is an array of nodenames that have changed since + * the last token. + * + * The deleted property is an array with nodenames, that have been deleted + * from collection. + * + * The $syncLevel argument is basically the 'depth' of the report. If it's + * 1, you only have to report changes that happened only directly in + * immediate descendants. If it's 2, it should also include changes from + * the nodes below the child collections. (grandchildren) + * + * The $limit argument allows a client to specify how many results should + * be returned at most. If the limit is not specified, it should be treated + * as infinite. + * + * If the limit (infinite or not) is higher than you're willing to return, + * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. + * + * If the syncToken is expired (due to data cleanup) or unknown, you must + * return null. + * + * The limit is 'suggestive'. You are free to ignore it. + * + * @param string $addressBookId + * @param string $syncToken + * @param int $syncLevel + * @param int $limit + * + * @return array|null + */ + public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null); +} diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/Card.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/Card.php new file mode 100644 index 0000000..c9cd2bb --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/Card.php @@ -0,0 +1,202 @@ +carddavBackend = $carddavBackend; + $this->addressBookInfo = $addressBookInfo; + $this->cardData = $cardData; + } + + /** + * Returns the uri for this object. + * + * @return string + */ + public function getName() + { + return $this->cardData['uri']; + } + + /** + * Returns the VCard-formatted object. + * + * @return string + */ + public function get() + { + // Pre-populating 'carddata' is optional. If we don't yet have it + // already, we fetch it from the backend. + if (!isset($this->cardData['carddata'])) { + $this->cardData = $this->carddavBackend->getCard($this->addressBookInfo['id'], $this->cardData['uri']); + } + + return $this->cardData['carddata']; + } + + /** + * Updates the VCard-formatted object. + * + * @param string $cardData + * + * @return string|null + */ + public function put($cardData) + { + if (is_resource($cardData)) { + $cardData = stream_get_contents($cardData); + } + + // Converting to UTF-8, if needed + $cardData = DAV\StringUtil::ensureUTF8($cardData); + + $etag = $this->carddavBackend->updateCard($this->addressBookInfo['id'], $this->cardData['uri'], $cardData); + $this->cardData['carddata'] = $cardData; + $this->cardData['etag'] = $etag; + + return $etag; + } + + /** + * Deletes the card. + */ + public function delete() + { + $this->carddavBackend->deleteCard($this->addressBookInfo['id'], $this->cardData['uri']); + } + + /** + * Returns the mime content-type. + * + * @return string + */ + public function getContentType() + { + return 'text/vcard; charset=utf-8'; + } + + /** + * Returns an ETag for this object. + * + * @return string + */ + public function getETag() + { + if (isset($this->cardData['etag'])) { + return $this->cardData['etag']; + } else { + $data = $this->get(); + if (is_string($data)) { + return '"'.md5($data).'"'; + } else { + // We refuse to calculate the md5 if it's a stream. + return null; + } + } + } + + /** + * Returns the last modification date as a unix timestamp. + * + * @return int + */ + public function getLastModified() + { + return isset($this->cardData['lastmodified']) ? $this->cardData['lastmodified'] : null; + } + + /** + * Returns the size of this object in bytes. + * + * @return int + */ + public function getSize() + { + if (array_key_exists('size', $this->cardData)) { + return $this->cardData['size']; + } else { + return strlen($this->get()); + } + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->addressBookInfo['principaluri']; + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + // An alternative acl may be specified through the cardData array. + if (isset($this->cardData['acl'])) { + return $this->cardData['acl']; + } + + return [ + [ + 'privilege' => '{DAV:}all', + 'principal' => $this->addressBookInfo['principaluri'], + 'protected' => true, + ], + ]; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/IAddressBook.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/IAddressBook.php new file mode 100644 index 0000000..3f489f4 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/IAddressBook.php @@ -0,0 +1,20 @@ +on('propFind', [$this, 'propFindEarly']); + $server->on('propFind', [$this, 'propFindLate'], 150); + $server->on('report', [$this, 'report']); + $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']); + $server->on('beforeWriteContent', [$this, 'beforeWriteContent']); + $server->on('beforeCreateFile', [$this, 'beforeCreateFile']); + $server->on('afterMethod:GET', [$this, 'httpAfterGet']); + + $server->xml->namespaceMap[self::NS_CARDDAV] = 'card'; + + $server->xml->elementMap['{'.self::NS_CARDDAV.'}addressbook-query'] = 'Sabre\\CardDAV\\Xml\\Request\\AddressBookQueryReport'; + $server->xml->elementMap['{'.self::NS_CARDDAV.'}addressbook-multiget'] = 'Sabre\\CardDAV\\Xml\\Request\\AddressBookMultiGetReport'; + + /* Mapping Interfaces to {DAV:}resourcetype values */ + $server->resourceTypeMapping['Sabre\\CardDAV\\IAddressBook'] = '{'.self::NS_CARDDAV.'}addressbook'; + $server->resourceTypeMapping['Sabre\\CardDAV\\IDirectory'] = '{'.self::NS_CARDDAV.'}directory'; + + /* Adding properties that may never be changed */ + $server->protectedProperties[] = '{'.self::NS_CARDDAV.'}supported-address-data'; + $server->protectedProperties[] = '{'.self::NS_CARDDAV.'}max-resource-size'; + $server->protectedProperties[] = '{'.self::NS_CARDDAV.'}addressbook-home-set'; + $server->protectedProperties[] = '{'.self::NS_CARDDAV.'}supported-collation-set'; + + $server->xml->elementMap['{http://calendarserver.org/ns/}me-card'] = 'Sabre\\DAV\\Xml\\Property\\Href'; + + $this->server = $server; + } + + /** + * Returns a list of supported features. + * + * This is used in the DAV: header in the OPTIONS and PROPFIND requests. + * + * @return array + */ + public function getFeatures() + { + return ['addressbook']; + } + + /** + * Returns a list of reports this plugin supports. + * + * This will be used in the {DAV:}supported-report-set property. + * Note that you still need to subscribe to the 'report' event to actually + * implement them + * + * @param string $uri + * + * @return array + */ + public function getSupportedReportSet($uri) + { + $node = $this->server->tree->getNodeForPath($uri); + if ($node instanceof IAddressBook || $node instanceof ICard) { + return [ + '{'.self::NS_CARDDAV.'}addressbook-multiget', + '{'.self::NS_CARDDAV.'}addressbook-query', + ]; + } + + return []; + } + + /** + * Adds all CardDAV-specific properties. + */ + public function propFindEarly(DAV\PropFind $propFind, DAV\INode $node) + { + $ns = '{'.self::NS_CARDDAV.'}'; + + if ($node instanceof IAddressBook) { + $propFind->handle($ns.'max-resource-size', $this->maxResourceSize); + $propFind->handle($ns.'supported-address-data', function () { + return new Xml\Property\SupportedAddressData(); + }); + $propFind->handle($ns.'supported-collation-set', function () { + return new Xml\Property\SupportedCollationSet(); + }); + } + if ($node instanceof DAVACL\IPrincipal) { + $path = $propFind->getPath(); + + $propFind->handle('{'.self::NS_CARDDAV.'}addressbook-home-set', function () use ($path) { + return new LocalHref($this->getAddressBookHomeForPrincipal($path).'/'); + }); + + if ($this->directories) { + $propFind->handle('{'.self::NS_CARDDAV.'}directory-gateway', function () { + return new LocalHref($this->directories); + }); + } + } + + if ($node instanceof ICard) { + // The address-data property is not supposed to be a 'real' + // property, but in large chunks of the spec it does act as such. + // Therefore we simply expose it as a property. + $propFind->handle('{'.self::NS_CARDDAV.'}address-data', function () use ($node) { + $val = $node->get(); + if (is_resource($val)) { + $val = stream_get_contents($val); + } + + return $val; + }); + } + } + + /** + * This functions handles REPORT requests specific to CardDAV. + * + * @param string $reportName + * @param \DOMNode $dom + * @param mixed $path + * + * @return bool + */ + public function report($reportName, $dom, $path) + { + switch ($reportName) { + case '{'.self::NS_CARDDAV.'}addressbook-multiget': + $this->server->transactionType = 'report-addressbook-multiget'; + $this->addressbookMultiGetReport($dom); + + return false; + case '{'.self::NS_CARDDAV.'}addressbook-query': + $this->server->transactionType = 'report-addressbook-query'; + $this->addressBookQueryReport($dom); + + return false; + default: + return; + } + } + + /** + * Returns the addressbook home for a given principal. + * + * @param string $principal + * + * @return string + */ + protected function getAddressbookHomeForPrincipal($principal) + { + list(, $principalId) = Uri\split($principal); + + return self::ADDRESSBOOK_ROOT.'/'.$principalId; + } + + /** + * This function handles the addressbook-multiget REPORT. + * + * This report is used by the client to fetch the content of a series + * of urls. Effectively avoiding a lot of redundant requests. + * + * @param Xml\Request\AddressBookMultiGetReport $report + */ + public function addressbookMultiGetReport($report) + { + $contentType = $report->contentType; + $version = $report->version; + if ($version) { + $contentType .= '; version='.$version; + } + + $vcardType = $this->negotiateVCard( + $contentType + ); + + $propertyList = []; + $paths = array_map( + [$this->server, 'calculateUri'], + $report->hrefs + ); + foreach ($this->server->getPropertiesForMultiplePaths($paths, $report->properties) as $props) { + if (isset($props['200']['{'.self::NS_CARDDAV.'}address-data'])) { + $props['200']['{'.self::NS_CARDDAV.'}address-data'] = $this->convertVCard( + $props[200]['{'.self::NS_CARDDAV.'}address-data'], + $vcardType + ); + } + $propertyList[] = $props; + } + + $prefer = $this->server->getHTTPPrefer(); + + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer'); + $this->server->httpResponse->setBody($this->server->generateMultiStatus($propertyList, 'minimal' === $prefer['return'])); + } + + /** + * This method is triggered before a file gets updated with new content. + * + * This plugin uses this method to ensure that Card nodes receive valid + * vcard data. + * + * @param string $path + * @param resource $data + * @param bool $modified should be set to true, if this event handler + * changed &$data + */ + public function beforeWriteContent($path, DAV\IFile $node, &$data, &$modified) + { + if (!$node instanceof ICard) { + return; + } + + $this->validateVCard($data, $modified); + } + + /** + * This method is triggered before a new file is created. + * + * This plugin uses this method to ensure that Card nodes receive valid + * vcard data. + * + * @param string $path + * @param resource $data + * @param bool $modified should be set to true, if this event handler + * changed &$data + */ + public function beforeCreateFile($path, &$data, DAV\ICollection $parentNode, &$modified) + { + if (!$parentNode instanceof IAddressBook) { + return; + } + + $this->validateVCard($data, $modified); + } + + /** + * Checks if the submitted iCalendar data is in fact, valid. + * + * An exception is thrown if it's not. + * + * @param resource|string $data + * @param bool $modified should be set to true, if this event handler + * changed &$data + */ + protected function validateVCard(&$data, &$modified) + { + // If it's a stream, we convert it to a string first. + if (is_resource($data)) { + $data = stream_get_contents($data); + } + + $before = $data; + + try { + // If the data starts with a [, we can reasonably assume we're dealing + // with a jCal object. + if ('[' === substr($data, 0, 1)) { + $vobj = VObject\Reader::readJson($data); + + // Converting $data back to iCalendar, as that's what we + // technically support everywhere. + $data = $vobj->serialize(); + $modified = true; + } else { + $vobj = VObject\Reader::read($data); + } + } catch (VObject\ParseException $e) { + throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid vCard or jCard data. Parse error: '.$e->getMessage()); + } + + if ('VCARD' !== $vobj->name) { + throw new DAV\Exception\UnsupportedMediaType('This collection can only support vcard objects.'); + } + + $options = VObject\Node::PROFILE_CARDDAV; + $prefer = $this->server->getHTTPPrefer(); + + if ('strict' !== $prefer['handling']) { + $options |= VObject\Node::REPAIR; + } + + $messages = $vobj->validate($options); + + $highestLevel = 0; + $warningMessage = null; + + // $messages contains a list of problems with the vcard, along with + // their severity. + foreach ($messages as $message) { + if ($message['level'] > $highestLevel) { + // Recording the highest reported error level. + $highestLevel = $message['level']; + $warningMessage = $message['message']; + } + + switch ($message['level']) { + case 1: + // Level 1 means that there was a problem, but it was repaired. + $modified = true; + break; + case 2: + // Level 2 means a warning, but not critical + break; + case 3: + // Level 3 means a critical error + throw new DAV\Exception\UnsupportedMediaType('Validation error in vCard: '.$message['message']); + } + } + if ($warningMessage) { + $this->server->httpResponse->setHeader( + 'X-Sabre-Ew-Gross', + 'vCard validation warning: '.$warningMessage + ); + + // Re-serializing object. + $data = $vobj->serialize(); + if (!$modified && 0 !== strcmp($data, $before)) { + // This ensures that the system does not send an ETag back. + $modified = true; + } + } + + // Destroy circular references to PHP will GC the object. + $vobj->destroy(); + } + + /** + * This function handles the addressbook-query REPORT. + * + * This report is used by the client to filter an addressbook based on a + * complex query. + * + * @param Xml\Request\AddressBookQueryReport $report + */ + protected function addressbookQueryReport($report) + { + $depth = $this->server->getHTTPDepth(0); + + if (0 == $depth) { + $candidateNodes = [ + $this->server->tree->getNodeForPath($this->server->getRequestUri()), + ]; + if (!$candidateNodes[0] instanceof ICard) { + throw new ReportNotSupported('The addressbook-query report is not supported on this url with Depth: 0'); + } + } else { + $candidateNodes = $this->server->tree->getChildren($this->server->getRequestUri()); + } + + $contentType = $report->contentType; + if ($report->version) { + $contentType .= '; version='.$report->version; + } + + $vcardType = $this->negotiateVCard( + $contentType + ); + + $validNodes = []; + foreach ($candidateNodes as $node) { + if (!$node instanceof ICard) { + continue; + } + + $blob = $node->get(); + if (is_resource($blob)) { + $blob = stream_get_contents($blob); + } + + if (!$this->validateFilters($blob, $report->filters, $report->test)) { + continue; + } + + $validNodes[] = $node; + + if ($report->limit && $report->limit <= count($validNodes)) { + // We hit the maximum number of items, we can stop now. + break; + } + } + + $result = []; + foreach ($validNodes as $validNode) { + if (0 == $depth) { + $href = $this->server->getRequestUri(); + } else { + $href = $this->server->getRequestUri().'/'.$validNode->getName(); + } + + list($props) = $this->server->getPropertiesForPath($href, $report->properties, 0); + + if (isset($props[200]['{'.self::NS_CARDDAV.'}address-data'])) { + $props[200]['{'.self::NS_CARDDAV.'}address-data'] = $this->convertVCard( + $props[200]['{'.self::NS_CARDDAV.'}address-data'], + $vcardType, + $report->addressDataProperties + ); + } + $result[] = $props; + } + + $prefer = $this->server->getHTTPPrefer(); + + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer'); + $this->server->httpResponse->setBody($this->server->generateMultiStatus($result, 'minimal' === $prefer['return'])); + } + + /** + * Validates if a vcard makes it throught a list of filters. + * + * @param string $vcardData + * @param string $test anyof or allof (which means OR or AND) + * + * @return bool + */ + public function validateFilters($vcardData, array $filters, $test) + { + if (!$filters) { + return true; + } + $vcard = VObject\Reader::read($vcardData); + + foreach ($filters as $filter) { + $isDefined = isset($vcard->{$filter['name']}); + if ($filter['is-not-defined']) { + if ($isDefined) { + $success = false; + } else { + $success = true; + } + } elseif ((!$filter['param-filters'] && !$filter['text-matches']) || !$isDefined) { + // We only need to check for existence + $success = $isDefined; + } else { + $vProperties = $vcard->select($filter['name']); + + $results = []; + if ($filter['param-filters']) { + $results[] = $this->validateParamFilters($vProperties, $filter['param-filters'], $filter['test']); + } + if ($filter['text-matches']) { + $texts = []; + foreach ($vProperties as $vProperty) { + $texts[] = $vProperty->getValue(); + } + + $results[] = $this->validateTextMatches($texts, $filter['text-matches'], $filter['test']); + } + + if (1 === count($results)) { + $success = $results[0]; + } else { + if ('anyof' === $filter['test']) { + $success = $results[0] || $results[1]; + } else { + $success = $results[0] && $results[1]; + } + } + } // else + + // There are two conditions where we can already determine whether + // or not this filter succeeds. + if ('anyof' === $test && $success) { + // Destroy circular references to PHP will GC the object. + $vcard->destroy(); + + return true; + } + if ('allof' === $test && !$success) { + // Destroy circular references to PHP will GC the object. + $vcard->destroy(); + + return false; + } + } // foreach + + // Destroy circular references to PHP will GC the object. + $vcard->destroy(); + + // If we got all the way here, it means we haven't been able to + // determine early if the test failed or not. + // + // This implies for 'anyof' that the test failed, and for 'allof' that + // we succeeded. Sounds weird, but makes sense. + return 'allof' === $test; + } + + /** + * Validates if a param-filter can be applied to a specific property. + * + * @todo currently we're only validating the first parameter of the passed + * property. Any subsequence parameters with the same name are + * ignored. + * + * @param string $test + * + * @return bool + */ + protected function validateParamFilters(array $vProperties, array $filters, $test) + { + foreach ($filters as $filter) { + $isDefined = false; + foreach ($vProperties as $vProperty) { + $isDefined = isset($vProperty[$filter['name']]); + if ($isDefined) { + break; + } + } + + if ($filter['is-not-defined']) { + if ($isDefined) { + $success = false; + } else { + $success = true; + } + + // If there's no text-match, we can just check for existence + } elseif (!$filter['text-match'] || !$isDefined) { + $success = $isDefined; + } else { + $success = false; + foreach ($vProperties as $vProperty) { + // If we got all the way here, we'll need to validate the + // text-match filter. + if (isset($vProperty[$filter['name']])) { + $success = DAV\StringUtil::textMatch( + $vProperty[$filter['name']]->getValue(), + $filter['text-match']['value'], + $filter['text-match']['collation'], + $filter['text-match']['match-type'] + ); + if ($filter['text-match']['negate-condition']) { + $success = !$success; + } + } + if ($success) { + break; + } + } + } // else + + // There are two conditions where we can already determine whether + // or not this filter succeeds. + if ('anyof' === $test && $success) { + return true; + } + if ('allof' === $test && !$success) { + return false; + } + } + + // If we got all the way here, it means we haven't been able to + // determine early if the test failed or not. + // + // This implies for 'anyof' that the test failed, and for 'allof' that + // we succeeded. Sounds weird, but makes sense. + return 'allof' === $test; + } + + /** + * Validates if a text-filter can be applied to a specific property. + * + * @param string $test + * + * @return bool + */ + protected function validateTextMatches(array $texts, array $filters, $test) + { + foreach ($filters as $filter) { + $success = false; + foreach ($texts as $haystack) { + $success = DAV\StringUtil::textMatch($haystack, $filter['value'], $filter['collation'], $filter['match-type']); + if ($filter['negate-condition']) { + $success = !$success; + } + + // Breaking on the first match + if ($success) { + break; + } + } + + if ($success && 'anyof' === $test) { + return true; + } + + if (!$success && 'allof' == $test) { + return false; + } + } + + // If we got all the way here, it means we haven't been able to + // determine early if the test failed or not. + // + // This implies for 'anyof' that the test failed, and for 'allof' that + // we succeeded. Sounds weird, but makes sense. + return 'allof' === $test; + } + + /** + * This event is triggered when fetching properties. + * + * This event is scheduled late in the process, after most work for + * propfind has been done. + */ + public function propFindLate(DAV\PropFind $propFind, DAV\INode $node) + { + // If the request was made using the SOGO connector, we must rewrite + // the content-type property. By default SabreDAV will send back + // text/x-vcard; charset=utf-8, but for SOGO we must strip that last + // part. + if (false === strpos((string) $this->server->httpRequest->getHeader('User-Agent'), 'Thunderbird')) { + return; + } + $contentType = $propFind->get('{DAV:}getcontenttype'); + if (null !== $contentType) { + list($part) = explode(';', $contentType); + if ('text/x-vcard' === $part || 'text/vcard' === $part) { + $propFind->set('{DAV:}getcontenttype', 'text/x-vcard'); + } + } + } + + /** + * This method is used to generate HTML output for the + * Sabre\DAV\Browser\Plugin. This allows us to generate an interface users + * can use to create new addressbooks. + * + * @param string $output + * + * @return bool + */ + public function htmlActionsPanel(DAV\INode $node, &$output) + { + if (!$node instanceof AddressBookHome) { + return; + } + + $output .= '
+

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/composer/vendor/sabre/dav/lib/CardDAV/VCFExportPlugin.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/VCFExportPlugin.php new file mode 100644 index 0000000..431391e --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/VCFExportPlugin.php @@ -0,0 +1,165 @@ +server = $server; + $this->server->on('method:GET', [$this, 'httpGet'], 90); + $server->on('browserButtonActions', function ($path, $node, &$actions) { + if ($node instanceof IAddressBook) { + $actions .= ''; + } + }); + } + + /** + * Intercepts GET requests on addressbook urls ending with ?export. + * + * @return bool + */ + public function httpGet(RequestInterface $request, ResponseInterface $response) + { + $queryParams = $request->getQueryParameters(); + if (!array_key_exists('export', $queryParams)) { + return; + } + + $path = $request->getPath(); + + $node = $this->server->tree->getNodeForPath($path); + + if (!($node instanceof IAddressBook)) { + return; + } + + $this->server->transactionType = 'get-addressbook-export'; + + // Checking ACL, if available. + if ($aclPlugin = $this->server->getPlugin('acl')) { + $aclPlugin->checkPrivileges($path, '{DAV:}read'); + } + + $nodes = $this->server->getPropertiesForPath($path, [ + '{'.Plugin::NS_CARDDAV.'}address-data', + ], 1); + + $format = 'text/directory'; + + $output = null; + $filenameExtension = null; + + switch ($format) { + case 'text/directory': + $output = $this->generateVCF($nodes); + $filenameExtension = '.vcf'; + break; + } + + $filename = preg_replace( + '/[^a-zA-Z0-9-_ ]/um', + '', + $node->getName() + ); + $filename .= '-'.date('Y-m-d').$filenameExtension; + + $response->setHeader('Content-Disposition', 'attachment; filename="'.$filename.'"'); + $response->setHeader('Content-Type', $format); + + $response->setStatus(200); + $response->setBody($output); + + // Returning false to break the event chain + return false; + } + + /** + * Merges all vcard objects, and builds one big vcf export. + * + * @return string + */ + public function generateVCF(array $nodes) + { + $output = ''; + + foreach ($nodes as $node) { + if (!isset($node[200]['{'.Plugin::NS_CARDDAV.'}address-data'])) { + continue; + } + $nodeData = $node[200]['{'.Plugin::NS_CARDDAV.'}address-data']; + + // Parsing this node so VObject can clean up the output. + $vcard = VObject\Reader::read($nodeData); + $output .= $vcard->serialize(); + + // Destroy circular references to PHP will GC the object. + $vcard->destroy(); + } + + return $output; + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using \Sabre\DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'vcf-export'; + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds the ability to export CardDAV addressbooks as a single vCard file.', + 'link' => 'http://sabre.io/dav/vcf-export-plugin/', + ]; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Filter/AddressData.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Filter/AddressData.php new file mode 100644 index 0000000..b60fceb --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Filter/AddressData.php @@ -0,0 +1,66 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $result = [ + 'contentType' => $reader->getAttribute('content-type') ?: 'text/vcard', + 'version' => $reader->getAttribute('version') ?: '3.0', + ]; + + $elems = (array) $reader->parseInnerTree(); + $elems = array_filter($elems, function ($element) { + return '{urn:ietf:params:xml:ns:carddav}prop' === $element['name'] && + isset($element['attributes']['name']); + }); + $result['addressDataProperties'] = array_map(function ($element) { + return $element['attributes']['name']; + }, $elems); + + return $result; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Filter/ParamFilter.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Filter/ParamFilter.php new file mode 100644 index 0000000..0a7ec06 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Filter/ParamFilter.php @@ -0,0 +1,86 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $result = [ + 'name' => null, + 'is-not-defined' => false, + 'text-match' => null, + ]; + + $att = $reader->parseAttributes(); + $result['name'] = $att['name']; + + $elems = $reader->parseInnerTree(); + + if (is_array($elems)) { + foreach ($elems as $elem) { + switch ($elem['name']) { + case '{'.Plugin::NS_CARDDAV.'}is-not-defined': + $result['is-not-defined'] = true; + break; + case '{'.Plugin::NS_CARDDAV.'}text-match': + $matchType = isset($elem['attributes']['match-type']) ? $elem['attributes']['match-type'] : 'contains'; + + if (!in_array($matchType, ['contains', 'equals', 'starts-with', 'ends-with'])) { + throw new BadRequest('Unknown match-type: '.$matchType); + } + $result['text-match'] = [ + 'negate-condition' => isset($elem['attributes']['negate-condition']) && 'yes' === $elem['attributes']['negate-condition'], + 'collation' => isset($elem['attributes']['collation']) ? $elem['attributes']['collation'] : 'i;unicode-casemap', + 'value' => $elem['value'], + 'match-type' => $matchType, + ]; + break; + } + } + } + + return $result; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Filter/PropFilter.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Filter/PropFilter.php new file mode 100644 index 0000000..5dedac8 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Filter/PropFilter.php @@ -0,0 +1,95 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $result = [ + 'name' => null, + 'test' => 'anyof', + 'is-not-defined' => false, + 'param-filters' => [], + 'text-matches' => [], + ]; + + $att = $reader->parseAttributes(); + $result['name'] = $att['name']; + + if (isset($att['test']) && 'allof' === $att['test']) { + $result['test'] = 'allof'; + } + + $elems = $reader->parseInnerTree(); + + if (is_array($elems)) { + foreach ($elems as $elem) { + switch ($elem['name']) { + case '{'.Plugin::NS_CARDDAV.'}param-filter': + $result['param-filters'][] = $elem['value']; + break; + case '{'.Plugin::NS_CARDDAV.'}is-not-defined': + $result['is-not-defined'] = true; + break; + case '{'.Plugin::NS_CARDDAV.'}text-match': + $matchType = isset($elem['attributes']['match-type']) ? $elem['attributes']['match-type'] : 'contains'; + + if (!in_array($matchType, ['contains', 'equals', 'starts-with', 'ends-with'])) { + throw new BadRequest('Unknown match-type: '.$matchType); + } + $result['text-matches'][] = [ + 'negate-condition' => isset($elem['attributes']['negate-condition']) && 'yes' === $elem['attributes']['negate-condition'], + 'collation' => isset($elem['attributes']['collation']) ? $elem['attributes']['collation'] : 'i;unicode-casemap', + 'value' => $elem['value'], + 'match-type' => $matchType, + ]; + break; + } + } + } + + return $result; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Property/SupportedAddressData.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Property/SupportedAddressData.php new file mode 100644 index 0000000..536c5a1 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Property/SupportedAddressData.php @@ -0,0 +1,77 @@ + 'text/vcard', 'version' => '3.0'], + ['contentType' => 'text/vcard', 'version' => '4.0'], + ['contentType' => 'application/vcard+json', 'version' => '4.0'], + ]; + } + + $this->supportedData = $supportedData; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + foreach ($this->supportedData as $supported) { + $writer->startElement('{'.Plugin::NS_CARDDAV.'}address-data-type'); + $writer->writeAttributes([ + 'content-type' => $supported['contentType'], + 'version' => $supported['version'], + ]); + $writer->endElement(); // address-data-type + } + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Property/SupportedCollationSet.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Property/SupportedCollationSet.php new file mode 100644 index 0000000..b19eddd --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Property/SupportedCollationSet.php @@ -0,0 +1,44 @@ +writeElement('{urn:ietf:params:xml:ns:carddav}supported-collation', $coll); + } + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Request/AddressBookMultiGetReport.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Request/AddressBookMultiGetReport.php new file mode 100644 index 0000000..491f969 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Request/AddressBookMultiGetReport.php @@ -0,0 +1,116 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $elems = $reader->parseInnerTree([ + '{urn:ietf:params:xml:ns:carddav}address-data' => 'Sabre\\CardDAV\\Xml\\Filter\\AddressData', + '{DAV:}prop' => 'Sabre\\Xml\\Element\\KeyValue', + ]); + + $newProps = [ + 'hrefs' => [], + 'properties' => [], + ]; + + foreach ($elems as $elem) { + switch ($elem['name']) { + case '{DAV:}prop': + $newProps['properties'] = array_keys($elem['value']); + if (isset($elem['value']['{'.Plugin::NS_CARDDAV.'}address-data'])) { + $newProps += $elem['value']['{'.Plugin::NS_CARDDAV.'}address-data']; + } + break; + case '{DAV:}href': + $newProps['hrefs'][] = Uri\resolve($reader->contextUri, $elem['value']); + break; + } + } + + $obj = new self(); + foreach ($newProps as $key => $value) { + $obj->$key = $value; + } + + return $obj; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Request/AddressBookQueryReport.php b/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Request/AddressBookQueryReport.php new file mode 100644 index 0000000..02402f6 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/CardDAV/Xml/Request/AddressBookQueryReport.php @@ -0,0 +1,193 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $elems = (array) $reader->parseInnerTree([ + '{urn:ietf:params:xml:ns:carddav}prop-filter' => 'Sabre\\CardDAV\\Xml\\Filter\\PropFilter', + '{urn:ietf:params:xml:ns:carddav}param-filter' => 'Sabre\\CardDAV\\Xml\\Filter\\ParamFilter', + '{urn:ietf:params:xml:ns:carddav}address-data' => 'Sabre\\CardDAV\\Xml\\Filter\\AddressData', + '{DAV:}prop' => 'Sabre\\Xml\\Element\\KeyValue', + ]); + + $newProps = [ + 'filters' => null, + 'properties' => [], + 'test' => 'anyof', + 'limit' => null, + ]; + + if (!is_array($elems)) { + $elems = []; + } + + foreach ($elems as $elem) { + switch ($elem['name']) { + case '{DAV:}prop': + $newProps['properties'] = array_keys($elem['value']); + if (isset($elem['value']['{'.Plugin::NS_CARDDAV.'}address-data'])) { + $newProps += $elem['value']['{'.Plugin::NS_CARDDAV.'}address-data']; + } + break; + case '{'.Plugin::NS_CARDDAV.'}filter': + if (!is_null($newProps['filters'])) { + throw new BadRequest('You can only include 1 {'.Plugin::NS_CARDDAV.'}filter element'); + } + if (isset($elem['attributes']['test'])) { + $newProps['test'] = $elem['attributes']['test']; + if ('allof' !== $newProps['test'] && 'anyof' !== $newProps['test']) { + throw new BadRequest('The "test" attribute must be one of "allof" or "anyof"'); + } + } + + $newProps['filters'] = []; + foreach ((array) $elem['value'] as $subElem) { + if ($subElem['name'] === '{'.Plugin::NS_CARDDAV.'}prop-filter') { + $newProps['filters'][] = $subElem['value']; + } + } + break; + case '{'.Plugin::NS_CARDDAV.'}limit': + foreach ($elem['value'] as $child) { + if ($child['name'] === '{'.Plugin::NS_CARDDAV.'}nresults') { + $newProps['limit'] = (int) $child['value']; + } + } + break; + } + } + + if (is_null($newProps['filters'])) { + /* + * We are supposed to throw this error, but KDE sometimes does not + * include the filter element, and we need to treat it as if no + * filters are supplied + */ + //throw new BadRequest('The {' . Plugin::NS_CARDDAV . '}filter element is required for this request'); + $newProps['filters'] = []; + } + + $obj = new self(); + foreach ($newProps as $key => $value) { + $obj->$key = $value; + } + + return $obj; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractBasic.php b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractBasic.php new file mode 100644 index 0000000..3132333 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractBasic.php @@ -0,0 +1,136 @@ +realm = $realm; + } + + /** + * When this method is called, the backend must check if authentication was + * successful. + * + * The returned value must be one of the following + * + * [true, "principals/username"] + * [false, "reason for failure"] + * + * If authentication was successful, it's expected that the authentication + * backend returns a so-called principal url. + * + * Examples of a principal url: + * + * principals/admin + * principals/user1 + * principals/users/joe + * principals/uid/123457 + * + * If you don't use WebDAV ACL (RFC3744) we recommend that you simply + * return a string such as: + * + * principals/users/[username] + * + * @return array + */ + public function check(RequestInterface $request, ResponseInterface $response) + { + $auth = new HTTP\Auth\Basic( + $this->realm, + $request, + $response + ); + + $userpass = $auth->getCredentials(); + if (!$userpass) { + return [false, "No 'Authorization: Basic' header found. Either the client didn't send one, or the server is misconfigured"]; + } + if (!$this->validateUserPass($userpass[0], $userpass[1])) { + return [false, 'Username or password was incorrect']; + } + + return [true, $this->principalPrefix.$userpass[0]]; + } + + /** + * This method is called when a user could not be authenticated, and + * authentication was required for the current request. + * + * This gives you the opportunity to set authentication headers. The 401 + * status code will already be set. + * + * In this case of Basic Auth, this would for example mean that the + * following header needs to be set: + * + * $response->addHeader('WWW-Authenticate', 'Basic realm=SabreDAV'); + * + * Keep in mind that in the case of multiple authentication backends, other + * WWW-Authenticate headers may already have been set, and you'll want to + * append your own WWW-Authenticate header instead of overwriting the + * existing one. + */ + public function challenge(RequestInterface $request, ResponseInterface $response) + { + $auth = new HTTP\Auth\Basic( + $this->realm, + $request, + $response + ); + $auth->requireLogin(); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractBearer.php b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractBearer.php new file mode 100644 index 0000000..b681747 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractBearer.php @@ -0,0 +1,130 @@ +realm = $realm; + } + + /** + * When this method is called, the backend must check if authentication was + * successful. + * + * The returned value must be one of the following + * + * [true, "principals/username"] + * [false, "reason for failure"] + * + * If authentication was successful, it's expected that the authentication + * backend returns a so-called principal url. + * + * Examples of a principal url: + * + * principals/admin + * principals/user1 + * principals/users/joe + * principals/uid/123457 + * + * If you don't use WebDAV ACL (RFC3744) we recommend that you simply + * return a string such as: + * + * principals/users/[username] + * + * @return array + */ + public function check(RequestInterface $request, ResponseInterface $response) + { + $auth = new HTTP\Auth\Bearer( + $this->realm, + $request, + $response + ); + + $bearerToken = $auth->getToken($request); + if (!$bearerToken) { + return [false, "No 'Authorization: Bearer' header found. Either the client didn't send one, or the server is mis-configured"]; + } + $principalUrl = $this->validateBearerToken($bearerToken); + if (!$principalUrl) { + return [false, 'Bearer token was incorrect']; + } + + return [true, $principalUrl]; + } + + /** + * This method is called when a user could not be authenticated, and + * authentication was required for the current request. + * + * This gives you the opportunity to set authentication headers. The 401 + * status code will already be set. + * + * In this case of Bearer Auth, this would for example mean that the + * following header needs to be set: + * + * $response->addHeader('WWW-Authenticate', 'Bearer realm=SabreDAV'); + * + * Keep in mind that in the case of multiple authentication backends, other + * WWW-Authenticate headers may already have been set, and you'll want to + * append your own WWW-Authenticate header instead of overwriting the + * existing one. + */ + public function challenge(RequestInterface $request, ResponseInterface $response) + { + $auth = new HTTP\Auth\Bearer( + $this->realm, + $request, + $response + ); + $auth->requireLogin(); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractDigest.php b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractDigest.php new file mode 100644 index 0000000..297655d --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractDigest.php @@ -0,0 +1,160 @@ +realm = $realm; + } + + /** + * Returns a users digest hash based on the username and realm. + * + * If the user was not known, null must be returned. + * + * @param string $realm + * @param string $username + * + * @return string|null + */ + abstract public function getDigestHash($realm, $username); + + /** + * When this method is called, the backend must check if authentication was + * successful. + * + * The returned value must be one of the following + * + * [true, "principals/username"] + * [false, "reason for failure"] + * + * If authentication was successful, it's expected that the authentication + * backend returns a so-called principal url. + * + * Examples of a principal url: + * + * principals/admin + * principals/user1 + * principals/users/joe + * principals/uid/123457 + * + * If you don't use WebDAV ACL (RFC3744) we recommend that you simply + * return a string such as: + * + * principals/users/[username] + * + * @return array + */ + public function check(RequestInterface $request, ResponseInterface $response) + { + $digest = new HTTP\Auth\Digest( + $this->realm, + $request, + $response + ); + $digest->init(); + + $username = $digest->getUsername(); + + // No username was given + if (!$username) { + return [false, "No 'Authorization: Digest' header found. Either the client didn't send one, or the server is misconfigured"]; + } + + $hash = $this->getDigestHash($this->realm, $username); + // If this was false, the user account didn't exist + if (false === $hash || is_null($hash)) { + return [false, 'Username or password was incorrect']; + } + if (!is_string($hash)) { + throw new DAV\Exception('The returned value from getDigestHash must be a string or null'); + } + + // If this was false, the password or part of the hash was incorrect. + if (!$digest->validateA1($hash)) { + return [false, 'Username or password was incorrect']; + } + + return [true, $this->principalPrefix.$username]; + } + + /** + * This method is called when a user could not be authenticated, and + * authentication was required for the current request. + * + * This gives you the opportunity to set authentication headers. The 401 + * status code will already be set. + * + * In this case of Basic Auth, this would for example mean that the + * following header needs to be set: + * + * $response->addHeader('WWW-Authenticate', 'Basic realm=SabreDAV'); + * + * Keep in mind that in the case of multiple authentication backends, other + * WWW-Authenticate headers may already have been set, and you'll want to + * append your own WWW-Authenticate header instead of overwriting the + * existing one. + */ + public function challenge(RequestInterface $request, ResponseInterface $response) + { + $auth = new HTTP\Auth\Digest( + $this->realm, + $request, + $response + ); + $auth->init(); + + $oldStatus = $response->getStatus() ?: 200; + $auth->requireLogin(); + + // Preventing the digest utility from modifying the http status code, + // this should be handled by the main plugin. + $response->setStatus($oldStatus); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/Apache.php b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/Apache.php new file mode 100644 index 0000000..ebf67ca --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/Apache.php @@ -0,0 +1,93 @@ +getRawServerValue('REMOTE_USER'); + if (is_null($remoteUser)) { + $remoteUser = $request->getRawServerValue('REDIRECT_REMOTE_USER'); + } + if (is_null($remoteUser)) { + $remoteUser = $request->getRawServerValue('PHP_AUTH_USER'); + } + if (is_null($remoteUser)) { + return [false, 'No REMOTE_USER, REDIRECT_REMOTE_USER, or PHP_AUTH_USER property was found in the PHP $_SERVER super-global. This likely means your server is not configured correctly']; + } + + return [true, $this->principalPrefix.$remoteUser]; + } + + /** + * This method is called when a user could not be authenticated, and + * authentication was required for the current request. + * + * This gives you the opportunity to set authentication headers. The 401 + * status code will already be set. + * + * In this case of Basic Auth, this would for example mean that the + * following header needs to be set: + * + * $response->addHeader('WWW-Authenticate', 'Basic realm=SabreDAV'); + * + * Keep in mind that in the case of multiple authentication backends, other + * WWW-Authenticate headers may already have been set, and you'll want to + * append your own WWW-Authenticate header instead of overwriting the + * existing one. + */ + public function challenge(RequestInterface $request, ResponseInterface $response) + { + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/BackendInterface.php b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/BackendInterface.php new file mode 100644 index 0000000..133eac9 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/BackendInterface.php @@ -0,0 +1,65 @@ +addHeader('WWW-Authenticate', 'Basic realm=SabreDAV'); + * + * Keep in mind that in the case of multiple authentication backends, other + * WWW-Authenticate headers may already have been set, and you'll want to + * append your own WWW-Authenticate header instead of overwriting the + * existing one. + */ + public function challenge(RequestInterface $request, ResponseInterface $response); +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/BasicCallBack.php b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/BasicCallBack.php new file mode 100644 index 0000000..5a8bb98 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/BasicCallBack.php @@ -0,0 +1,56 @@ +callBack = $callBack; + } + + /** + * Validates a username and password. + * + * This method should return true or false depending on if login + * succeeded. + * + * @param string $username + * @param string $password + * + * @return bool + */ + protected function validateUserPass($username, $password) + { + $cb = $this->callBack; + + return $cb($username, $password); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/File.php b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/File.php new file mode 100644 index 0000000..ea2d396 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/File.php @@ -0,0 +1,74 @@ +loadFile($filename); + } + } + + /** + * Loads an htdigest-formatted file. This method can be called multiple times if + * more than 1 file is used. + * + * @param string $filename + */ + public function loadFile($filename) + { + foreach (file($filename, FILE_IGNORE_NEW_LINES) as $line) { + if (2 !== substr_count($line, ':')) { + throw new DAV\Exception('Malformed htdigest file. Every line should contain 2 colons'); + } + list($username, $realm, $A1) = explode(':', $line); + + if (!preg_match('/^[a-zA-Z0-9]{32}$/', $A1)) { + throw new DAV\Exception('Malformed htdigest file. Invalid md5 hash'); + } + $this->users[$realm.':'.$username] = $A1; + } + } + + /** + * Returns a users' information. + * + * @param string $realm + * @param string $username + * + * @return string + */ + public function getDigestHash($realm, $username) + { + return isset($this->users[$realm.':'.$username]) ? $this->users[$realm.':'.$username] : false; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/IMAP.php b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/IMAP.php new file mode 100644 index 0000000..3a18311 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/IMAP.php @@ -0,0 +1,82 @@ +mailbox = $mailbox; + } + + /** + * Connects to an IMAP server and tries to authenticate. + * + * @param string $username + * @param string $password + * + * @return bool + */ + protected function imapOpen($username, $password) + { + $success = false; + + try { + $imap = imap_open($this->mailbox, $username, $password, OP_HALFOPEN | OP_READONLY, 1); + if ($imap) { + $success = true; + } + } catch (\ErrorException $e) { + error_log($e->getMessage()); + } + + $errors = imap_errors(); + if ($errors) { + foreach ($errors as $error) { + error_log($error); + } + } + + if (isset($imap) && $imap) { + imap_close($imap); + } + + return $success; + } + + /** + * Validates a username and password by trying to authenticate against IMAP. + * + * @param string $username + * @param string $password + * + * @return bool + */ + protected function validateUserPass($username, $password) + { + return $this->imapOpen($username, $password); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/PDO.php b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/PDO.php new file mode 100644 index 0000000..9a06912 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/PDO.php @@ -0,0 +1,55 @@ +pdo = $pdo; + } + + /** + * Returns the digest hash for a user. + * + * @param string $realm + * @param string $username + * + * @return string|null + */ + public function getDigestHash($realm, $username) + { + $stmt = $this->pdo->prepare('SELECT digesta1 FROM '.$this->tableName.' WHERE username = ?'); + $stmt->execute([$username]); + + return $stmt->fetchColumn() ?: null; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/PDOBasicAuth.php b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/PDOBasicAuth.php new file mode 100644 index 0000000..d142cbf --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Backend/PDOBasicAuth.php @@ -0,0 +1,114 @@ +pdo = $pdo; + if (isset($options['tableName'])) { + $this->tableName = $options['tableName']; + } else { + $this->tableName = 'users'; + } + if (isset($options['digestColumn'])) { + $this->digestColumn = $options['digestColumn']; + } else { + $this->digestColumn = 'digest'; + } + if (isset($options['uuidColumn'])) { + $this->uuidColumn = $options['uuidColumn']; + } else { + $this->uuidColumn = 'username'; + } + if (isset($options['digestPrefix'])) { + $this->digestPrefix = $options['digestPrefix']; + } + } + + /** + * Validates a username and password. + * + * This method should return true or false depending on if login + * succeeded. + * + * @param string $username + * @param string $password + * + * @return bool + */ + public function validateUserPass($username, $password) + { + $stmt = $this->pdo->prepare('SELECT '.$this->digestColumn.' FROM '.$this->tableName.' WHERE '.$this->uuidColumn.' = ?'); + $stmt->execute([$username]); + $result = $stmt->fetchAll(); + + if (!count($result)) { + return false; + } else { + $digest = $result[0][$this->digestColumn]; + + if (isset($this->digestPrefix)) { + $digest = substr($digest, strlen($this->digestPrefix)); + } + + if (password_verify($password, $digest)) { + return true; + } + + return false; + } + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Plugin.php b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Plugin.php new file mode 100644 index 0000000..47fbe20 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Auth/Plugin.php @@ -0,0 +1,255 @@ +addBackend($authBackend); + } + } + + /** + * Adds an authentication backend to the plugin. + */ + public function addBackend(Backend\BackendInterface $authBackend) + { + $this->backends[] = $authBackend; + } + + /** + * Initializes the plugin. This function is automatically called by the server. + */ + public function initialize(Server $server) + { + $server->on('beforeMethod:*', [$this, 'beforeMethod'], 10); + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'auth'; + } + + /** + * Returns the currently logged-in principal. + * + * This will return a string such as: + * + * principals/username + * principals/users/username + * + * This method will return null if nobody is logged in. + * + * @return string|null + */ + public function getCurrentPrincipal() + { + return $this->currentPrincipal; + } + + /** + * This method is called before any HTTP method and forces users to be authenticated. + */ + public function beforeMethod(RequestInterface $request, ResponseInterface $response) + { + if ($this->currentPrincipal) { + // We already have authentication information. This means that the + // event has already fired earlier, and is now likely fired for a + // sub-request. + // + // We don't want to authenticate users twice, so we simply don't do + // anything here. See Issue #700 for additional reasoning. + // + // This is not a perfect solution, but will be fixed once the + // "currently authenticated principal" is information that's not + // not associated with the plugin, but rather per-request. + // + // See issue #580 for more information about that. + return; + } + + $authResult = $this->check($request, $response); + + if ($authResult[0]) { + // Auth was successful + $this->currentPrincipal = $authResult[1]; + $this->loginFailedReasons = null; + + return; + } + + // If we got here, it means that no authentication backend was + // successful in authenticating the user. + $this->currentPrincipal = null; + $this->loginFailedReasons = $authResult[1]; + + if ($this->autoRequireLogin) { + $this->challenge($request, $response); + throw new NotAuthenticated(implode(', ', $authResult[1])); + } + } + + /** + * Checks authentication credentials, and logs the user in if possible. + * + * This method returns an array. The first item in the array is a boolean + * indicating if login was successful. + * + * If login was successful, the second item in the array will contain the + * current principal url/path of the logged in user. + * + * If login was not successful, the second item in the array will contain a + * an array with strings. The strings are a list of reasons why login was + * unsuccessful. For every auth backend there will be one reason, so usually + * there's just one. + * + * @return array + */ + public function check(RequestInterface $request, ResponseInterface $response) + { + if (!$this->backends) { + throw new \Sabre\DAV\Exception('No authentication backends were configured on this server.'); + } + $reasons = []; + foreach ($this->backends as $backend) { + $result = $backend->check( + $request, + $response + ); + + if (!is_array($result) || 2 !== count($result) || !is_bool($result[0]) || !is_string($result[1])) { + throw new \Sabre\DAV\Exception('The authentication backend did not return a correct value from the check() method.'); + } + + if ($result[0]) { + $this->currentPrincipal = $result[1]; + // Exit early + return [true, $result[1]]; + } + $reasons[] = $result[1]; + } + + return [false, $reasons]; + } + + /** + * This method sends authentication challenges to the user. + * + * This method will for example cause a HTTP Basic backend to set a + * WWW-Authorization header, indicating to the client that it should + * authenticate. + */ + public function challenge(RequestInterface $request, ResponseInterface $response) + { + foreach ($this->backends as $backend) { + $backend->challenge($request, $response); + } + } + + /** + * List of reasons why login failed for the last login operation. + * + * @var string[]|null + */ + protected $loginFailedReasons; + + /** + * Returns a list of reasons why login was unsuccessful. + * + * This method will return the login failed reasons for the last login + * operation. One for each auth backend. + * + * This method returns null if the last authentication attempt was + * successful, or if there was no authentication attempt yet. + * + * @return string[]|null + */ + public function getLoginFailedReasons() + { + return $this->loginFailedReasons; + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Generic authentication plugin', + 'link' => 'http://sabre.io/dav/authentication/', + ]; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Browser/GuessContentType.php b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/GuessContentType.php new file mode 100644 index 0000000..5cda0b8 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/GuessContentType.php @@ -0,0 +1,93 @@ + 'image/jpeg', + 'gif' => 'image/gif', + 'png' => 'image/png', + + // groupware + 'ics' => 'text/calendar', + 'vcf' => 'text/vcard', + + // text + 'txt' => 'text/plain', + ]; + + /** + * Initializes the plugin. + */ + public function initialize(DAV\Server $server) + { + // Using a relatively low priority (200) to allow other extensions + // to set the content-type first. + $server->on('propFind', [$this, 'propFind'], 200); + } + + /** + * Our PROPFIND handler. + * + * Here we set a contenttype, if the node didn't already have one. + */ + public function propFind(PropFind $propFind, INode $node) + { + $propFind->handle('{DAV:}getcontenttype', function () use ($propFind) { + list(, $fileName) = Uri\split($propFind->getPath()); + + return $this->getContentType($fileName); + }); + } + + /** + * Simple method to return the contenttype. + * + * @param string $fileName + * + * @return string + */ + protected function getContentType($fileName) + { + if (null !== $fileName) { + // Just grabbing the extension + $extension = strtolower(substr($fileName, strrpos($fileName, '.') + 1)); + if (isset($this->extensionMap[$extension])) { + return $this->extensionMap[$extension]; + } + } + + return 'application/octet-stream'; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Browser/HtmlOutput.php b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/HtmlOutput.php new file mode 100644 index 0000000..be5a284 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/HtmlOutput.php @@ -0,0 +1,34 @@ +baseUri = $baseUri; + $this->namespaceMap = $namespaceMap; + } + + /** + * Generates a 'full' url based on a relative one. + * + * For relative urls, the base of the application is taken as the reference + * url, not the 'current url of the current request'. + * + * Absolute urls are left alone. + * + * @param string $path + * + * @return string + */ + public function fullUrl($path) + { + return Uri\resolve($this->baseUri, $path); + } + + /** + * Escape string for HTML output. + * + * @param scalar $input + * + * @return string + */ + public function h($input) + { + return htmlspecialchars((string) $input, ENT_COMPAT, 'UTF-8'); + } + + /** + * Generates a full -tag. + * + * Url is automatically expanded. If label is not specified, we re-use the + * url. + * + * @param string $url + * @param string $label + * + * @return string + */ + public function link($url, $label = null) + { + $url = $this->h($this->fullUrl($url)); + + return ''.($label ? $this->h($label) : $url).''; + } + + /** + * This method takes an xml element in clark-notation, and turns it into a + * shortened version with a prefix, if it was a known namespace. + * + * @param string $element + * + * @return string + */ + public function xmlName($element) + { + list($ns, $localName) = XmlService::parseClarkNotation($element); + if (isset($this->namespaceMap[$ns])) { + $propName = $this->namespaceMap[$ns].':'.$localName; + } else { + $propName = $element; + } + + return ''.$this->h($propName).''; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Browser/MapGetToPropFind.php b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/MapGetToPropFind.php new file mode 100644 index 0000000..0bbe70c --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/MapGetToPropFind.php @@ -0,0 +1,58 @@ +server = $server; + $this->server->on('method:GET', [$this, 'httpGet'], 90); + } + + /** + * This method intercepts GET requests to non-files, and changes it into an HTTP PROPFIND request. + * + * @return bool + */ + public function httpGet(RequestInterface $request, ResponseInterface $response) + { + $node = $this->server->tree->getNodeForPath($request->getPath()); + if ($node instanceof DAV\IFile) { + return; + } + + $subRequest = clone $request; + $subRequest->setMethod('PROPFIND'); + + $this->server->invokeMethod($subRequest, $response); + + return false; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Browser/Plugin.php b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/Plugin.php new file mode 100644 index 0000000..a8a6f43 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/Plugin.php @@ -0,0 +1,789 @@ +enablePost = $enablePost; + } + + /** + * Initializes the plugin and subscribes to events. + */ + public function initialize(DAV\Server $server) + { + $this->server = $server; + $this->server->on('method:GET', [$this, 'httpGetEarly'], 90); + $this->server->on('method:GET', [$this, 'httpGet'], 200); + $this->server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel'], 200); + if ($this->enablePost) { + $this->server->on('method:POST', [$this, 'httpPOST']); + } + } + + /** + * This method intercepts GET requests that have ?sabreAction=info + * appended to the URL. + */ + public function httpGetEarly(RequestInterface $request, ResponseInterface $response) + { + $params = $request->getQueryParameters(); + if (isset($params['sabreAction']) && 'info' === $params['sabreAction']) { + return $this->httpGet($request, $response); + } + } + + /** + * This method intercepts GET requests to collections and returns the html. + * + * @return bool + */ + public function httpGet(RequestInterface $request, ResponseInterface $response) + { + // We're not using straight-up $_GET, because we want everything to be + // unit testable. + $getVars = $request->getQueryParameters(); + + // CSP headers + $response->setHeader('Content-Security-Policy', "default-src 'none'; img-src 'self'; style-src 'self'; font-src 'self';"); + + $sabreAction = isset($getVars['sabreAction']) ? $getVars['sabreAction'] : null; + + switch ($sabreAction) { + case 'asset': + // Asset handling, such as images + $this->serveAsset(isset($getVars['assetName']) ? $getVars['assetName'] : null); + + return false; + default: + case 'info': + try { + $this->server->tree->getNodeForPath($request->getPath()); + } catch (DAV\Exception\NotFound $e) { + // We're simply stopping when the file isn't found to not interfere + // with other plugins. + return; + } + + $response->setStatus(200); + $response->setHeader('Content-Type', 'text/html; charset=utf-8'); + + $response->setBody( + $this->generateDirectoryIndex($request->getPath()) + ); + + return false; + + case 'plugins': + $response->setStatus(200); + $response->setHeader('Content-Type', 'text/html; charset=utf-8'); + + $response->setBody( + $this->generatePluginListing() + ); + + return false; + } + } + + /** + * Handles POST requests for tree operations. + * + * @return bool + */ + public function httpPOST(RequestInterface $request, ResponseInterface $response) + { + $contentType = $request->getHeader('Content-Type'); + if (!\is_string($contentType)) { + return; + } + list($contentType) = explode(';', $contentType); + if ('application/x-www-form-urlencoded' !== $contentType && + 'multipart/form-data' !== $contentType) { + return; + } + $postVars = $request->getPostData(); + + if (!isset($postVars['sabreAction'])) { + return; + } + + $uri = $request->getPath(); + + if ($this->server->emit('onBrowserPostAction', [$uri, $postVars['sabreAction'], $postVars])) { + switch ($postVars['sabreAction']) { + case 'mkcol': + if (isset($postVars['name']) && trim($postVars['name'])) { + // Using basename() because we won't allow slashes + list(, $folderName) = Uri\split(trim($postVars['name'])); + + if (isset($postVars['resourceType'])) { + $resourceType = explode(',', $postVars['resourceType']); + } else { + $resourceType = ['{DAV:}collection']; + } + + $properties = []; + foreach ($postVars as $varName => $varValue) { + // Any _POST variable in clark notation is treated + // like a property. + if ('{' === $varName[0]) { + // PHP will convert any dots to underscores. + // This leaves us with no way to differentiate + // the two. + // Therefore we replace the string *DOT* with a + // real dot. * is not allowed in uris so we + // should be good. + $varName = str_replace('*DOT*', '.', $varName); + $properties[$varName] = $varValue; + } + } + + $mkCol = new MkCol( + $resourceType, + $properties + ); + $this->server->createCollection($uri.'/'.$folderName, $mkCol); + } + break; + + // @codeCoverageIgnoreStart + case 'put': + if ($_FILES) { + $file = current($_FILES); + } else { + break; + } + + list(, $newName) = Uri\split(trim($file['name'])); + if (isset($postVars['name']) && trim($postVars['name'])) { + $newName = trim($postVars['name']); + } + + // Making sure we only have a 'basename' component + list(, $newName) = Uri\split($newName); + + if (is_uploaded_file($file['tmp_name'])) { + $this->server->createFile($uri.'/'.$newName, fopen($file['tmp_name'], 'r')); + } + break; + // @codeCoverageIgnoreEnd + } + } + $response->setHeader('Location', $request->getUrl()); + $response->setStatus(302); + + return false; + } + + /** + * Escapes a string for html. + * + * @param string $value + * + * @return string + */ + public function escapeHTML($value) + { + return htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); + } + + /** + * Generates the html directory index for a given url. + * + * @param string $path + * + * @return string + */ + public function generateDirectoryIndex($path) + { + $html = $this->generateHeader($path ? $path : '/', $path); + + $node = $this->server->tree->getNodeForPath($path); + if ($node instanceof DAV\ICollection) { + $html .= "

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/composer/vendor/sabre/dav/lib/DAV/Browser/PropFindAll.php b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/PropFindAll.php new file mode 100644 index 0000000..34702bd --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/PropFindAll.php @@ -0,0 +1,128 @@ +handle('{DAV:}displayname', function() { + * return 'hello'; + * }); + * + * Note that handle will only work the first time. If null is returned, the + * value is ignored. + * + * It's also possible to not pass a callback, but immediately pass a value + * + * @param string $propertyName + * @param mixed $valueOrCallBack + */ + public function handle($propertyName, $valueOrCallBack) + { + if (is_callable($valueOrCallBack)) { + $value = $valueOrCallBack(); + } else { + $value = $valueOrCallBack; + } + if (!is_null($value)) { + $this->result[$propertyName] = [200, $value]; + } + } + + /** + * Sets the value of the property. + * + * If status is not supplied, the status will default to 200 for non-null + * properties, and 404 for null properties. + * + * @param string $propertyName + * @param mixed $value + * @param int $status + */ + public function set($propertyName, $value, $status = null) + { + if (is_null($status)) { + $status = is_null($value) ? 404 : 200; + } + $this->result[$propertyName] = [$status, $value]; + } + + /** + * Returns the current value for a property. + * + * @param string $propertyName + * + * @return mixed + */ + public function get($propertyName) + { + return isset($this->result[$propertyName]) ? $this->result[$propertyName][1] : null; + } + + /** + * Returns the current status code for a property name. + * + * If the property does not appear in the list of requested properties, + * null will be returned. + * + * @param string $propertyName + * + * @return int|null + */ + public function getStatus($propertyName) + { + return isset($this->result[$propertyName]) ? $this->result[$propertyName][0] : 404; + } + + /** + * Returns all propertynames that have a 404 status, and thus don't have a + * value yet. + * + * @return array + */ + public function get404Properties() + { + $result = []; + foreach ($this->result as $propertyName => $stuff) { + if (404 === $stuff[0]) { + $result[] = $propertyName; + } + } + // If there's nothing in this list, we're adding one fictional item. + if (!$result) { + $result[] = '{http://sabredav.org/ns}idk'; + } + + return $result; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/favicon.ico b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..2b2c10a22cc7a57c4dc5d7156f184448f2bee92b GIT binary patch literal 4286 zcmc&&O-NKx6uz%14NNBzA}E}JHil3^6a|4&P_&6!RGSD}1rgCAC@`9dTC_@vqNoT8 z0%;dQR0K_{iUM;bf#3*%o1h^C2N7@IH_nmc?Va~t5U6~f`_BE&`Of`&_n~tUev3uN zziw!)bL*XR-2hy!51_yCgT8fb3s`VC=e=KcI5&)PGGQlpSAh?}1mK&Pg8c>z0Y`y$ zAT_6qJ%yV?|0!S$5WO@z3+`QD17OyXL4PyiM}RavtA7Tu7p)pn^p7Ks@m6m7)A}X$ z4Y+@;NrHYq_;V@RoZ|;69MPx!46Ftg*Tc~711C+J`JMuUfYwNBzXPB9sZm3WK9272 z&x|>@f_EO{b3cubqjOyc~J3I$d_lHIpN}q z!{kjX{c{12XF=~Z$w$kazXHB!b53>u!rx}_$e&dD`xNgv+MR&p2yN1xb0>&9t@28Z zV&5u#j_D=P9mI#){2s8@eGGj(?>gooo<%RT14>`VSZ&_l6GlGnan=^bemD56rRN{? zSAqZD$i;oS9SF6#f5I`#^C&hW@13s_lc3LUl(PWmHcop2{vr^kO`kP(*4!m=3Hn3e#Oc!a2;iDn+FbXzcOHEQ zbXZ)u93cj1WA=KS+M>jZ=oYyXq}1?ZdsjsX0A zkJXCvi~cfO@2ffd7r^;>=SsL-3U%l5HRoEZ#0r%`7%&% ziLTXJqU*JeXt3H5`AS#h(dpfl+`Ox|)*~QS%h&VO!d#)!>r3U5_YsDi2fY6Sd&vw% literal 0 HcmV?d00001 diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/ICON-LICENSE b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/ICON-LICENSE new file mode 100644 index 0000000..2199f4a --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/ICON-LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Waybury + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.css b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.css new file mode 100644 index 0000000..e748674 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.css @@ -0,0 +1,510 @@ +@font-face { + font-family: 'Icons'; + src: url('?sabreAction=asset&assetName=openiconic/open-iconic.eot'); + src: url('?sabreAction=asset&assetName=openiconic/open-iconic.eot?#iconic-sm') format('embedded-opentype'), url('?sabreAction=asset&assetName=openiconic/open-iconic.woff') format('woff'), url('?sabreAction=asset&assetName=openiconic/open-iconic.ttf') format('truetype'), url('?sabreAction=asset&assetName=openiconic/open-iconic.otf') format('opentype'), url('?sabreAction=asset&assetName=openiconic/open-iconic.svg#iconic-sm') format('svg'); + font-weight: normal; + font-style: normal; +} + +.oi[data-glyph].oi-text-replace { + font-size: 0; + line-height: 0; +} + +.oi[data-glyph].oi-text-replace:before { + width: 1em; + text-align: center; +} + +.oi[data-glyph]:before { + font-family: 'Icons'; + display: inline-block; + speak: none; + line-height: 1; + vertical-align: baseline; + font-weight: normal; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.oi[data-glyph]:empty:before { + width: 1em; + text-align: center; + box-sizing: content-box; +} + +.oi[data-glyph].oi-align-left:before { + text-align: left; +} + +.oi[data-glyph].oi-align-right:before { + text-align: right; +} + +.oi[data-glyph].oi-align-center:before { + text-align: center; +} + +.oi[data-glyph].oi-flip-horizontal:before { + -webkit-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + transform: scale(-1, 1); +} +.oi[data-glyph].oi-flip-vertical:before { + -webkit-transform: scale(1, -1); + -ms-transform: scale(-1, 1); + transform: scale(1, -1); +} +.oi[data-glyph].oi-flip-horizontal-vertical:before { + -webkit-transform: scale(-1, -1); + -ms-transform: scale(-1, 1); + transform: scale(-1, -1); +} + + +.oi[data-glyph=account-login]:before { content:'\e000'; } + +.oi[data-glyph=account-logout]:before { content:'\e001'; } + +.oi[data-glyph=action-redo]:before { content:'\e002'; } + +.oi[data-glyph=action-undo]:before { content:'\e003'; } + +.oi[data-glyph=align-center]:before { content:'\e004'; } + +.oi[data-glyph=align-left]:before { content:'\e005'; } + +.oi[data-glyph=align-right]:before { content:'\e006'; } + +.oi[data-glyph=aperture]:before { content:'\e007'; } + +.oi[data-glyph=arrow-bottom]:before { content:'\e008'; } + +.oi[data-glyph=arrow-circle-bottom]:before { content:'\e009'; } + +.oi[data-glyph=arrow-circle-left]:before { content:'\e00a'; } + +.oi[data-glyph=arrow-circle-right]:before { content:'\e00b'; } + +.oi[data-glyph=arrow-circle-top]:before { content:'\e00c'; } + +.oi[data-glyph=arrow-left]:before { content:'\e00d'; } + +.oi[data-glyph=arrow-right]:before { content:'\e00e'; } + +.oi[data-glyph=arrow-thick-bottom]:before { content:'\e00f'; } + +.oi[data-glyph=arrow-thick-left]:before { content:'\e010'; } + +.oi[data-glyph=arrow-thick-right]:before { content:'\e011'; } + +.oi[data-glyph=arrow-thick-top]:before { content:'\e012'; } + +.oi[data-glyph=arrow-top]:before { content:'\e013'; } + +.oi[data-glyph=audio-spectrum]:before { content:'\e014'; } + +.oi[data-glyph=audio]:before { content:'\e015'; } + +.oi[data-glyph=badge]:before { content:'\e016'; } + +.oi[data-glyph=ban]:before { content:'\e017'; } + +.oi[data-glyph=bar-chart]:before { content:'\e018'; } + +.oi[data-glyph=basket]:before { content:'\e019'; } + +.oi[data-glyph=battery-empty]:before { content:'\e01a'; } + +.oi[data-glyph=battery-full]:before { content:'\e01b'; } + +.oi[data-glyph=beaker]:before { content:'\e01c'; } + +.oi[data-glyph=bell]:before { content:'\e01d'; } + +.oi[data-glyph=bluetooth]:before { content:'\e01e'; } + +.oi[data-glyph=bold]:before { content:'\e01f'; } + +.oi[data-glyph=bolt]:before { content:'\e020'; } + +.oi[data-glyph=book]:before { content:'\e021'; } + +.oi[data-glyph=bookmark]:before { content:'\e022'; } + +.oi[data-glyph=box]:before { content:'\e023'; } + +.oi[data-glyph=briefcase]:before { content:'\e024'; } + +.oi[data-glyph=british-pound]:before { content:'\e025'; } + +.oi[data-glyph=browser]:before { content:'\e026'; } + +.oi[data-glyph=brush]:before { content:'\e027'; } + +.oi[data-glyph=bug]:before { content:'\e028'; } + +.oi[data-glyph=bullhorn]:before { content:'\e029'; } + +.oi[data-glyph=calculator]:before { content:'\e02a'; } + +.oi[data-glyph=calendar]:before { content:'\e02b'; } + +.oi[data-glyph=camera-slr]:before { content:'\e02c'; } + +.oi[data-glyph=caret-bottom]:before { content:'\e02d'; } + +.oi[data-glyph=caret-left]:before { content:'\e02e'; } + +.oi[data-glyph=caret-right]:before { content:'\e02f'; } + +.oi[data-glyph=caret-top]:before { content:'\e030'; } + +.oi[data-glyph=cart]:before { content:'\e031'; } + +.oi[data-glyph=chat]:before { content:'\e032'; } + +.oi[data-glyph=check]:before { content:'\e033'; } + +.oi[data-glyph=chevron-bottom]:before { content:'\e034'; } + +.oi[data-glyph=chevron-left]:before { content:'\e035'; } + +.oi[data-glyph=chevron-right]:before { content:'\e036'; } + +.oi[data-glyph=chevron-top]:before { content:'\e037'; } + +.oi[data-glyph=circle-check]:before { content:'\e038'; } + +.oi[data-glyph=circle-x]:before { content:'\e039'; } + +.oi[data-glyph=clipboard]:before { content:'\e03a'; } + +.oi[data-glyph=clock]:before { content:'\e03b'; } + +.oi[data-glyph=cloud-download]:before { content:'\e03c'; } + +.oi[data-glyph=cloud-upload]:before { content:'\e03d'; } + +.oi[data-glyph=cloud]:before { content:'\e03e'; } + +.oi[data-glyph=cloudy]:before { content:'\e03f'; } + +.oi[data-glyph=code]:before { content:'\e040'; } + +.oi[data-glyph=cog]:before { content:'\e041'; } + +.oi[data-glyph=collapse-down]:before { content:'\e042'; } + +.oi[data-glyph=collapse-left]:before { content:'\e043'; } + +.oi[data-glyph=collapse-right]:before { content:'\e044'; } + +.oi[data-glyph=collapse-up]:before { content:'\e045'; } + +.oi[data-glyph=command]:before { content:'\e046'; } + +.oi[data-glyph=comment-square]:before { content:'\e047'; } + +.oi[data-glyph=compass]:before { content:'\e048'; } + +.oi[data-glyph=contrast]:before { content:'\e049'; } + +.oi[data-glyph=copywriting]:before { content:'\e04a'; } + +.oi[data-glyph=credit-card]:before { content:'\e04b'; } + +.oi[data-glyph=crop]:before { content:'\e04c'; } + +.oi[data-glyph=dashboard]:before { content:'\e04d'; } + +.oi[data-glyph=data-transfer-download]:before { content:'\e04e'; } + +.oi[data-glyph=data-transfer-upload]:before { content:'\e04f'; } + +.oi[data-glyph=delete]:before { content:'\e050'; } + +.oi[data-glyph=dial]:before { content:'\e051'; } + +.oi[data-glyph=document]:before { content:'\e052'; } + +.oi[data-glyph=dollar]:before { content:'\e053'; } + +.oi[data-glyph=double-quote-sans-left]:before { content:'\e054'; } + +.oi[data-glyph=double-quote-sans-right]:before { content:'\e055'; } + +.oi[data-glyph=double-quote-serif-left]:before { content:'\e056'; } + +.oi[data-glyph=double-quote-serif-right]:before { content:'\e057'; } + +.oi[data-glyph=droplet]:before { content:'\e058'; } + +.oi[data-glyph=eject]:before { content:'\e059'; } + +.oi[data-glyph=elevator]:before { content:'\e05a'; } + +.oi[data-glyph=ellipses]:before { content:'\e05b'; } + +.oi[data-glyph=envelope-closed]:before { content:'\e05c'; } + +.oi[data-glyph=envelope-open]:before { content:'\e05d'; } + +.oi[data-glyph=euro]:before { content:'\e05e'; } + +.oi[data-glyph=excerpt]:before { content:'\e05f'; } + +.oi[data-glyph=expand-down]:before { content:'\e060'; } + +.oi[data-glyph=expand-left]:before { content:'\e061'; } + +.oi[data-glyph=expand-right]:before { content:'\e062'; } + +.oi[data-glyph=expand-up]:before { content:'\e063'; } + +.oi[data-glyph=external-link]:before { content:'\e064'; } + +.oi[data-glyph=eye]:before { content:'\e065'; } + +.oi[data-glyph=eyedropper]:before { content:'\e066'; } + +.oi[data-glyph=file]:before { content:'\e067'; } + +.oi[data-glyph=fire]:before { content:'\e068'; } + +.oi[data-glyph=flag]:before { content:'\e069'; } + +.oi[data-glyph=flash]:before { content:'\e06a'; } + +.oi[data-glyph=folder]:before { content:'\e06b'; } + +.oi[data-glyph=fork]:before { content:'\e06c'; } + +.oi[data-glyph=fullscreen-enter]:before { content:'\e06d'; } + +.oi[data-glyph=fullscreen-exit]:before { content:'\e06e'; } + +.oi[data-glyph=globe]:before { content:'\e06f'; } + +.oi[data-glyph=graph]:before { content:'\e070'; } + +.oi[data-glyph=grid-four-up]:before { content:'\e071'; } + +.oi[data-glyph=grid-three-up]:before { content:'\e072'; } + +.oi[data-glyph=grid-two-up]:before { content:'\e073'; } + +.oi[data-glyph=hard-drive]:before { content:'\e074'; } + +.oi[data-glyph=header]:before { content:'\e075'; } + +.oi[data-glyph=headphones]:before { content:'\e076'; } + +.oi[data-glyph=heart]:before { content:'\e077'; } + +.oi[data-glyph=home]:before { content:'\e078'; } + +.oi[data-glyph=image]:before { content:'\e079'; } + +.oi[data-glyph=inbox]:before { content:'\e07a'; } + +.oi[data-glyph=infinity]:before { content:'\e07b'; } + +.oi[data-glyph=info]:before { content:'\e07c'; } + +.oi[data-glyph=italic]:before { content:'\e07d'; } + +.oi[data-glyph=justify-center]:before { content:'\e07e'; } + +.oi[data-glyph=justify-left]:before { content:'\e07f'; } + +.oi[data-glyph=justify-right]:before { content:'\e080'; } + +.oi[data-glyph=key]:before { content:'\e081'; } + +.oi[data-glyph=laptop]:before { content:'\e082'; } + +.oi[data-glyph=layers]:before { content:'\e083'; } + +.oi[data-glyph=lightbulb]:before { content:'\e084'; } + +.oi[data-glyph=link-broken]:before { content:'\e085'; } + +.oi[data-glyph=link-intact]:before { content:'\e086'; } + +.oi[data-glyph=list-rich]:before { content:'\e087'; } + +.oi[data-glyph=list]:before { content:'\e088'; } + +.oi[data-glyph=location]:before { content:'\e089'; } + +.oi[data-glyph=lock-locked]:before { content:'\e08a'; } + +.oi[data-glyph=lock-unlocked]:before { content:'\e08b'; } + +.oi[data-glyph=loop-circular]:before { content:'\e08c'; } + +.oi[data-glyph=loop-square]:before { content:'\e08d'; } + +.oi[data-glyph=loop]:before { content:'\e08e'; } + +.oi[data-glyph=magnifying-glass]:before { content:'\e08f'; } + +.oi[data-glyph=map-marker]:before { content:'\e090'; } + +.oi[data-glyph=map]:before { content:'\e091'; } + +.oi[data-glyph=media-pause]:before { content:'\e092'; } + +.oi[data-glyph=media-play]:before { content:'\e093'; } + +.oi[data-glyph=media-record]:before { content:'\e094'; } + +.oi[data-glyph=media-skip-backward]:before { content:'\e095'; } + +.oi[data-glyph=media-skip-forward]:before { content:'\e096'; } + +.oi[data-glyph=media-step-backward]:before { content:'\e097'; } + +.oi[data-glyph=media-step-forward]:before { content:'\e098'; } + +.oi[data-glyph=media-stop]:before { content:'\e099'; } + +.oi[data-glyph=medical-cross]:before { content:'\e09a'; } + +.oi[data-glyph=menu]:before { content:'\e09b'; } + +.oi[data-glyph=microphone]:before { content:'\e09c'; } + +.oi[data-glyph=minus]:before { content:'\e09d'; } + +.oi[data-glyph=monitor]:before { content:'\e09e'; } + +.oi[data-glyph=moon]:before { content:'\e09f'; } + +.oi[data-glyph=move]:before { content:'\e0a0'; } + +.oi[data-glyph=musical-note]:before { content:'\e0a1'; } + +.oi[data-glyph=paperclip]:before { content:'\e0a2'; } + +.oi[data-glyph=pencil]:before { content:'\e0a3'; } + +.oi[data-glyph=people]:before { content:'\e0a4'; } + +.oi[data-glyph=person]:before { content:'\e0a5'; } + +.oi[data-glyph=phone]:before { content:'\e0a6'; } + +.oi[data-glyph=pie-chart]:before { content:'\e0a7'; } + +.oi[data-glyph=pin]:before { content:'\e0a8'; } + +.oi[data-glyph=play-circle]:before { content:'\e0a9'; } + +.oi[data-glyph=plus]:before { content:'\e0aa'; } + +.oi[data-glyph=power-standby]:before { content:'\e0ab'; } + +.oi[data-glyph=print]:before { content:'\e0ac'; } + +.oi[data-glyph=project]:before { content:'\e0ad'; } + +.oi[data-glyph=pulse]:before { content:'\e0ae'; } + +.oi[data-glyph=puzzle-piece]:before { content:'\e0af'; } + +.oi[data-glyph=question-mark]:before { content:'\e0b0'; } + +.oi[data-glyph=rain]:before { content:'\e0b1'; } + +.oi[data-glyph=random]:before { content:'\e0b2'; } + +.oi[data-glyph=reload]:before { content:'\e0b3'; } + +.oi[data-glyph=resize-both]:before { content:'\e0b4'; } + +.oi[data-glyph=resize-height]:before { content:'\e0b5'; } + +.oi[data-glyph=resize-width]:before { content:'\e0b6'; } + +.oi[data-glyph=rss-alt]:before { content:'\e0b7'; } + +.oi[data-glyph=rss]:before { content:'\e0b8'; } + +.oi[data-glyph=script]:before { content:'\e0b9'; } + +.oi[data-glyph=share-boxed]:before { content:'\e0ba'; } + +.oi[data-glyph=share]:before { content:'\e0bb'; } + +.oi[data-glyph=shield]:before { content:'\e0bc'; } + +.oi[data-glyph=signal]:before { content:'\e0bd'; } + +.oi[data-glyph=signpost]:before { content:'\e0be'; } + +.oi[data-glyph=sort-ascending]:before { content:'\e0bf'; } + +.oi[data-glyph=sort-descending]:before { content:'\e0c0'; } + +.oi[data-glyph=spreadsheet]:before { content:'\e0c1'; } + +.oi[data-glyph=star]:before { content:'\e0c2'; } + +.oi[data-glyph=sun]:before { content:'\e0c3'; } + +.oi[data-glyph=tablet]:before { content:'\e0c4'; } + +.oi[data-glyph=tag]:before { content:'\e0c5'; } + +.oi[data-glyph=tags]:before { content:'\e0c6'; } + +.oi[data-glyph=target]:before { content:'\e0c7'; } + +.oi[data-glyph=task]:before { content:'\e0c8'; } + +.oi[data-glyph=terminal]:before { content:'\e0c9'; } + +.oi[data-glyph=text]:before { content:'\e0ca'; } + +.oi[data-glyph=thumb-down]:before { content:'\e0cb'; } + +.oi[data-glyph=thumb-up]:before { content:'\e0cc'; } + +.oi[data-glyph=timer]:before { content:'\e0cd'; } + +.oi[data-glyph=transfer]:before { content:'\e0ce'; } + +.oi[data-glyph=trash]:before { content:'\e0cf'; } + +.oi[data-glyph=underline]:before { content:'\e0d0'; } + +.oi[data-glyph=vertical-align-bottom]:before { content:'\e0d1'; } + +.oi[data-glyph=vertical-align-center]:before { content:'\e0d2'; } + +.oi[data-glyph=vertical-align-top]:before { content:'\e0d3'; } + +.oi[data-glyph=video]:before { content:'\e0d4'; } + +.oi[data-glyph=volume-high]:before { content:'\e0d5'; } + +.oi[data-glyph=volume-low]:before { content:'\e0d6'; } + +.oi[data-glyph=volume-off]:before { content:'\e0d7'; } + +.oi[data-glyph=warning]:before { content:'\e0d8'; } + +.oi[data-glyph=wifi]:before { content:'\e0d9'; } + +.oi[data-glyph=wrench]:before { content:'\e0da'; } + +.oi[data-glyph=x]:before { content:'\e0db'; } + +.oi[data-glyph=yen]:before { content:'\e0dc'; } + +.oi[data-glyph=zoom-in]:before { content:'\e0dd'; } + +.oi[data-glyph=zoom-out]:before { content:'\e0de'; } diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.eot b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.eot new file mode 100644 index 0000000000000000000000000000000000000000..7ca7c170f1a7780c7ed612e469d746adad4fabeb GIT binary patch literal 23144 zcmdsfd3;>eeeeC9JBvmdX=XH=Bx|Huq#0|mEX^*FEZoYPLI_uavict*V{?@TO zMkW-`8y`~?`wHZ(8ar|*sx;95Q5460cy8M@a&Y4EWz?ix|5@Bu?7IB}JD+&;moFlT zv2NJCdwfKrExSW__7i;aoZ)!Dh8|D=_bt2cICSS{9#tA}|E!{@_usyMY~;Hy|K)1b z|54<9>yD8-Cn&2tjC2w2NB51~G5)JjaZFJL@xHiwV*kNIzw~BwMcIz$wlO)OsC{De z@{6~4mj1g^rAARs`R+$fJT`Z|{D>Nt`4#3;p?b6)z5IwWpz^pBH9osEe9M17lR2*| zyUo=T$RnAz0#nX^Hlfp`Vn@F!L^tjyj4R!vq?GTL(*wUeO9Du5*||njzJ6Xg|C;R8 z0KUhO&Ff_SMHM2dE@77Pwf7>(kQRbP~cYFu*RbH+< zUEW_YJ%7CK_Fj1zPSeHtA@`&1QgvN* zclFxp+p8z5zghj`>R;5@Y8q=+)ZAHfs%E<8hc&l5?ewnpj(Sgf zzv+F$x6^l@?~U4M?b_PowO_Bj=&$v+`v?4c{a^6E9#8_&z>2`Ffd>N9fu97g4!#&# z651PjHdG4zbJ!7X4{r(|37-xBxUQ)#UALlcux@AFvAR#xovJ%q_pQ3~_1*RT^#|%d zQU7%P>kZuv0}T%}oNah@fnz~&!Qg_u3m#qYtp&emY;3%}@##o=M2kESIUSjf{2=mH zQ?_Ym)2Eug)bx6@t+}arRrAi~C!3#-c1CZC-Wh#7`orjNV@EJ#yGx%3MW#4d0uU0)$(@zSTAHszP; zuQ=>KS^BgpkW{{+a<-kbpLROvt))+c{C}Cw_%gm!#+PXL!LyG(DuOd_Hqg&xo%m9t z;4e-E=!9avSPq|Na^{erPP(YOX;PoiN+o@QCdKs3YK<;xQ&XB|?5MYwbp~VjmntjE z6_l)^nx;(|TT_!|JwQ;=P=qqhR3?{A#vQ@z%M^bZ4QeO8cS20R{7|}7O7A{#V)sMn zD(~9aa!NE5Onyi;<8+j3vpo-w4t0aPM*6e1+Dtf%`iSr^QuPQI_U3tF0vU9o9N!`Le*GMm=NjI<4i_=) z&DJq#l+e%3uccIauR7$C(3N$})le#-XYbJm;{B2-FOL#)#f@R0yhV>gJXa)JZ8a!g zKPQHG)~>Fro>ufam8BNa8bUGAp#FJTnC|&TSoJXEK zg8y3nrabpu-sykj_v?QJ?qHx4@E2Luql6_-Gj;?RS|CixFoiN{^z}{J@aCKPGjG!5 z@v}vGV$(_e=1n8V&#F<*#KVuAJwCFDo+#d>-&{lzRbxi^i+QwNaY3V0K$_q;#=QCx zV0CYur;8wzb0y}oXOIEH0(qgB$#>ANkW#O-vT92CaHuwn1@Q$OiC8?D^CfZ_e?IJt z*_f3ELh1Z6%C?lH>A_kr{eAczEl@M_i<*FT&xM-0Ns8+K(jI}xBaghOQK}(bZ`AY? zHGx14?bE2<_o1gsk)Rfg%>P2a1U?oY_hX%7X)y6%A{aoAa=vWX7xTHq7hIa=n%Uuk z#1Ye#&zD|sxx`wR#)1gN%V-?j{K^{UN|zOt?Oy(@oXZwv5lUtAeQJysx`TqXVen96 zO0tE#KbMF*67i%x5p+77vBh*#gI~4V=^~FN{fWBxV85>P560_0ktPQPBlPK#V`VRu zjyc4~YonN#2lanj*tfWUaIk;TvV~OrAT>o!Reo^K8&E6Dd0(}H@hfRGjpk!$IG6Qj znDD6o$s_l#dh7hE`?XTvU>|61@R9wwty^>Z9~qo^uC1?+>yfk}l33qrsJc9p89M|* zBIisMQiX6KSa1dlQ)wFh+0XP->5UtwrZ#Sb^qx|qmrg!>>5`d&y$lv)(i!RsDiP2M zBqVps*+g<$av)z{HkpV8LCCnOKI2Lk2aj~F+Q`7#7v(aDr-@)qXN29ILi0-5KJ`3hmQ918kk zaYrDO!A@WrF7#7DL)%(97@2vM*GMF1UhN-J9UT#_3yh~T5}bLJG1^eSNctlk;MgW_ z<2{d^0-{Sna5AHZ0KP)Ls1{}blg|JnNlN7xsq;J3=6dU2U20U*cI}$^ipw>-XHjn$ zy-uMabQvTLGB-#JTda@3*21U^tRa>z7_^VkxAnc?LRVJ@Bd!mA_q#N&f&dU zI=#5Qv0i=Z>F^Eo=FA!Ib^c>_2~S13phou&r_;@ezQ&`C+Q=^LnESfTTJ^f6%{MP! z!?R`XqX0PdLIvyOmpdsq%T+MA5U&u3rC_X-BpW+sIc`&Jb{>;4%)lQ@4l>2})G2dc z6+>d$c*>&}p2)3Ox!L~`jAF==k0{VoB0#Tc>=ze|EJw z9QyxuxaxhI^X_UoI`f--n;+S~deyS^YD8^wxb%;?Z0`JKU%iJa^&^`L`yWXr?p$Bs zc0hxd{{mXkw6Y5$&E_-dW-4@1Kc$<<;S3j$!OX)$m}-Na1e{@GK$j~*# z!Zkxpp;gXEu)4ufOn5Mv@#sQZ1f{AHYg^-qkJ#)EyZyRgBY&X&nT#g~5>;p_ zVp|xEM}mPSPoijN>zLpV)@53$0Iy)`#=>`y&)(`w1>??esD_d$H4&F&E=ISyTsP{+ zD1IqyAE>BFrYd&b=yE+Hrd>17xy1GtPwMWw)cU9WDQ}IRysl?lK$YA8?|H`Rez}X9 zl?3KK#x0pr0`yGSKNLtUI0EtkMuADB2rp|> zo!6?=6oTfC_gn4ZYPH9z4UYwqO8%LxmE_)}j~JOoGjsB|PKjdQbj!H|)%MZcxbnDJ zPI+PO9*APOi;^ZFp@*NTunpTP231 zKWA(p30`XPM&BS$GxJxlJm4;@sxL-hmlI-0pRk_6QiFBCGx8Cua+2vm0!zMZJ@dKD zXZi$}oL8rdVl+vUtRC?*L(Vj#J#z+pkixcfsF`AE4?qMKH;HYqm<$1oO)PO(i+NoD zF>EVUH`tRj9dd3}gH`&e%Ai`4udbXTI|Z9)t1%brI6DWpcVh-mhQpU543_HV4OttZ*8s#fqlWSqU<989HKeF`+Jm%$;32QxP z+jNmL0U)|2gbxQA=6KO@BkJn5y6HLu+codxcfv$uj>UMC;iurM+SYL+@RXXA%c;qQfVrn zMq%vmZtn?3(u};)J({k&DUIQmp*vSZC`0ir|VBpH&4)9y})C1VA!2V7k8g1 z&2K{TAft~ebSmf%lp$!V&Tt~tnh84-1>W&eM1XsXA<4oGZ55g^|IM4rW&_~?5`>-2 z6!c(|raY8bjQv$uOu6OQj^Tm@hGGE3YC;9*#O{FR?acA1gXBVe!?=^X$a6|6ay5NAQaQ5 z@f7x_d9#$?d=Ww;n5Kj65zsRz;1Z}BI&LRR3;@x0C}9+L;{{^}MB2XyLx+4n%{lG7 z?<9YgG|g+21>O%ryu@>iMoLf_)Ud1atqUcF@nt%@Q(_A&IcS%p)y(4{RPxd*%Nu zi7gi!hGc^@X3mJeH@k;zC1H`#CB{a^o^+XrkI|mV=bgY`hHhRk5m>$WRaMPf=b4{_ zgV$Eo@Fo@024gSjA7g(1WzLgbo$YTev|I3WKV$}WtB;Ki38-^Wio!k$Qw)mYi&~n? zNf<}NY5f!pvu>Bx^nPT6GiaQJ{PKC=wF-l$!%~0cYFYE(%;pPyf;9(&d~=0V(t2ji zws!Zw2nk?*SiEL}QAeAbqF7cFU z7{11qzW&V+9-GDVo1X){9{*g!(2#%6RV-hw+QVa!^eQog9U6-p{R=R?@;{){R%R0N z1BIfKCTCUy6tR6)N&tqq454^>u>PP}{70SG)0aQXrYQxc7|>HD9R{%`a!IIj(vTLm zHxcXmuq`EEO-ezc0y(f?hdfWFTDyqldLNavjXlv$t-^Py(oV(O*Q__a9MwQr4uzX{AV3h=tkIf~i^V-88t+QgnyiRL}uVqp>es(r7 zuU%OeYQautYf}Ql-4$ z!6VpDeQm+AeIZz0VN#jt85)kcjod8Ej!;IOr<9x48d$ zZBaqfHXSS8aig}PMZ5R&CKizGSu%$00Q02`W~pF)SPN%&B`<-sOBtBV{(;oW=Hhqb zB~&7DUOA4r68ji<>AkVfyfVO823~q^jP<+l5_%?ikfbrm+g;3anR4L4g=}N>73dR= z&>@Z2mUTQ0x*+>ru(+)1X_t)oF!u2RSXXkqu1&UPs<9>Ftk3p4ER>N-v)_eRneAMF zT6sE|9=m!?Pm9at2yfnT|C)6lxoMrxr@Gs^2CmWWm3D|yWo=_5*i<7$l9-O=R;3Kr^Wd`q zU~KM40@gJGHlP%%2Vn?s+Ni3kGvpaw)p2)wXXnbBYa_vWPrrV`t^b;iF82mMtbh9+ zS}LZ=ck{}Qj?UwqtA=apf|0;V{iK(?bl38lkSFpZ{iXY8skMjM4#jd~PGXcx31i#w zb}oOvLE1!_1=ZNXT{bWwTb3k8O2jx_s#vfl*U_prkA(s=j6Q_ zwRwvxb{D&DUN^Y*rY_@V;g%JvR<7El<BmN6ieAmntSMgT4u)Si%qHi*74#=gevLdda{_h+0LHA8BqkbslFG&Kd8cmFD82Zofl_?m3P&fLU8eHK1L z8y6W%7}i_H2f$6tS$Y2%b_$-)Zr~(oGj#^p-UX4^1>6pRk5zTz10ia79;B=<PmRh&yoU5>5wf%gMMi1Vb|ZJ6U=a@~|Rzi9)}cO0rQq ziHllDLPHLR(%~i*)?~}JLX%t8heFjMS4XYgrdD~p4u4&&raj=G+KQ{IlSOYaz2TmA zH5h5??pZ+8+~ljSwg*-dsZ|cY+WTUn*RJ+CRF~6J=?~5tXYTLxdlD{NP1swdIvqCO zRqI5evMH}RszY})hpt)@sB;DuHhDT@3#)2t>MDa_hrO!WSu6Z^R5rUS>}sIeQ5EVc zlc5}q0a;CKi30o%#>5LF9B;zT97}UWLi>(Ji}`AXbV?nuTrMntJtG0Fu|rv`WRwCh z*>Zy)TJnLfuoTV8i}!PH$q_JH_F3a%{D1eGc@k$<^pqt41SAneu?ODX|9P^Xdu!o< z^7I@gK(NRxJ}6IPyYe3kFzdTque92g{0x#JX2*xGbJ)=EAR9Q5SsOnA3*gHv4NE*- z4Zm`X(LUR+4Gqng^+}JGVW+orR==3?gD0F}Rv?Cg5&z6yzvwfy8NdED&Zmuj_Gjs@ z`TburwU=3*0i2zY+y-W0Y5N1t&c%YbL&HGT(gp}N!b-k%#}3O5U@qw4?EXv36t4O? zC98r5QmQDJ0(RyYxE?4o@Y}O& z17IKc;nRmrH#izQdmPPxk73Be*B%BcKE-KdXvbwcUWt2Pjs&6xOO%EWr;CzOQXc8k zzo>t)j~%Z^2Jv~aPyak!*UR0T>mztM%)roVq2I!(3IM>=z_9`6RQ(Q+%_P4s_{)?R z99aMw@cRldwZLNKGmsP)w8AwY4CLO#<}5E;1$}+KXU3NaX9^jgUtIG0XB>XezpR7P zoCiu`hpfR1Aza`KV0+|3)RGSOvvrq6dol%Q07H$i^dExzbx{obP@?$KEqgM0g39^YHMj>}3UnW7!P#gQf4=BG`Usd9I_pJcpvqax-rF|FQimBG}65#Rs0j zc6^@MloJ*hsNI-hi&N0^-$Ue{x6#22C+(o^raKFveGUhxn9_~j-CeW(E0cynL+n$V z^+RVE9>SFbrQ65Y5ZIXAg!ij9n~dVX24o+*7T5yX6hqkcr=$mZiiX+UY*DB|qt+gg zwqo8TtqHGhDzlX*i7aiTU??YwMeW2v#Zi zBc7@n_|a^3@Fg%ML(=U=4vmtrd|*5~>@6giAAnNp;r;h{{EfX`<|Cd1bEH#S)QC04Z4NaAovO)MB?M}b zgOWqd{v%aX3B&d8B7th=o>{}4!ZN;x4$gRiIY!XL4n|ty2Z$Qp1ke}cH;W!4#j=M( zbl74juQ7^2|%Dk27f75{bH68K1t z3^H?=)COypv};34+kG8w&F;MB`iEYo;k}cP8z&pWP##+I`g*@aPquGQ$wKdKc@^;eeyO)y)Z(IRwksK4{BwuLaQEZNZ5&u%Fz z#sSNS^$}rFf8s)u$;WNz}?_oAzFE&7cVIZD+h8nEhi3!1f09wA5)1|a| zB4Q+l&m@zsJUwv6uz$+AHt1KrXIiFb-{S_^Mryog>UyxXN_z(R0A$(j&X#Dlrt-4M zezV_D+GJ&Y86TSi5gBXQx$)-Q7_g^#{*3|7j$M&^0U9Z+CYT?PZo-$5lVsTV*mlCu znRR%z42#V}V)IbcwDuBY7hdN+Uj8iB;On3-fKMsuB7*taGLg+YXC3}#1TJttI}hNO z0CmLJPXHSTze%B*3TY$jI%iX50065N?U7I3mc|~M=xa;g_Q`;lw)M5y5KD*%LJ?c? zR~!bAW=%t?Bk6soYxnlGwe{_-J-sh&ga+{3%-W&lcqF5{Seos-9IuQnoQw5?Z)2{1 zjFSzlH7~~R6884Y8AL9^nHtz)ME2=!KrikAU>Met0T*b5`*8VbwG!*dc52h&D766a z67q&FobFw>@OvLVec{t$dil!J7wSLonG2^s{5`p@hTm9np9G*DSP(9Jsw5FivC}X? z!)%cOvyuPtB{`3E!&jpoNdv6#L7QXs142D#bI#rP`fV^ZvHLi@K>znYFjKa`(4B8! zLn7!DIj&bK+Vz7*k{-6z#2ff0O<&SkQ|G)Ci9meo7t)l;p2!CFthsB;8;@_H zP%qAS0JdCg8Q9h0UFlu3JYEsEf2uK)G*TlT+oe!m($`O3+Yujr3K zu|2d^)3zQ$Io{JaF^TbT%pvG#UaTRQg%yzJr&kd8pY}t8{e1}^)TpJ{!f8wwWt6eu zW0!SGdSY8CNC0c@qdcZW8n08-8g{Z}lZNA7{YT@p@Sdk$e(0_d^6I;O`Qz+Tl++JW z+pfK}-?;aom$#Dl`xnl&eQ@muB^`ZM^2G%D3Fa^x#V&WF&zbTWEW?PQyGeq5He^9 zX|pn|NPOx5@&L?TpLhXL8Lk^=&fMsdw;Vd-(tqwMm0Z-|LeHTC$V&N4hGk0w<*{_) z5%5Wdu=(MT1mIn_G{Zr6WiOTd7%@d$4fDL8N#2UcS_=8W3S_``3NcS9hnh@5m1?GZ zEv;$T<5}Vv@O=KCsy8GvtJ}6U77tCkRL>4EeVzW*)=#xgHfoK}-uS0K`UCy9LtoP3 zjvhknNZFdeL?@z3H&K6#9X{&^yGk;|LIrW?*jrj`n!Cs;R=&nB2Cx zwa%$k_N*G<+3(9=m%n9kLZhGQjM5XO|;)z{achl5^&1o1nAxYK0_^!%gHg0?V`J|?P=LHqXFMSDayz({; ze#q_<%qJh_NEgB&#iTb+laGJksDAE_8;-IQ{zE&~9Dee1Z@wv}k0Nv54FH=s@5S*S zW__mIf@Piko`j7IM<5XAX?=qK8xb5v3OClCkh~scaHcKDha7VWs5B-1lyNB8U#V*i zvE`oQ&2fY;5yUY+i=**;$dcuJhR?9Ew>oVL)nZ$TmIQ($IgkZcY^zh$_QShNkL$m9 z@)b&!zI+#A8`S9CKh%G8>PZSbuIaD7a@UtxLBro1O;&<;v7Z)u#Kx6&2~-AAGFvNZ zw^U{Ad0I8C-SYT99Q)|c@7FGzmik_sX-S*bpMUmdisK)8_wF(7$Yl^ zSH^fg%?K>81PE@^#Ls~&3{$~0af*b`_2?6zN}7ZOaWkevqy&tiZ6#?@yQH6!I8@$< zWZN9pz@D$=^8>u2Atfx69akHZBC@En37@LyVR8KUq^Nc{(IntQ^C^W%u$#qcsNhm4p-oYvN}V)`rq{BHTy zd)9OMphS~DmEXcAFZ$1AB!!{wn}L%vFO|PxybgamXRedqZ2dx7njwsQBj=~(Pi0yT zi@zj2@mc^eyloB2uy0)4$;UfqK8o)xbn$=6o~Tjev~a%o3C@RJfr006q>XnZBJKq2 zV=@w#V+Ek=yR8E&@_6}0J|l>uDU6-jMlb~GWo`<5g>&t#_(~Nldkn7+NOjxexNbVC zw;K}!%YpHrwCR|P#Fg=`bF^!%p<-cLt2%2%to~gYmHA=07%=C-D0gKDrV+$~2TQii z>Lj3V(_?@b6&${U)fArYLRi3+{V;YWV?pdu5U1p2k0IB}D3>LC;veA?^YjrTuo63i zLx}kx(A0p26B(PBcCB+xn`9P(S<7f1o)@5UNDQ3TDtDB!A2p#y(|v7I>=vDXonWf1 z4`)SSL*N6I>=zZuzBXfQs%et}uQXyz(&1VoXam4e1~GtX0Yid^Rpbs*Z zMA}WEEJ;Qxt)((n5a)rwKMgynG?KzXIy+C`V;{C_n35Hf){A4(Xz~DqYT?(4VN?x&t9ATers*era_Tqb+b1rnNuMIz5-B@vh{n|L=C*_X80~k$pa9jaemf8kdl~=TA zTv@0AmvEmDi!tw7`88KbjN(gwjlX33CG-ot$n~{K;~V{X0q557(Ofq|pVTzI!|BHx zaV?(%K3^h(7>1n%E<{ZFCaW3 zGJT~CywZHcouwr_qA=1$*fk2{|2a*l#*W-@a0<3y0KbM|-TEhsAKOvhLLP zo~Uza4&QAL+~#v=s_M$d;(3>9(jeREoh%ismhJD*$Q5Gz+l!R(va17nP{ za*^$d(0~Z25W8p@!5pVT7vK-&2H|{6s(b=erKF88d0@gbJN!8Jy48=FwhVabsK)8Gk+nT(ju}IT%&Udx#Pg6FJbn{#z^`;e2g>! z+5NSd7C?vp!%VA)BlxwMwkfqVYNqWG$DhI}hOJIit!^`6HdXkyAIYapa}(emAZQ@!OM!@NYs{ z0*t>HznaEB4SGmkjd#27W1u_n_CAz5qyQ3Kbzpqt(6|;I{Xld}*M_d>=6(AQ?1>)T zb7*&T?f!j-*6u&BYdo6n>W(hledy4{lEsU6B6la}b{*W=wQu|o>L~jS()DBe_Z?Kw zS>snph1pHk`_0&KMs1crsCj4SV6h6v0&bqcxD^`FVa)=XY?idGM%Nb(9`r^>8tcLdWQZRJxhO1&(UAdH2oz#PcP8d>96QT`UZWI zUZQW&x2Z&bjb9b~4*e~?Os~*)>3j5j`T_lSI!Av;KcpYgkLmA~w@<3*=@y9C>q(2A zjOfXVo}B2(i=Kk$DTxwNbeMxZjtGMy<225B9j%FoXF%wrXVs!k?9qg5s?`cnK6;k zL}pxMc8YAb$o7bAT4XaKn-$p{oUbBV5ZR*0_KNI?$c~Ean8<1(J1(+2MXp=qdPFWQ zav9jrMJ^|Dd66rKTv6nDMQ%joMn!H+4o_-J;MV3TaWuh(cBra-xtIg@Py)MWI&|Mnqv$6vjkB z6NPb6*eQzLqSzyfX;I9GVpbG$qL>%Of+!Y6u~!sFL~&FU$3#&R#c@&GDSEp_Z;$9r zi{6ar&5GWf=*^4Xg6J)Z-d@o=B6>$f@0jS-MDMuh-6=-8#Ym4BNsEz;7|Du}oEXWA zk%Aa0ijiJ1G9pGs#mJZ#(ZtA3G1@Igd&Fp3jAq1WR*dGvXkLsK#As2B_KMLFF*+(n z$Hb^6M#shIPBGRk#(Kn9T8w4HSXPYX#8_U86~tIkjP;7K5ivF@#>T{$CdS6a*iNB! z3#~_JX`y9=mK9n~XnCO(gjN(i^Ey7PQWIG;<SQt;ScwXs*-`HK=76p0$;!^O`vT-g6sA9SOvHx!L(@xe7gk-ya|LxS+E zDBmc1qGI6lm7?G|&fqXu`)_rbqT~LC7K`IzbFWq?F6BeiDtb`hB1O6SLQ%;ae@}tZ zdPX^OTHbvKz6)`hp4t3FD&`2nlwId*2PupEWfk}Qv%jn<7Uc(jIY_x#z0+R~R<2N| z`^&l#rf%?;ElQ|*6f39(Ux8cE7|pVZm3`M=)|76_=l*h#GEDi&Uk+A!sCoXfuJpn< z&1)@6gt}81tdw9Bxk@R%3-J7T$}D_mDt*yIUzCO`Q4+R5CGg#>CPIkr_o(N;8T|o+ylr?;9K0XF_Rdalbq67?0ZVRNHsj_`XvL zOVP(rC12iX8jATvQ?Q%puc5s%5p@%>|94N`4^KZ*j*G@QSZm>eV~bG|@gI*; z97;;pN9z|ka!TpM(#eGdagqHZ6Jlc$W8!1u(CpG4mGdnXWfbmGg87&zA3hR2(=3`Z zsf;QtnKrR#N@QH$SPWi4lVUWVf`{lbwxZ&~DKUWoO4=a$06+Vw=T2I2c^r(9fCI4M z_{{0wz`19X-+qf!{Vf&M(wU&q_W#e1UjqDANwVqEkE#S?2356N=b2YEOkzLpzZCD! zs`nSodoIXp4|aqoJ;pNT-*vMty`_C``>T4d;480!#J#$geyn>h<)&WuRP|84pzBr0 zpCBxc4gq0R4CM+icsC^sa;S$Au0$x;DA!?G9azR2l_=#FFk7^8yCj!*kYuuw3Szxe zxl0+K3{vg}u@3*CfhJIMd0UC-_)uBeJH>y$U zEovV%TD@KEtH!GFYNDE~rmFqaJJq|?0qP+2ZZ$(4q7GBWk|CQO~I&EAg(ZOe0*$tTzq_dLVRL;QhaiJN_=X3T6}JNUVMIhL40BS_=MPmxP=l9G~>Qj$`W(votM@{;nC3X%$w#wW)n$0f%nCnP5(CnYB* zrzEE)rzPhm=OyPS7bF)Zk57qBiA#x3Nk~adNlHmhNl8gfNlVF1$xF#kDM%?y8J`-P z8kZWMnvj~9nv|NHnv$BDnwFZInwOfNT98_pIzBBnEiNrSEg>y2Eh#NIEhQ~AEiElK zEiWxUtst#1ZG3KQZd`7BZbEKiZc=V?Zc1)yZdz_`ZeDJFZb9z&yx6?By!gC?yu`eu zyyU!;ywtq3yxhFJy!^a^yu!Tk`LX$N`SJM)`HA^S`N{by`KkG7`MLRd`T6+;`Gxu8 z3t|i63gQbA3K9#F3X%&_3Q`Nw3UUkb3i1mI3JRqZ{eM?A=Y_Xl%_!<(kjWAd3InM; z3u37Oz8D0}dbe^9SnytTIf$ngTdNFb&uMlHmk3yd)0mFe)WKQP(7r+rnacBtAA8m)i>0`>Jjx# z^{D!m`nLLx`mXw(`o4Nh{XjjgeyE;MKT`jso>Wh%e^x(M|Dt}PeyV<^ey)C@eyN^T zzf#Yre^vjceyx6^eye_`{$2fE{XzYQ`cJhDr2V7nQGZhZOFgUpOZ{2>1*FbZuiCEu zstVPocBucY{-!CKs%ct~7Od%-MY94;U7>|&p;|ZXN-a#gO1oO?uJzDtTDWG{BD9{` zHQKe>b=vh>FYN}+p*gikt+#fgc9RyR-K^cBxwJmoty;8pn|8YvqxDsmX?JL`TAUWI zC1{CSl9sHcXsKG7)=%rN-KnK(cWHmn251AdLE2#LZtWf|L%UZSq7BuCY4>UOYY%7- zYQwcm?IG=9ZG@JkjnqbIk7$o-+1g{;<64e3S{tLBA-Q`Kq+~fHmk*NVZb*f%FsK+@ zGFRKKy{UZ}1b7+LKWI$Q{Ggq|LxN`pza7l=(R!nP(lW)e$I@Ya%DTk*k+sbTH=em7 z{ECHFd=>I@=!npR-LC0&sM|Z;KJWI=ZtYi=T)FMaFRtWa5n)MTnPJ6Yi^IOVD)Or5 zufFo?Yp))Dbyat@`%T?5x=-lt?$N);q#kuWUhmM|ye>?Y*J* z(Hmd6X;ak7sE=;G^5)(*kG*;A&40fo?v|Oibhr{+J6x~3-gk*U_C7Iv2K5=;XKJ6d zefHft@z&B?KZ*{Io*2FVwqdtTx!r#Iq}%t#*kYcHamT#U_u9Ts_1$qt=p8#^uZ_Dl zZbjTL@saTl#LtL-Hz6jWHsPDZs}g4=?o9L~MJ7Fzv@7W^$%B%gO@2A~yObMJvQxIE z{5>^2bsYpsLE6cF;r#~md$-@&{;vM(`oGuzvpaj=`OKXi>G!4AryooIr7vspW;dx# zP5kMmpr#0Z%KL&iWohy)uzR!l)4ptL81wKP`cvC8Jwf4Tcx!v6Ju<`>C35U8UzE!m z<+Ab|9Q9xTW^s?+tCw!f&u3)eG``N6<~dxvyn;yG_Lcju2V zrY>!oWg8z232n@yi`vMO=!Z`B4)Gs_vG(R5Zxo(0gAJSc@MMF3Easi$WqRhz8Jh<% zLo|wD`yU29b_)|0R`Tq$no@(m$4hjt!MocOA4 z3>M+~VWIykiHDaO`}ipPyCPfX*Vb|UllQr5gq_<&3wA z9LBtEJ#6E2EUStNOmHhbVzd3G5SPo2IY*U6w9-arv10yKk`MwdgRwN|46Smk)bK(p z<~iS8Fq+7XQMU}=yp{U9^u}gOJR5b0hV-q6@m7z@TfJ>|)GkLI_26|AA-Vl$y>2?2 z<8!;ba+Ixnx$Yl5QP_pyAHhSm$ZqBEX=Lb{2&h#YL4AwAYmUBT-#G7e(|J?~+?D3A zl^^NcdkZE_w9tGp<^x63%*qKnKmAyKyTF*t7M}ABusi_@Gbbx5M6|l>zBZ4y4YzUG zZw5iSaBt8#aiWJ9(S!92#c&4=9(^+t8vYKrP7O9roYb@4nDl4%CHwNFzZ`zU@GdxM z&ls#9&U$GOOJOOK24{^lP7Kyhojl-ST*Ke!p9Y;WeEmeE{fQ?^N3krHwK@CcC#auU z>lbWc+u8P&+t;r*_{2ZiGw#;c&ssT^6|tfPrL$&HKQF($`89Tg9VvbNiIB8Iw>fZEh%5L5_V)DG zCK$dHUy45AwdrrO4;b&qufx`l8Qd+p+jl*-aX1^wL_cvIwtgrM-}u-r>W=17%Bdi5 zb4O--v!^2ybLfqVpcV1Dt+aYrWFkE&(?C-G=?n@__4dXB&09LRvT}hT=82smoG}-c z$*1YF>dV(Vyw6+K*VnIidNq$ngjB%T0EX#0Kz=%~u=u5$W5vsP8Rmpj_;vMjHzUhd>0thS%<{1)5y zF8;7(xx1#y;rrTU_qu&iKDQ+dv!(umpZ+-XJfgOmoLy6J0`Ar z^G3s$V|nxEWBe+{jO8mCPbP+^fk@FIw758-IoVB*#H8JuJ8|0M>=C2A*~0!f?NIK{ zv=1Nof&I(iTHLonbGSM(?OFTB96r2%|6zy8)X`vpXe=2M)dM~rFn~u3xqUp~}lhokzPwv`cpd`wMs+2o}s!4}8();yJdT+na-3*2DY9WI26N z)~qpOvK%h&L2E!QIhiN(OmdT9^KRd{ZXY`sct(-qvU@Ca8C~LZS?Den+s~(u4@pfO zG9=Y0qAlj5w~{DmHN^zTv6Uo>UlxK;JV73upXt^MDko2_EHKE~qHgf93aTCXtXR(J^#<~%;*@o!uve?>qn5AOO)JHNhjPm(R01uExn9ub0 zPwhIs=4}H#hyhrG$`5B8VXw12bvxH>T(_}du#QZ<%h201t#TDQGQkXsi5+;b%YJJJ z&xzozn4PksqO!6gC)Viv(urya32z%%yUhXxtU|zT2x_0x{tASHFH~%pH)no{ezNp% z*5<*+)>ZnlXP;YMz0BYnyrFt^%`?v}t}*hObziy9VIP$F;237@&&W2W*ZrJX1%Cv+Ef~n za~W%4IxBs)tY*5Ap07_|R9?!aGdjgqvenCKmm44K(zlG>RnT0rYs2=ftkGaRiY;R` z>`VQbY80xmEn8Y$vw7pXt<0dYJ6R%bv4~a4$r0DuvE*H*1w zw30QlO`Gtwv3&FNWk!0FUbFGpO>DE#BB@JCn|m!)>lUnASZ{Fa5q)j#ss^@>(FwMY zRk^Dc8=~jXqD}gzr_*_V5z6qFF48~iXG|#*JwMQ&uBoW5WW{V|F)K9)HwJmnVNn;j zneT`1m@Y-I_cN1(ePtpW3@C;{mXoL!!%Wisj-3@-^}3Q5Dz?^bt*gV%7|b*D>NRzB zYnZ{RA#l1&NI_&HbdV&}b9AIS&SkQic4@h9~MRlq` z1CpiYeJ>7(UFxZKDFp<$QQiqh9-GG- zLnC2}7cW_4#E4fbDIOV$SzKS4I!aYMKRC)ht2y2Pv>ae7^Yq~F&_Heby$6GfHcK^GO=^-yvy~ZdJ`{;|t(R+y0tWi&oCXFh22Rvci+7czW%O)w3}hIgkXVYMcSp z$ba>XvfoCa;I_N_1Q48<6jCfH!n zkUiBVcI`4S^b)gWxSk&?Y=CaKE2MpRP|nAY}YQ@`H?0Vp!;CkKm< zu?^q@PM-9*%=3O*hzt$%pJBf}Bn-op0+(nG=1O=fY{F){6r9-utaB?W<~j%P){m`g zD(dDs20$#3-V>j??DABH!#cOFVvVESVIgjGwmbX{iJ<_hu-GQr)El~_aEA5v;ad0n6|>YsdJ<4fyyuQ<%RbK7Zt(=(&r9^8}$#vqc# zRi>rX3i|~!=55nqkJ$kNJosZkSC~N^h#ZGhN$MVzAPUe>E3d{Z%RM;+n(f&lv}nEv zZ5cecMTE{T5~11XAvAl{@|c5l!q&=nYL`sYz6bv^SV9idKY%NT*Hs1=mTdycoU^+{Iv;r%MF;lJq~<%%gHromU_X!m;IPX@P}rlpcr>6>pshCwbr;M-vIWWN3s`@& zcv4Ma+Q>JBx%XpRH~~Cl@U>p>((q^!zW(-7Wx|)W6;o^qm-c4AaOtQg;T!4~C<{%2 z;teH@n$uN9LXeXrhZRYjNwD4Ja=|0vb-QQ*UEWbvk0-muz6 z$Ay3*``Fq1=gL=98sEn1w$lTuO7wu~ba>4EEJsOI?6-R5it^{?Goun<6>!xLZEY{L z^0SBRaWwnzh>+LBG*zU-gO&LERQwv}@)eS%;O>EuY;QxSZ7?n<<-;~T!CkBNVW6WsiAG6bA3E2OEwRfN@Fz`%KXiqE2`~!U(NRYI$5GAL;#8j5RxHDknx3}?2L-MjbU56TU`a+tnmal=$L*T}Zcn?HXZ z*vT@VEv#N#ZTx`F*u;h@Tjp#oZ>)NO86Sf{KDL}Va^!@w#mYV$_vX-gL##Ze&#jrU zjje&ESiNf1Y6q{jtYRzNHEtuu66!gtEJjh{t2b^O^?VD8lj=vUc zL${(HT(acy*v{a-qSb0UK~9S^$suT*6G0VkQpG#Tn7A^UHG&5wB~6^DS5$~vQO87) zA=3Eu;(DHj?Rso!4#tZ_Ew8JnGsOMAVGXmogeOh9*6M%JAKo!+Lk6r|lc3LQ zoS<-;02dhA6mz8!Xig}#%V|!zCg;`S-H~=pE%}(H$RKP`Y?gZqildW{GaIdH=FP}dHj?pqQU>jD|*VitrURb>VGwi*M ze}#T$R+Ynb;Frqp-R}$ab!TEL-^y0ido{26?l8T!V#C7K0ULOA<+=s6M&BQfeW1T} zl>IaF=!iG_u!kQuh7ODQ9+G=@?R*Gr_!*WOZVT5%k)8JxJ;e!Z;rtp%JaWR4SO^F@ z(~tmblHYUV^d4vcDz||bps|3&pnMq!nV<~fE4T8;`0M`#4wI11!yLpucH74D4GlOo zn%-`~Xyl0?xP#uU=q(!}V=T=l$i6SuS^DcFW8|=*xb1a7X_7Gs{n8|Z z=Oj{s1d@ICuz{C@p?z!{;mw6*Y-{x(#DK@~jq2)sf~Lh|04qotM<|oJgN%~*lwinq zf(>oXdTWLurdVy?WX{OTVndApesZw4C$?m7S+Sz-IWqEcEMI>3;g`<0thRUFYG^*p zJ}?MIQ8y0TDStPaz*1!}L2@j!*{sEL78yLmYCF@Zxq~JK&z}~8XqUa8WCp?r{X#%r z)%LUtgv;Bw*OVv-yad0*b}W2h!Sor^X4POjwv3Hu#krL)Ox`u2&jU+FyHo!*_XIOu zWwkTh)3W{$|CoyvvI#3Dt=_b6)VDc}rJF}r6s=siec|?{FBsL!mMyDg8`!q-YpU1R z+`(;ow>GS5eq`?9`L7=;eSY<}x`r1ftvAXx==-<-aorJ&yIiRD^<`_Ot|~TWmg!T9 z7ayuvIj=5z-lDaY>)+h+iuBs6?ISPxaYbzWb>Av}PvDaErTUqC5kG9m+*48=s-hdYr zjzGT`)O!%CAR(|h4!}zS!QrHD0q7-?hhth&Up8F*9s=rcn@ARlzO)|9Xc~A}E|&@6 z;fp5q6sRZT5@P`G0w~ zK?va5&P`xhN@Lb`DjVPpJwo%RA$sc$W?7rEcRyk9AH+ZT5B#6{?%kVSV~3%!V!sne zlZcHKNSe4%mMlfgjg1&46HXE!8boPGVdL@Xo8%Z2vdaU%$z@bSdo$D<;wrH8q|cm2 z%iuaEDd{5r1Z*1gMiyV^W$mc;uUxgca_!ZDWd*{Z&fxHeNsE|~OQKz(Kz|=(B-usJ zj9dEjIdKc`>HLhncjV{?M~A*M;tTLx&l5lW_+!RTqNlSTdvL@fPo?{Rs{=Sg_-uAtWNZ7t1z2&fdPf--7v;{~lN(!`WaY>`lVf#svob4O-vWNG9eEed_iByMwx}{!m@f7RR^YwJ`=$+z` zy96BeVFV~dvSs-y{ZszvXZ(>*cs6hh0a=NOqzxCa-~47Ac4koi-?zDJ7~UU$zbFh( zT7u}rv|6wa@*X64cHYNPUOl&Zt|8tMZxJOC8j$qlS-xCf<6h&g$C91TCosJ^FwO0E z681*(fHBCg!-?k5wpVfM~Q^(5c-9Y6oE&t$+F{ z<2}+pDQcXw{)yUIH8a?(rG_s|ye*xgdU`q&JwCnb$cWF%|Fm`c%57^mZ8G-l(^uEl zHL$g2WMU4RQBgUM9%bUhd#duA^7pV~O-*L!wYAUHvpTcg61I5h67up@vV|3k0VVc| zy+FCN;?^*};vYZ!nC|IetMGn>0FgPou0x|ozI0gQ#*JSz^_i*9&Z%R}jDG;5mIA2G zuA8~Ga&2Y9+!nXd;y$tA-FGiDUc*0NtWAz6w`V_l@2ug&&H1{_$S*VMONZRlv};-8 z(#EA579t>QYptzX(<$&Xn*u-6m(5$7{cLB`#I$ew!$i@ydr?jv!b~S4*)ViB%e>=G7hxwV?I@kuqpOT@ewReVNGve&^Cf z%9uSy*)MHDvQTn3(oNuc?B{d^r@~=%8)+=e#(CSB))t}}j-_V4(27WC zO&}6#cwe!KqrQU>ZitpI!@?o2#3%h<$;fZjXo)Y;u zpdUh5aDN06g+L>qe?lz8XjD%S&KibL4N!bGxpqxh&|*4s$)`&i@0TAu-aZtY#HG<5 zOqK0dz60DjZo!1HY%E3U;j#(X>ToaRuo_*$9w@IRPcJb9A~920%GD#Cd0 zX-03W?bmFJL{4xL%Ij?BvVXDg-dniJq3!#?O-K641maec>+nrpG=$N+25^wULv(~h zGDI1q6UcNa62|PBUEp(@C;<%V^54GAygfW`)h6afGLwW27cvgsB}5A$f^>p(Gma9( z$+s=yMMV+>lkaOwSpzyj5R=?fOjVhdI-SjpC}TxqF%Ql9R@UpT$PTn_mlhNUD*~gYm9*ayX(a&mI4R-B^ zgey8XcGLrtGgtTo0f=scVrSFUkU~rkIuH+eugY0cM`=2qgi6X{r4HX1-?&jZ@s*FHUshS=-vqF{vfu3 zDBjNgWbzrtM>3A2VY$Cs^hB;+`WC|%Bd)P$jeKGtvLkQbn|xxV!6)+xGT9X4m-K&W z@?4IeD@i?A_PLHwoRe}{rG&t0nz1wqT(R{2gdh{SQfM5l&%1gceuyW4prvbB3JCi0 zYa-mh)IyjksfA1rp3HP4$w*=HVyU!l4iPg&l)Z4W-v7RleV7O~0xFb4m0}wS@fZBa zk7@54g*zTd-#uABI(pCDtiM4TQffrMrgX5+U;fk414h#h$*)q&K|7Lq#Laq1%gz+j zR^Qifp_utGyZrevrqut=pU}7~kl2D@62Y6^M|6pgA3Q*?cVv2yKjm$UFbfn6#Uy&H zfKV3|!gx8>bI_2n;flm#bGH@(+j^WN+-dl=2=q+CxOlZ;;@{gh# zxFsuEKV#*z>MFQ(=YzS#JhYl;VXKy|T(Z1k%ZvtOLLGhe)uTsW&3(whie}ANFwI@% zrnY<@+O1+Mmo5iWZrxJRFr$%~O>St8}Qk&{sZ{? zUoLN$)iA5Jl$Eg5$)(w2v&UxK$HWyziD#>oo!+^*Wnas_58h||3U+Ar8{_tj+jCz- zDl@VX*&}zHlmSGZB&y^myqE1YnSNmVB9Q-J*k1EvVfIKv_DfF~wlB7A+F8GwHLwkf z);*2W>)t^;_6uqo^Pb%&1P%q7VY!hkrrpqIfMgq@@h zi`L{~#VW?RI<i& zJcKVo&J<$)D{l}D;;&Bl4!jQt!G-Su2e2fa>d&tg*NXmhiZ>#yw@fser^s9a3k*6n1d3GbF8Mr?HMm37xzoAn{Ay$ z($&)QN{%*%?RLtkvf3b9t+v~x5+TYn4I>yOAA|hzCrrq9qWAn=6Pg^}QI@7%yPBN5 zyEH$6ygn@e zSU3Z2Bnz6HfYe8}X!rIP?ZP%n^vDw7j~J7!==Zp|<@3m04~X7ESL!3k{+Nm+ip4cG|{#b zaNXK=Q!IjBzyJf1|C*+Bx5tArEO|+!=2VzeLi$rGv;pK?plKx1G@T3bp3>nU=k9VB zboEA<7f>gdLGwd;Q`(i6EFrSg>)11mY|4g7xF!jtz?U@%koE^nh z`36OMH}egO_G0V$^7>g0AGtc+K5Q0VeIC}C<~WH2azEfC^+zc+I$lYFm(}D95Dd

r5p_BLAu#&L59BB`)Vtc`5zf(^6auU$X0O^jSUb@kM3 z^S0M+t6kk#xxI2*0l$`QHMVZrO7aZBS5m=^QG6eBR<575db-pjBn8cmPk1vSt7a~l zGk?L1c~hgs$XPQD+nJfON){9`1NQw{>qfSIb?thCo#p!)B|ye25k+NNw>lA<-MWWg zQ@g!(`@F`fwT5rJXu*q5qzBz?)qw?&Kn!6WtGH{Lh!9wWyDx)t_b+^P-YZ7TM7{2* zWlyommq6TrGsy7*Ypi;HfwAicy{xvRrliJLF0%#j8XiPQuy|<(KJ3Yc84WXDWDN}s z8~AJd7x*o~f*>X6Ilt$t&E;+T9gmlM8v$7;QlJxI(0dK^0h0Pc$}Zb$QgirC3cO2U zS`il87qUP^W%retk*9!J zo^;w}jR6f&#J-h~AEkeg2!r(p-AzUY6m)nhy?Qh-#G4#3FvK?K$Thlrb85vxMG= z;=?Yy5%q*8BGwA$A8Lo1wclB7jh+Z<+qo90wt)=dP7iJRjb6k}2I7r~h(Yu<5ZO=| zTMICXjAmjIq-PE~TILp!vx46H0_Raw5NJ$#TI8yOxhQ!8?75W>_Snwyzd^>`9b!92 zQJA%?eo6gequTP2l9%bJF^^E?&p23?D?W9CaO6b4MC1sq--=I4kqC1@r z%sq+M29Y+lPc-QT1?>Jk1r0N{RbXD(wiOK<4BpI}^~MD+RK3VJ8M{1~_VtL^tubO^ zOzr=}4?1Fd)mRYUs166I1>T!(fh2nU^W-yWU*Z=TQquY-<1IH4sh|1$%QL6HJe~Sw z%9;KKkK~bh|1-&7rky^0`l~NLKVyhU-dj)ZpO%ataX6j!Rr2Si{RmEwjspm2pww1T zMxdUMJ+!h}A|E}$VjN>7DLV$zM3h2@W4R>h|Dp?CYQQtZn0N;-ay;XeF&&SIrOc||#bpJM7zo%mglNAX)A)9|Yw zi|`{2Tkz8gZ{Vj9&f-T5uEviN#NY=92IJ=frfVzkI{>e1?*{b@8Vdacg^%afs&_Y*HV_&uY8 zQKDKAJ@{n66RL-j|Gjha@+CG~=y|KU^WdBS{4)4{si7Q-cYToRX2sMHkhOMsIH#0b zp;FFULgAYyAO*3ehmuyIH<1AoLq#CJgN}i)LoW>;$y$g2x)GGeMK9#|)d_A8MMI@w z;{{n+g6s?hAd(Y7Mh@^2q8*60;t9-~1>Tk<6(sE=`D`E(sH0Hw!ysM|8pwc`lYm;X z2Z4Jf(gmnTxFS@#p?uT3>JRU9Q_GlVh2KeaUc30+Q=w8Pi4hpz2!DGl64|IUA{eGOFE}CI4>~1l(-?NY%6Mnnl%1jI&Xfi&RCO)$xL4&EjV-j#Pyb zBq1-HlV#2vIdPofcI)%1<{_e^5=WD2}{@nFCs{2o`y|}Oc f&0~6oE_2;^JyG_5=y&0iW9ggm%ZFWmXYv067aL~9 literal 0 HcmV?d00001 diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.svg b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.svg new file mode 100644 index 0000000..0792c00 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.svg @@ -0,0 +1,543 @@ + + + + + +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/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.ttf b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..0f94acd1ebc42d7ae7cf1ada87d4301d73d64e30 GIT binary patch literal 25568 zcmdsfd3;<~eeXTz&Z3b<(u`))Hqsr9G^1rCOS8+eWm~pv*-mU(UNXv}XrwD|iEY*K z5{v^W0RwIl6q~9Ep~TM#u7Na+Q7ib=&q0OUx4y8cdmXBXFe&64@ zcSa*;A^m*b`{zlTImNjs)5{{a- zOh^)|MtOX4UwT@qeDl*N--*w_0er$eGlK7k?ZHVulN~>&mw?~q$yNu`Y-Ka3#Yru-G524(=d*7iu zKlcTx7Uf|{l26=zU^4wu^hwM${A0}j&3)-Rr&-$aza?qp4B9LE)BC1=Il+&i`~v3q z==6a*4*iQQl9QzED<#P=Iel-BM6OYc3gOYODQVi-n)zor9|M!W+^XugeN5hwwdxRLrC|QbdXR z)2Cj4whSd|RWBJN1*rkQD8KRg)vNfHu3lBN=Xk_S3j;Ea3i=fP6wg*%|NmE>6K+Vq z;4VPUEJ+c2#2$>;{k1=M<_llouKBlW+0Qh{$OHa^7b1lPtBjl_db{M7>Hv(e$J`P(dyIitw8iamn*A+~E*3QUDwvtg z>`f2lp1V2oeD2#Oo|(43o;y1|_DAh(BRkad=mekZ``M-N^vTzo0_Sf#{Do+DGI!Q~ zbVvQxy(QXxW0!F=agQ88n=%&3s@eaX$HH>f+(icRBW{$eppC#AoS@C51Z*vKzlHhj zm)LceF6F*r3AHD37pXsu@ZMus7~x)Fj2W#*?&9K6vf1|=LmUaac8rA) zvRSO`eb&I}wnU2iqsKybZ5!yH$Hx@GiiufUG|bOkG9_-2%Ad-u)=|{UXROj5oJ1Co9Z3M>}gPtV^`9aB-OzrkoRF5&##?Nk@lY z*2b2I-D(Te+T`(}PHRJJrbUS@U>t$pd=6_b+%JI8FXKZgi@5x3Mpd(_N(t_mxMZLS z=Rsknfg<`CO~CP#R4P@0&If>f9+S)IF$2HjEpD4R7>@>xg(`va!FVhhJUW<44IW*= z_a3EO?vcUz`oTW7y{``$o*LTmk>fjt4u5gWXA?(rPahuI@!Qmzq54o;{ZQY?_N9H> zM*4>8+d}mkPP3T;PKVu2Q&S-7aEBB2p!vvSNARb{Z;sR2MJe}(4oB{7>@pDLVwR}vc~k(gfL`dP(;ckK zE!9XZQV)(i?r~Rna3FT4$?t0l#O(f9#1Z$HeFh?>PIoxIoJE_9^6b7U8~Z!Y-Kw)< z{udQa_3ld*)iX@V{cF3|+wNtRB9RqV!E(8JHFv7Q>8xP;RaRsF$m3bBOLch{e_>r5 zpFACR;GBJ75OJ5^<;03&_Nd41vs?I=Evjmn-(dmA;d7>2`I}7srhw7!GMmi4CG6%} zhio*m%e0&D{nbq){W+3x_u7Ij1j@mT+3M@|g9{l>rvHQP7R z2b>#u_&{vSme_%ZN9Lbx>+2Ik&=(Jgacne_BVq(Y@W;%-L@?n=xDsYpVm8die)hB6 z+3?1Vv$GpFf_u-(%9Ya(UAdxH;4Xs%>3D{*T#^^K0uD*bGB+>*mKf029u4??E+8^e z*{-GR^2m|Us?Dc%&%cI6wXu*&i8|Y#O7+vC*Crb8+_3wUibpeTeY6##o~h$0TDqV= zg0_IeF?)o8R)h$Xb=pESTBA`!Tfp-iK6}E0ncXhCugTTn|d z(ToBh zO(z6oNoXe}(8dTTHyLDuk@lo*)A>h&g3MBxg+%RZJjLglu*NsrYbsf3?#SlEfrkVBJJ%;@9^l~BzvQog2Jga7qwz?%ktMoV zKMOZ7li8C%1rd)*22g9{#A)_u7pG|nS_GDx=`c)Yx6|yEo#sH$OvD8+TEPOb4kogF zJe~-4F-x7J!g`Ol-qgH)$NFYtef>RUr7o|n><*_9I052g+EnTGx@?uJnpdvyx*YW~ zWBvVsfl8ap>#p2n3LB}FanR^IWVNxcS1INJw>NctGI4#X!98I1y2@)!Nq;4P*`%}@ zyy#WtU)$2;|D?faG8%7ic|CU7CXX15Gl8bSkiQI5c@3>flh@^JsPrd|q*ZYG;9TaU z63`05ZXA3Evm0CNL06O6n4lkX+CF} zf7Zgczj!)ly-Tin(h;;(IGD}yw1xB)Oh9*@WOawQMU9dlu=mkSI+p-GGu)pDeZUlr zdY)*%&Np&a65myk$O>H{i(FwLZ3Ywp^$ z$Xz&9yN`ifPV-c5I{!=(G7+9JJ0n$25It~UL6`H->=yl5Zkl>78q>mjY=+H{dPL7O zKGTT#bO_kNg$-eMBlCqTAw=MC1GolDn8tu{iG>{2tfzHg!-i~mtuesn+~zH^t1Nf6 z)FoHM%S&gOk+~Y!77Z5bMZyDBioI;eJzrW`S=x5Kt%fz^wiXD9w>?ioNWL=R+{J0z z{bs1bK_7%u!8xGv0w#077qAf=uh`$9T#i0AY}R9vKyvsJmT-IeIsg58H|KuTKGd{f zv}W+W>(>YGKg8$m8$s2evi`>Eb@vTzM3n}kuz)*wpTm>zSlH~9E3*VZo)Vb_IY;C1 z#3Masj~SrWTFMHgXeQ1=$N@B#g@OB{Iya5SbetlSKhMIP7G@5bgLE`B3LOrmI!#q@ z_sk@179?3T_7qeqtWX_#oDCFoqNG;FB)}A1wlR}LlJB3u!m&w+B|&puvAQxeJDdMo z7%5+aM>LcePtXE8NhQDsVx;);fqW7ZHAX-PhyWLOPc+VO`WwqCau+MgHs=0iBfE@3 zMcMoje9H=GD5_BtcCE{1_3AQyvW$#-MRJfCvw{lfNvJrPfc^fL!&05KIW zC&LKHXq>&c`&37-<#|X>xnI>E8L@w6gKS_X?()uNv$IZFfw9B3y~pJZljIfdiHscC zaS*aHcq%mj3tcq7Q)*l^f6(mDPwjym4gqvI^Xqfom(9K|X1wuA7FP$vB0pC)tIyMa z1%emgia7`uHQBzZXa*rzi>+ycruuXJBS&Z(d`=s!KLx_H<6sf~lt~T1=CB9$46slD z#+ZlK!f01MeEn?~+QQ+QQ>>c+v@kB@F?Mjw>JKM(pUN(tLY%R-Mj1L4^amDU&{oYJ zf3PLuG5ZsA;{}TV_2wx-!ZmFbnlb$>f@Py7;Xx$um>Zd^5|=bs$^1)jzY0+ z3(cx!HbT}DQY^$-jbh7+Cgm^gV-|gMEnT(;X|vD;gal0ei73!si|1L?2U{hr6RdPv zlbf|(1fb$Rg|!(y!r-Ry5G+lfi4+i;hh%c_ebCh)cw?`$Tv{t_#to!%hj}or$yU%n z1Wy2l3QQGb0OC|sz?evaP+v(uAs%hk80d25gD5P7P=HS*5cY@ZvJ@A-0;b^#vpd?o zz-M5<6<{@V+z@dL2%_)OL+fru1MLQQ+rI)shiD(Bns&N(fXpROT7B-E%M60Gl9G9TiIBXb_HPo0t&;L$e9u3wBL<6G?kIoi{`N(sc8LE`inXUzSz8{RYuaffu$t>f zH7J8hRj4n91D%xzG(Gyx)5^oa9E~UXIB5k@C-g#E?ya3UxvqF|j+!Z&~ zK&Kw*Gu>z-z6Nkzd$as#vR^^a4K?w~$Pmuq@&-z`jp{Ftlwl zuD<<-(77QI!`x5cTIeU|DT`6F{%N}=wb#ziKvEGjb)Vc)#Pl`eHXAEq z?}DU;UN5TA=hen3)44BcvaqB1qb;6T@ zV#L?`SXSNGqlDBF`<2ql>QcsfmaUI&TvV64D(XOI-E|Rl%v4z#E^CliSvN(((e9FhPCdMpP~~{0kXbaZ7z?A|rjxbFkL4D!q0DiNLJJoe zTDusAwj(mMrmL5P{k%$Q!kRn{2EiR-(SA7rWeUV!vse-KuuxNZ>*!zt_>&kMZ7sjG z-Fsw&B$zf>+M3Ec4jwqQoG@wmsRIW)%5xjMu8||SPkn8!qI{4FKHr0@e&D}u%EmX|Pykn@V=m}_MpOz35KH&&WsdToa7@cc_p^;S>{7t0K_M7rjMlei%7dmCKb3xRKv(w?p>jrz8Ef$k!^NtgP>ppq& zI=fxAwsj3%uihi<5ZThIdatXYLa-!0=Zmde8ME80ZM!zFg4osAXma=W4u+dl%^+c_ zXmEM!t4al{)935twHjbk+$On-=1Sb&WwbU_nw3JYe8#qb$p2 zcjeeX$4A>kp;fn3d0jP?{kc=t+^^Wt6*kw$a^Jd}E#q^{e#@$kj?nSYz*t4K%j;Z~ zJ8fe&cGrpuccu47xtBi9mgVm-*`bJUEO3lsE}>mJy3WPzYlTgeNKl2r(`5h=GDMl6 zNPZvXvn6$dv5t=At2)bSMAp~S+oP~*kzdi0yC}-CVz4Dw?oM{yvTkJU&0Sidb$I2# zs)0?pd9Ao}La)$isiezg&^KWe^E*JC%pPCN4}2!-Lo^{Tb${n2?Op@AU~d#i$0$n2C z35#{AzvueOBe0(LRbJmiYA}j@{V7}3P)IMmMh}W*8!j@q@?r}aPEBT0 zlL<)|H=AHv4m6qFFeKy8Ox!CGhZVVtC;H`JfQ;G!Byu7E4cX%kdm3a|lk>I}HZ#25 z?Jjp)I;xBYxvbJ=a#Z^&+MOm=RdQW*o z-20-x*C_XyWQ)17)Zr=^XHJA1m41t%!ec9w%_f6=^*Zh^ZHUXJa`(PQ_v)q2YO}Mo zp)%xaEvu@iE_HcK#UUJOgi;YPg##oVv6wH^!JgrV*4QB}ks?w8GT91^9_IN2rLYtg_{9e)=lKYT zE%~gGXn*frSj2G(tfwdgK|mmZf;;fe^)KT6wA#EJP@G?o2@ot&kPnLUxUTd=3@ra$ ztXJme74!_8!e_*X(goR2^B`+-B9S(F0un$;0f&V=T@JspkEDIFUuzngJ!%&oEzM4! zx0*TltOGRRJdpyPa(Nx|dmX$_*Jd2Kr>LH7bdWzQ_qxOJl&-xLXa>$ZBB%|_!ov0k zIXmWap@fMcRSO#+$OtL<`4TxSJ0NpG4=49uSmcoAE}{nJa6N|<=jL_I!Ud%w%}r}* zzRYG^uzvNmN|}TK*zA< zv8{(86`!R%D%9g*9-EMRV2*@D4U#AfA7%>+2u^vVFZY$)SNh2DdSnEj$NF+#VmBbL zuy~E2BxG3lL0c~ceJ*O|7Ur=NLaKK?tz|;bZm0gphsAE1{ z1I$3&Oi^>W*F>-t(E4`ga<|g z13`}r4X9^ZO3TPWoV%#GfZ2@ZFwRXcF7K_XX>cJ}CF}53mQ}!yW-x*-fheKQ<>gOn zuHjkD;j0z0#f7Bot%7@vd7ai;(`6Cl!eF4+@S*|y%m_Js1MLsiH5k-d5NRYtb#0Sy zX-0zq_{a-ICL})r3e+M12HtQD4k(&U5N@IgVtg+Dw{S-6<4x{F`_iSem38n@&o(qP z?x^ditao{i@y|0fwBgjuyO>?8Qed%L;v(;Xgw_>(o zyVuL>LMYT%Vvz6K;4N`tgyQN(o1?mY=@*yYO9~g^no({*yg`8M(iBUj?J$VN0?Wt- z|NnRj|;nHOXEidE;uo}7q&=K|IWOS7rcV%kP#nlDxzTTo<(elXaZ*c?kO!x=l8&CNbD9Sx6rl5 z(uqi7`2rUtic=9Q*L@VA4dkaF5e4aMh!}*d_?vA@;UhURLd0Q49jRK{uBz=iFQ9lG zyVxyy5M{}88LcmyS-Nd07|W3n5EK|@fdB3A+7lkW!~O}B*hyj9zS;Qzw*2Ai#j!-iPZ2;YjZBv6E5iiW}F!?P5>i5-zh zc;!k~4dVj|G(V1j1#C3%3dJx6T?ii&!w@ZY1iNAUZ@FbYxJTa$D z*qw<&S8zD?p}gl=6U>Feav^LOOpCUVmCW(v%t!VsinZge!>5zSuM<+5P8Y#bf#n|1 zi!LEw(MDXp7Laf*PMgFRLO9Zzj+37DMGRz<^W zS(*Pwxx_60@`|$oX6y5+m)()-58c()3e(Ec4fXxxmdeLC;23c}B>M}y0L71z7^nq} z(p|y!3FozTfClWvnh>BFh!>t=g#7IUK=22FmeOLp6gE#pjQH@GA=DLb51iNRp90nz z{)%?GWxCLgCL|lF)=t;;U~3ij3}%NQOLlj%M3Xg@j!mqabV6a172`$hWDZ1RY~Id| zmJ55pohJBe8!YTy5_bU_DXb=d4^Y={j|d=XHa@bQkmyW0yj&!UEt13*Nl|m^OW<8- zPHVjSd7Qxuz%K}&g4209(Y56~8aEdl{(1y1PCjSoYcn905D|pjxAdI7lR0$1%Q??oBPta-gT|7e(c<(2l?EJ zRp&0%eCTtR&VB4vaju$8Ku{k)L_LroB=lB^-xVaMp`VSBMFzx1{NYPbo_v8)Z5)9E zr0_wTBlQDJJ!UX36zu&5n3~9a?5WHB`zQ3A!I`@AOoubC_N=bX}4lTBVt?HzXogIhgThxmBp0|T@(W^8li7t(C>V7Ru<3zG2u)a={o6?2(xq3L zcMUb$R@w&p0_KWp^A)cb>^pZU%p%cKQRkk)yN2I9GtAt*c*g@`%jM>wUCp*tw!sxm zC2m{Ee>RMG8ye)?uc9-NXw=_x2bE<{u;TzJ923xaVYwnHv4@VGb+kVVc{YZ>aw{ey^k z;J??@mJLnKKfCq^Yn$3~);3uIV%u6nAK8&okvn|uTlN)!a|1*DvSZse%AO0r##6Lz z5pR@_7n6%7W6P=gVaw>RMu!i){l)=VIsMA%)34+nfns}Ti>hupL|dm#iCAQcIRqXp ziZukW$Oq&(*eeM94?Cd2{=N_&WF@cI!fDJdizs8w$1cVc_(Zl+pa9O?&f)-xFkS~) zi^oisO*YnK%l&AIwch>s%MaX@X13g}fBA8A8G7dKU~RkhR(<`R2VUO7Y~R0hvF*ca zKP>R*^MWq=u}%<&LKM5WjdpX;u8|BahVEt|?2{P-rVhbrlaSAFBemFdJz_oE*QRMb z2-AjOzkm$S70OFpTejZU7 zmYe3!-((R*3Z1d!es0NTEv(jpl|u&*gOV>SZ%cy(gt!y!fZk;A=pPD6fV^uJW;p1s zZ^ zcDw?FS2FvpGQXkm1xux2ymi7z`5%l}wH^gsU z;^i(wm8q)CVe?pPYT~zcH8{(B?$+wInx>ZOPV45Lf%!Gy(hD@L5!S|a(oFzgA?6G> z6>I2%(ru>22z{@g$qS78ovd81X?aH*EeLK!k9}iTows~==Ww~VF4`HRc(_`ftAnLG zpDoI>FX-`p^dKLC7a1;Zk6L?Htmv@@mJW-MKP&NiVOz`+U!{_!=A&d-RMK$o^8)vR zCn%Qa6qWmLgs_SCy(s=eA5Z67u&k5cli?!65eSL% zTyC2Fv#G^&G8DK--Xxw@)V8W zA$k_m8G46}ywzcYy9(EeH~SGBNr5a#ajnj>wjX{pdnWgb$6jH9>{stXY=f+P^oO}0 zoqdcsZAUb*Y5q@dwv0VhjAyU0(AJ7SFBlFfjFSV^Wsqy&tiZCPPayOO&or2nFJ%Ax6@mCH|WU=_*SYu5eAa0lY}ER}aOG`{im zElXmtC0ky4y|Lkr`J4IXV^5vl(A=n)1^*-Z(p^9IOr;n4j`_O&5W*lgcgkw0P;7A<+t$7=kVmUJYJB&iNKLcYR#?Jf8UCi3}c-IBo&7UFJmBf4N`Qh>>l3(AoQJEhThXHdQjB?k8U}`}uc(8O^K_>xz>mCEd zsNm&0SWV&SPIx$6*$?4nk}QZj3gi^D>=D%FD~fpuz44FmiADN|7Fda!K_SHSBGBxR ziZ?Pg5$@*SbJ`@T5X@SH^YFYtDhJ2F+ginrQu3qvWhLC#HcM{NY1j#7+xqaX2y6)S zLM8b{d7!UN+ncQ0WI!vm7?ZH4N(R>Kk_IQZOSHD8!k0VS-r(40%Z(FIWmAjuks3#%*JVAkVSOW;sdv8Ch_~OTwRvEV zA)8Sn>8{WS{4RR0Lx>_|Rt$<*Cr$N;`HhmMhC?0&a;agrzpG7{4sBy@QbQiU8==9u zA3H1C8!9a?-hy*9m6VLx$oRN>NvLV%a#Ba``DT@D%{}6%Y-q2v%og|#)*xa@gxwU% zk|3nQS}I}%@jejfr)DP=Mp9Tv3vdD*tJK~<0CRov(Pp$t4J}UmJ(Bg zTtaI?EXJa=^eIc0Pv9%}I(>=xvsf2sk>#mU?HlWP9`CK=qgrl)KB=ly!rPBGAuWah zy?>v4rnx0qbD#i!iVV(>b_DE zyu$Q~J8?^RL}8@!l4}&k|BI>+-?UH&%~m4xX*1*{oE%-nAe2R43}HxQwj>_}tXhar z@(0Y&hK0qWlCez#11E^Bwge_V`q2r0Yjy3a?b}z$)gHI3bhP_wXj>%il~$j9_0ejJ zYO>#U?`?LID$ACruPJVkbsQu+y_vW|erDslOmfYb3u6cwO85I0M$p%$uRz;lfiIF> z5gHJKRES%&oWUF?Ll>YAi;ckf7?9}=P?-e`48Q{sp5NiXd#_s@cpH@@3A(s6CTI@n zx+PdU^!+OM^GKNt8Y!TbGV#1={ZH7LFURhC=84b7*s+6?v5nhr*cczbPDt>4Zr{jF zW2qPWw=Y?;y&skw!7w$NfS5@i#;=u{uwo4AiU=|Wb&edbN7@;>hI*^F;+GjlGKp?~8n^erjvr-tKSJ?Hp;oS1aA8mH3>NzH1w7f{2AU z2hyh^4*bdx3B@m7-4MSK^`8R&q~f0(hzKR&U(8*EQAIC<4dLd2>Ja?%ex)GY`}g)N z37G&IDfy)F|8*c|gFhggCKeH%rV`^W-P5$oU=*IFZxW>+Uumn7V}eKvEFiCw0Zlcp z(=OReUZ*`^3Vqcj^6JsuU1+7_#lpHeKKQ!mMXwLA9^r}Ad|>alnm7?=f;T>2|Nft| zTPv6SyHtjohM%JU|8)jXd{}tr$b-x2DSpsLR<1rQ4apy;qTHmN9Od<&pD{)JPb!Ha zI*CSozYpZT_k3RbjPk{0((gaWxcNTC?elf-*Pq%K_bG0B?OKYT{lCn=+q(00`Sb(N zkJBq|`(Ar3Zu3F%ziXWLS|`=N;~CZ{>h$zo+W^0c`xWPl*9%&?Xut2(uDB27@xI2Q zepL5C`(8WN|IIS3VG*p0&l7q5rwaV13ij4b(q8F=bWZxF^mA5^0Ha}ckey;rvTv{- zvtO_ruj0)-4xinF{AvEC%;in;w0u&2MgEPU+Av~J4W|re4bK^VYwR)}G@dhcnZ`^H zn!asznw!ks=6>^a=Fgg+Fu!Vk)BGPLr6u(x%SzOe&zF4LQfgUdx!H2RC1d$bskJmv zy0Y}f(r=djq|8>&?~^)=arnUR~Z*zODSu^2f_xD8E?#iwZ+U zYsGlQ$%@A+o~!tA#cwLBD#t6&Rz6#K#pbkyY#VI%+a9-l*S^VqyZyUWB~@KjYSsCw zpHy9OR5`jGBaTy!Z#(|kS>kMU4ml4wA9H@g6?J{t-Q*r|pK)j0ueyKfsq(}L;qRHPtnNnj345)O@by+qKoT-L;2mAFO?`_Lp@^ z-O{>|x)XIz)LpDM)NiZ*oVUt*o%fLUe(z)6XT5JW)HiHuINmVZ@a=|QG@2UQ;itH_ z@topR)+po3rK*;-^A-qCkfSaMDEGiB=n>zt!{PG5=qBUY1($ z+Jn}{fS{Kndfq5iurKI&{Femw5;!R{kX3{*O3O27>3^f=IW+zMq3319g#Aj-8>A{W zq34ZKJ-b`anYtfRN21&(4IrLPpLi1QYF&e8SM;r_n_H3N0bUJC20p{IS6>{ z5sOzaGC@&;q_kt|;2nDo>{oiay0Li)O{UR&KNhiQZwIEQ_IKttsAHsb^&a{^sPFRg z52#}jXPn8*fdAfMf`zAqSy(A6lh#OsfY3T=NLtUVtQ_v}N@hbeNfmQ2Cv!13Y$?^O zhSjn+0Rz6m24FoV5`|0Hptepb!>>OXB+T86R%^}vrTL>sN)tk!nU$)Y&+Y* zZeS@k%5G#gv76Z~Y>eH?##x$8ut}z}DYldCV!PQMwwK+;ZfE=0es+LOv)==Ky@MTM zhuIN!l-_pUF zX*>Yxe42fh{UMuWf5aYS53$d&huP=ZAG0s8N7xzm-`H99D0_^3k)30I!p^fVNoUxX z*;m-(>`&Pf?5ixpE`YlK8T%T0iapK#oIS(-f<4RrlFhNdV$ZSX*$eEi*^BJ!p#Lwi zZ?bQ(Ec+YwZT21ZUG_42g?*2`%D&Hj!2UbC$o`i7ko^c;;_sw)0+si4b3|YFgn3Vd z_e6P5jQ7NOPlER(c~39zN%Niw-ZRO2RNgbidv@}0HxKu4c#pyn9***GjECbqoZ#Uk z5BKtLnujNNc#?-z9-iXiojlUbBR%k4@<@b7qC67gkvNYecqGXqy*!fUkqI7|M;1dmSgsLG>LJi3#|x_PXJ$HF`o zfj^YTVmub-u>_ALd90Vm(mXc7W0O3l^4JuQ?d0)p9`E7tFpo!gJj&xS9*^VSIq`Us z$9s7^&Epe1KFQ-Mk5BRVPM+xIi5{K^^F)LvqC64fi8xOrcp}LYy*!cTi3y&VTU4^M`9GQyKlo{aHioF@}JndHe{o=o%P1W!)#q{@?1Jh_wicJtmI-W%q< z5#Af+y)oV!=e-Huo8-N{yf@8zCwT89?^Sv46z|>1)7?DX!_#4&j_`Dpr(--F=jjAb zCwaP;r_($=!PApGt@89vKGDr5diX?`Pek}cluyL?M4V3~_(YOV^zw-`pP1kilYBzu z6H|O*C!g%*lRbPg%qJs!GRh}od@{}_6MQnsCwuv1nomyf$w@w`^2sSaxs$8iTL3* zyfBOs!(asDHDHzAllExB@ ztA|q=qkkY{@(GZZ$t|hz z;kxw5Xv#l|XDhd+@Su)XuTRyPF@!Sa)g4cQ?5-Xk=*$>V^bh!zjN~6kXYQWBWEn$y zXT}s#XhnQ*GQ$QZ#@C2Nn6*l&!+$|Cw2Ui*C;e#xn%JXMM{vj}b(k$bT}E#4r`Kqm zOF|cn#=#6ruj$NKLKsg`GM3dF2n5LZ2SzicR2o646s68gX-Ij-AWhKMBqqv~tsYlS zjw_imthX~$7TUNib-~Qmj5cS=r~G$zW~`x&TT&ahYL#_9)H_6ddFX;J1lkik(5oCI7kt)<=PN4I>wPJ2#Wc#C*C zifydJ;@6D>ehVPG0857-TVI`#`k!Qs32;|nEylrZDJf(14=Cdp_o?!72J7a)z{&9o z<;IT8zK*&kY`zi=D>^zew$KGesXcUoQ(6_eAXDlHT`*AU1XL+?g)W#Vb%!pPDfNUd zlu%k7x?rKSCX^}Z_<(cQV(uFBsl(iq)?;o;y_lQQ2Fy)qBj%=5!Q7PkFgK-5n43~R z=BBhcq%0Ho(-Oj#%g2?~0O&X!KpJVLx!u>934}5&9hnwjUJyvO4!Ds=J%2jsS59tE zU0aD0?##3n;K|&XV0(tSx&`>!inqJ?QF};QZLH*QIOKFUh&uL(l2rr)2u z(9T>~RS0{)3i7SSfk5eGXQngM3Wx^IQdDmSOVZl?iQs>Sx0van_hqN<|_K;z?%Uuk?UYMmjgt>ux_!CVsrooP>lU;sAS#}a*^(;JAy0kGe1|k%L9ZF`L z?$OFEsXyb2tknG({?k|O=m6d95>N=thZ+5Af!M2orMN{>heoFO>T%VdkyocxTwcC9 zU5EVmC~g9pr74j-O3g$2d^!@mkVVeSFA1n(O`VyTKI7^AAF$3uRli?+WO z^l8~>*M$Ic0<#l^Y5;$sI$5X|t@B+MwdoCIx;yeu77e;IliN(>`o&@s0V`2ci zD-*!{eF7qcFL?lFfD3-0Y8Nn9TjMg^Mo_Z?V&%8^yD?|oe+^j(I&@c;`IB|NqJ#5| z>ML1JTh4>7pLXdZ_|ii3)fP6k0vmH_H|9x+E{CHl6T$^q`5yJFKnIx9k?BO6fly`% zlGOy8K|oJg3o@RE#u^X?+?dq>-eBlSNm_=?T4Wey)`gyAqGkvgQL~=fEXSh_)P^z} zsSRbWqc)Vep4#*xvx(YJW;3;+%rLc~%ob|Xhs+4Iq0CllLz!*VhBDi!O+PX_s10Rq zpf;3AQ5(vPQk$j7+(>OGa}%|p%+1t>GPi^>;R2Y)sE~=H`c{!kAU7_s2GvOv(xFVG z(0YOjqV=T6QEOG?sOeNF6D>5|Nd?h#m&j4m-6BU#_k=RBLesre5KV6rIcj>l$WhaM zp^i++R7P$dzLUszCn)d6XQb>17%{tl^e;+aKAu!SCD&wR|8%BeP*L!>V#~&iY3q%t zOtdc3Iyw%KYdffye$6K`+`k6$k*WR3(wzP^`lO-NGB~p4f{CrU(9AxyC6!tEsni9N cyavdL5`+vYLY% literal 0 HcmV?d00001 diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.woff b/lib/composer/vendor/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.woff new file mode 100644 index 0000000000000000000000000000000000000000..793176af47c13605a36275252d9ce11fda817f33 GIT binary patch literal 12404 zcmY*1BH0K$K@cJKf3|Jwh5k&sZ62LM2rzE#|B;M>TJA}XpfvV3cD-(2Dw zhHQ|^#wK<~j^A3+Hy8P?MF2<#;U+&_iN4#b-x~P;fD7_wVdV0kZ}Xc&|A((H0GNfX zr}?*b002;C0su0Wg*lh~mS#q#006cBw};^y93V=Tn3mt-w^sYj3BN%G83hhvY3J(k zt<8RO6951#G$RLqYwKY0?W1A)9&`9V%uIEO*%^6!AD8z1+eh>tK-d83_C|JQ0015S zx5pX)0QI{v2K91uaB&3y=)=DG#rJ8OTq3i6dSsax8XB4c0$H0Qr-M%JFB6XjECGs) zP!RyYe|<*(Mz{h3k^=(5gU0*=0`dVOtQ<_GtXWVIEVV4Gy#av0G323eB{~O29(A?h;2T}qOxfbLOyUzy`5`t(7G+5{}$ouOH zOgi-I3nML`1_yf$+fWWtf`X`T&>t2XCzKxm!1ILi{JyI1`TgF5e9tMUM^>^ok}KX) zdd*c7+kTzdM;#mP#;sI}scg#S>g`Fc3Ar;Wlaui%FcDn17oEpWM5`B$%xEFFzF4-y zSKcN|z;5j+IZLRa(Q!RVmCLsh=qa0OHhm5w>+O@Wl|>KN;d{xOzm-zGLpuV6bp3Xf z9WQ|h!f|VPm7V_f)v@#Z_Nj+;eCFDw*K=fwb&dK%RFJ6pFTzZO{fa{)5IVWQL5a@k#+fX31y>Ua=cf4lEkv0vPm@W7=*U~*SM(g07(2~c2$bhvoMK2UBkQzA{ zbjzp5Omajl3^|r?O73!0X^(wGm%fjwQN28tpkH+88bBSO2@$IN~hKpxxK8HT*tlpt&7vGy8bRnU)BkZ=gqzNZu(0L7H;j? z%V=f9ey2B3f?mC*f8V+5i;Mm`U|v{runCevRCRDWu+&54RnI{TF>-8s7FpVRPkgEG~VlMdSGX~3XgpT zJof6m{Zk1o+XPE{uj5DH1O{lo6|uG`=VvV_0c+P02~Oo4f{*A5p#~F?>mSgrz9O-N zKG}nH+0;p(K1mQ{Tpv-=06w4Zpx)q})pO|#_s{ozMqU9sd7`la*=)o#$=YVCaN_(_a#Lq-{oRUH_1J3yY z+$V&mw$BjpolQ_5)k24~eljGi{M0Nmmv;LTo^8X}IeAFFztktsba7$ol7Z8z4HZ*M zhcbP1RDt*!VtLmh4rD|naBYo$p28A*+T&)5V-?hB&?HCNL?xNc1O(=m*p*4VWyz%+ zlgE6K2zNL=Up6{=H2ACq{5+g@I+80WJ0MwqBo?1`nH2Lc6pvL;8e$bfuxJ)S(4%6a zmg!LyU<6s+jwVX(Dl%^B^>#00y&8xT-#eT@9uv48xKks_$QZgy%Miy`Li+k<+WNiO z-M_YsZQi;R9)uJ8nbiHc`STrrCmQMRA=WfOj~$e!@k6lZ1Qpqj$M)0hRK2QR?)p*Q zF!q+-vu*E0I*@!o^j=gym==cCInPeOFL6DBYW)@HsM~IVka>z> zE9*IAK)|_0fhQ12z@)UEp(0vvxx3-j1A3Xex=f1H8QUuf=_tvNkIHE$+4YCc8s(`} z{S+haF?o20#Jn;x`xV&NA^V~%Z^Zo1oi7Udg?$~kV&@Q4;7AA|k#i;}_)rO%CSCB| zSa6(N5uda+`rr#wTN~@3H=ilYNS<#oKl2ZcABU%6Z<=f*$lBxvoWWD);ZAb9-HXcz zXwLKRvEB2Io?h*pKTUvE{L%$y=|?l0w~8DaBvAV>#Q02ugnl7 zpvuE2tXY$ye(FPQlBSP60cvWA#?&y+%hl;lmS*7(Swbfb$#J@S$RCD4;*6?6NeucG zzWWmk+43MVgv9ol7*RS4ej0zC-LAE;BFOJZ`;g-9X>F@spxz)E$^H*Qiy$ z=+-FPbLszU(tALm%k4aLm3|+MOLtiS7VOWJOCL|ji9&*n9t0#-9|kB1b$xQd!Crv2 zLaS#B)W~2c&L`ZsaCsVK(8H;Cbw7P=3zye)%`cH+V4dK=5s0lXk0%Bf2T1{Tc$nA? zn!oUV@T?s~CfAS9vZK$~Boy%bFBla`M~l7} zRh)~PMDzR6={^1(DRSVdM&rrDN6(Rd$Sq->9%;pplv7wzI3{^uDLS0?dQxt$8tYF~ zOpInH)PWmu>%y=|;;{JI(mNyOWX$-sUi|1pwO{_Trs1^c3!-8=Xm_4KjgH_PySL^? zkM?##d#jWsp$T6jq-;d_)xWW&&b>Wt77ML3zuP@T#t#=U{e(0~KM4Yl`fB*la&A7h z0k2NFEv@7Q8r698`kLVq&0*!6>(o&cA578VR{|}t#X1Qq4-^Roj^5z0$B^3Ufj<7Z zKfRAs!WT{JAu?39e79iK5hXvylokK5U*lI!1XjR!K&Wuiy#rvpyn?K_iD^)P(#5Z) zfeGC1Z}!sb4uquPW<#uN2eQSPh!?3v$88W+hY;Z#Z}Zd(-htO197%{TB4kATi~;CP z&pvR;$gPpa)nteF&a)|~LNnk`ikOVZ5rnkSojVD5C8>KBUi%$RaMGQMB6jBS0#H(B ztJq!J6g#u!P3PJb#2x&t7f(t}eRV>S)64MfoMTw4q61};LU@g7Imhg*{I=( z+$_JXQMye!nIW$$ae7fYnxvuz*#-TfO@Xxtp)hE@3^uF$%f1LL10b~A+t>Sw zpwDb^nOGC?^IH0|=m1?pkSVD#4uK9KEFjpGB(463jsr#5%H#OYe+;&*y0-<>SAUs| z433>Q7xy>|wm3TD63V+ebd>p?kBw1#3?4}px*P}>>8g>b{Lru719!&ADBZ`W*jJXI zDIn=rYhZk}?;Nl|*a;>Bo$rY_2dP<05bCe4SS}G8Y)l(2?$dE}y4lV{YvPHc^Ql}^ zqdCbJc!gh6SqPWV{*|nNQJ%^uGGY`5kex61F>z(R~_6ego57uTa+%|L*b;WfiZElU7m)42Oav?YQ$ghxhr4`#uU0zQN*0Z@|&*P}9ugAm!V#C*DyB zm>ET%QC=EyqNn*-SRv79iZrwb02U)zbOW+>4%G8>_r2Hal#ht=-u;JUNsz(TbVqpa znDr7Ma;@KEmbi_y+EE#YpK6D+r}Ve`Ip2 zCeL}M?L7MJM%%oHsunYB4cY%3x2P~3VFiIc_!YnwKdie+S-1fXQ<9jL30aW^{ysD<2cx}b#s+W z!3YNxNR}Bs-eJ7aNyT_rV^p#9T^}3ba$Y%qiXr~SODzkN+9Hqe$rNsk3YRc&l`~cZ zL0>LZXr+>6mEBg-(3@bH93jD|%ck%gcSWH*av%o^v{tQ&E)hZ~KS_le51<@h=n_R2 zP+n7-n-_(-Ht9g-lnygbCYKwdsZGhnnaw~hT&Mq zY8$Qo)CvPlM5*vAyf}fgo|vYKP4>;=EYOOZRZS2QJVnRq4@};(qe$>Wfmt}r6;?<6>&vjS$J3g0(Ze;AG z^6)Cc{Y-qZ-_Gz)HyvBB%VvT{Q*Oi#_ArVdvQn2kbZX&(IIgg_RM#rV{n04%9Aeg0 zO?l3V7gHGh?EIPR``!zIpz6)T3bUD@G+K07d~3uD;qEkrwQG&YgnK!1*f)=E>b;Gf zcwsThP4rr&sk7o@R*%J+b}&vD=nEEtH{TVuJv{3ko06a0jdZAN#0VtFkal>{6|D&A zKNhl2BG)l(hEU5h*#AClccEq2D}J3s95R`z$UD%C{X^+KF?%X#%Ux>Vr* zf`NC}os(phAmO+o>C7PuS^+}N^NdXC zJVSikJWA1w%q*cIT+X)#y8bC_qL#)V>vI{&$GqT@H{7veU**rpYwnU={G2|Oznx@B(E6P78Z9M_cYNLE zp0~7FJ-#4mD45JrZ(`s$X|0e>u;5r}cOY&w>}V76>$t9))E3fd+DQQ+%VSsmB!fs6 zlhKV>^{k2EK~-xd0fH~sITsdUB}i*7d=JXWAk%B6?n7?-kv-4GfIgJ>J2zD!(EhKIlr% zA3J~eT}IGvX@9PASQ;BaW>1{6(?|`3_h5zkX%D%H9tjhF_Ya0XpEW3WH*(xsQw)3gE*5kIieWy#j3rmLL(H=MSJ%<(r8_`I<}#n0Z1%wC^sK zPv(6$bJ@1}jKL>B8qH-j@C>uv#gw{9nF0d~8i8t=d|5YR?_A@&h}X2-J1x&8?y!AXEm&~A4Az|M(OC%02Augl@EW>j5!@yDTsurgsyU&4*} zftmK!2URM@%Y>lxjgs+PfmeZboSAvT0w$N9qC$PER73cPzaC-@B=Ih^0U>q)$$p=; zF~#t3(o0r2M_XdrdIPxnURhX5SR$GPCr5^e0()~xik<;e$8a9$=>w?N>MmunBrl2O+Tzl9}do4~qOg+D0<|K4LMGG647RlDJ-d(%Yv~o&|o*YI5 z3j<;=nnfiR9w!rqLJnIW6-Lz+5rTd-X`HV3XSOsu5YIp^Ahc49jeI;dQEkhr%SM-M z9ycCxozK2|2#73l>15!iMPw(x(p(||R=EE3`=6(FbTyu~s1zNpt=(gA4Mng6ipcxF z)P!?>AL(84xgGq3^XluqI6>+*UzxlREC~0T@+!T0Zk^qb<8$%)PSulm=;#b_Ft}>EOl}wiMQupG7Us3p<$J3B9E_nJjZM_P9aI zhCfG7hqUG?P-S3xb6jWN#rq^o~W6!H#$=dCeztbmq`6wGaVEp#1n-ez@{|TY5$7UD<)Wm60v()Qo878fSO6D(HE*Xcf0zOZ*hakU)rwM*r zBk&xgpCf@`3j|qte~ZwHgq6wHPR0#eK(bb_@)-XDZIpacwlPRox<@pY z1`1aw2DqXOpKefQCXxb9Yy1%Fpc0 zfoAcK#=WS8*~UVSxB*)nnH!owf5#SEgs25T9O-%nN2l~RbawblWEiqi%Jc!ozwsX7Z7_U1aKxUuCO8wvv>vSoE&W~N_Xx%J zls;aNtSFJ9g8p~2q@`V#`i?dTg25va7m!5mBPI?Dy!+qlJOwN$VJIpe^xf>l+m}Wt z+-&|K-Z^cd6_bA_SVCD@$1CidlaSdjF40pg6e zn8(-p0593>8nl)|jS>-rJJdG8-u8GW*J@_DD?4q!f)SvhwsLq0Pc;UG#FB$Pl~(={ zpJN~{;aa(2)vTLrlHQ=qmK7MojYMk&6_|{F;8|J!O*=Or*^Il-=HEPH&|9WG+YQ57 z?SAH(M`xX!`|!s$Zf1_X0~2*k+sS4LDbe{LNhX067Y_Sted{h>s`-L#Mau%-HoX8$ zKEPaENjZJJ>kv(u_eH8tx*oEtVpXMf-D$p7;x!w6QtqA1%MEM?-nB(45(;~~yz!gl z-2>B0@@@XPZfNyXKlARn1;)%f>5kXdo7&sXU_;_7ETmz9l@|K_W68v6=m|fyt=?5m zV9#9JeEiL<_J+?5DmX$cO79rz^qytEXlu^i6D()f%$9@;t{O$8J6*ttZwIY}0Iy#` zf}ck`f3$a3pKs%clfpl1wq}cQRy@q(C^|@uUf8LnplTA1CEAxlHL*Es;bmmbl%+S5 zRO;E}05<%)@XFy1=8RIF1_!6i1qi8kp1NS78c=F3*6vuccRsv{HcnLV)!H`q!|JpE+k-U)>Av6iFg0#!Rc4v zcn5vPbeDIG4nrxzKt%Iy#|!~5+xzC~%Ko}HeBh5ntbwd@#t@b;1!yvDFJtIQcVEEh zt$x%~enqrnd;>Orvnih5i@k{d^skEX92%XyazgEoxBcX@?Ih&sM>jz`kmF>({Y__AJ#EY~<0JJ6%bT9i}-^ zbr0HkM`Am08YFM1Bawj{b53&4af-RZbStp)AMXUqkGJV&S=|rw_8h}CRw-nYtW^A_ zHvApsOu8yYydp~E6!;E-mjte(0B2^7-t|>GzQ|Shb5(Da41_R_zD%wIAC0XReR|9vyt1^J47SA*}J#UkC9m%rs}>f(F$XRbS(<-BhwY zn*xsdd~r@iFo06iiDr-2A4qS@=A^l+ck47=$5a-3Fi~a%27xC|Y{mlv|=AkrnM^kq-n1dt)gyqL%X`})To1| z7jGCa*DSn1yo`{Px^xQsZBmBnI=|?4CXKzq&76vK0BW+J@v;^x4wv4SE*6Zw*}B*} zK2Itmx$lR?4Y}S^`J^gWiQuJBbX3xwB%AXmY;p-W<|A|T-Z2W?ic7b%!eFI05b4Ee z!Cu?+%dCC%nb#$s24>N(eqWO^Z3D?rzd-QrNZep#Yd9V=pX;J^&Yr>*@}3z!Lcb>Had&qZS4iHx=r>5s|~WY#YxWFLQqF#KsE-4oBQ;qiJy$5a> zqb$Ou3{nzeBbv|VPZZCRU|lCs{GRkHW=7fsQnC#1{?X8HJ+fFKV=3Vra0fGj`UeTz z)SM%Bd%2+PyIowPPc#!~v_PVmDH+ClfSrpdHrKb1G%+Q`6r|gF>A-W}y5G)xzS;Vt z&+m4oe(xj{@_fDgxb?ijl{-wL`*|S|%|X}*b|-SMq%^uHk_UV*v+Jov*CoI~jAk)! zj%>g3Z=Q$#aZg0;Vaq1vg@Gc>pDqnPxgse3kI?I=zmU?N6y(0w;gh)qL?+`oelkGR zPVm>u99)O=?w?dl*4(xu)=4bUQ~oxZi0rH3yfnmhUDTn2DfEj%9Iz*W9U*qu*4bDKTm zG4t|oFDdl7`A!$2qeyrzchzKQ;eFA=Zk)K4wx+Jb#g}EcBRnjBLO>eIO3dmaGvA#$ zPh8YxShUaMKS@SC4_7aH(K101VY{Awp)#m66)uAcxqKz!78dzvYV$)_M~ymOrxN!h^@LC7{I@ z0-=e1D;5hUN5qTZrJr;T2p;7=_#JCjZK$qQuzI&kTpPrBdP==F4xvJ z7|y0hbt#;lWJB;^TM#RT{Zk80t^*8#jIo1XvH_c+DwgfK9{~WQ`ADQ% z@{H0wZ!F@u7`=guVS7!%xc-g93(nY4(Ij|GfCB{HmJI)#Q{rPB_p;VyQ>r$ODSM4` z;XKF^*kTjCl-5?G`jb429PR!3ol$vl^{ zh6FMD`cy2+{xQw)?b**)ABGqKq2bH0``3{)sz6mH zmI42*ui7l`fh}t^q-ab{TirY{A`RGl)Y~0GVMeN8DWkQeYkt}hWSv{D; zpD;})s5$D)rQ&vLdPQ7~<1vS(XIeqSVdazh=#5Kan7w#^$f&~160*JER%^RCtWyeY zR-D27KWknKg>I%sn20tia{YfRGhqCHdw1JWU&`gO-PQMcLa`YG$u*gV|2P+vt?HUc zgDT#|I@-bYCtYX@LXJ4}=3&~NMMH4-6Tz1#W-b;nkh^(sH`+#`3#cdn;ZHOiQn0 z$%JO`j`5I7gW=ik$ZmyhMiiSxX%3ZEx-!FoCZw=hdISVOO2KQo%p*%+zA&+1DW-w) z441WFU{Qs++G0=I2PGhN$slS!5gxDS zDnyyIkteXbUG6w1Oo$4qpvVZk+9P2(D>GyJrp4&O;up}#Gqy+mmVNY6%iaw3bjkpY z0+MUdn1Zp9K-EDm&INzm?<*mhXXloT3v$E8$s%gCdcG9-G!1X{+`s%V!}SN z?O;SafjvXG37UU)%4(S8ubTYT7lsaH4DGP>%71iM$p)AJ!Os?(CQXIXgcl>>&}|vT zMC|$<+2H5NSJwVUW?K9!%T;>=!%^do`-ukDeAF(0UY@JEH&^t z#nh6_FSDZXA07!B2nv_SQzr0yxnB3EdG#0tUOvk%RUz`^*O4vSXT&y$ek|(9m_S%q z9ndu-s*s+RyD4~ric{?70pw8XFKJIaEoG$+4v zd}0-k&G;v-`4gMoAiEjaO}hv4&#W!X^xCA!QnGlz_B?r-z zX1S*5R=4L>cZzw-jjoETsTpZVci#6jhg~GQ9seA_d3=tIdCzR_{V{o`tCiqMVtY*6 zN1lGph-imGZh3AtdI?InW*U?QuCB?w(A_lNS>_|Rp=irJA|36<+@tuam=)B`e5s4t zA17peEicz-jrfbzS3O+VIpl%Gp;9Dk5HseS6~ms;x#wi4OVs_WL~4{p?Q-E)n6)~y z2~sKzZ`BsRr-l~Fw{>35>#dG-2Tc;6XCc4Nfr$Ra%#E4OC$WwqxIW$<9}A^fPyO*B zG>CBb)l+e={!#0-dyweL@&$r7`|fyFZ*VM$B$|K5(cPgIOH#gh$Z6^;QLG~DbI$8S) zTn}yMcp&6UznC?}47~fy!nqg-PBNIygH@rHtJ!zdpjenOpaosWeFDbjAwaf&LLg2< zO>%Y^pFPrqSK)DS^*9gWvUW&p(!(s~YG( zk7p<${KJrT8XO2F)q9@#z!oc}4>!ZzM| zzMWv-^R)d{J#ypejXZH{2MZP{>k98V8x9hMN9BC7+1-DJdtDegnW2l^|5v(=ZW!?? zXne47^S{iXsi7fYFOW>P+T&Md4gBVJC=ey|zhEKh$yY;qa_=5Kyd04-0977l|NpXq z-+ql3!OXs(wh$2Lv}9~-9Hl-{fZR{8WX4V)y0*;1nKYnn<%;cN%6+K}L=O(bDI2YWV%P|e?!$p~9$gbp6JUS7 zigW@`vN4tV+aZ#tdI10~BpSN!SRsJsf14D*91sY|0dxbl0iPgjAQB*!AQ2$tAmbpL zAUB}kpqQXEpnRYTppKxmpwpo5V7OrBU}0bdU`ODv;7s7w;6C8x;O*ec5XcY`5D^gF z-)YC6khPF|P;^i{P*PADP^M6oP{U9sQ18%)&?L}o&}z^=(2dZ0FeET)Fs?8qFr%=r zuw<|Tuv)Oru-mW?a8z&-aE@>}aO3d!@Lcdp@UHMV@S_OW2uuhP2(}1u2!jZ#2p5Rp zi1>&?h#rUqh@FVbh&M>ENHjY%vQ`%%o8j~EE+5Yte;r5 zSgY8C*euvW*eclO*k0H%*ag_N*u&VnI7~PKILn75 zRgcZt_h7m)GH3TmjTgQTg)mvd-)?cbQPC*>>{A zW~HFp03DTedQto?dg&&+SEx_Gy|3%+7rTd$`&D*_W}I zTbiOdI^1rfInnM@iiL8cXu3@O(zZd7X2~)C_-1iFlTKbRGj)Q{C%Fs`^Dt~61uqyerhQm%v?856_F;6knp6Rwnft{gP3_(ZOBRj%X( zu54DW#0IWR7p~NMu3Wg*IBmVbA-#TEhC>#HW7z5gn(8Cm>O-FDV@Uf03i~5W`$G=< zW5lZihN~mOt3!dSW00d8BBWcU*c+rwTE-%89)~j_XUeNhYGamNnTk-a%rR!ZMzS%Xu|~l= z8e>N3oShZl;H)q`0ntX4dQh(ye|}i5mxy;T?2PuRusy2lNqK!3VjR&u0s zjb3u*T^^rCPN^%6#85dGMh&W3fsD!+jd-zU8I8P|5>AEOqWf92SH*VO^I6cpQh~CW zv&et?zUn)RNN1Gg+2zVjEJMe~y*{b<||7 ywW~@Wt<|f_5Ufp_!b25;<_y`n5cEZ7k=e-*v_93=5WMN&tpM*Mo6tJ&y1+Yn zgo9iR9c$ps0<0=RQh=ln;y#E2crSpp8h*V3&QfrYd++-F4d}fFgHuK@z*%x_KOE%4 ztO#lvY6=E5Bt=L{kVLR8fSEpgu~R&>4!pJd1I&}){RKJz*1)J79Pj=C^Xbd^6-p?OFxt+=7P6SKrDfi%b->Xze97ZRd7WF zhuVAk* z>{bvL4X2X`x)prI-n#?5TZi$^BB9wL=!ifU&E#t;rCOrR-$MZ;D$8kUre2P)c$7l&rm2x>+1|4jf(!CO8OHCq5*2w-tpg3pJlc2-WOz-oD+n}&yS zLW4&#Xl7Sc!D$899vQDxl>@}_Of_#9@_rzm58D9~ITQFBjO~US^YB4nj8__HV8K+C zP%VPa5*!ZjPzIRCQ%QcHEJ3d8Rcwz4r)tjTh^cx4L2Ci1W*I@OlKy3OFGL ztHfYcHwb`TJq1;%qgHo`XSy?J)&pO6B);Z1BuqDH%Gi^ZJLON?pKkD;l!^H8nE;-S zoP;J5aAPLcP$?Oq@HHGId7vqnQ{dPPqiYh7m~FfJ{Wgxjwue zIti^&aV(gEnZSGnhXDM;87~LkgWyeoF)QJBMHAS2(aU;809GSY!dwBsY&iIAIHFe! zm)ZpMoaBfQt}GZetF}Z6NM4Bfr${3>?F4n2k$9@a1C>Si7>lW;Sv1quwAJ%ul z4GZBIYj6vAD~AC!2Re4am;yXlbP}dtuQ;*gb!e2V(j`^x)+YA5U}6aWD%2<7y^2c( zLyv%z=(c{o0R7bDU7W)#zD9*6^YArd9CMP?x9 zUN|y@pqvpWerhw;u)7<+ih@GH48AijrA7eDd*Ji!Q0yj|z*4wcN{FVC?0^}P?ZCWl zr-WIS%KPXL?#q&4=h)qE>|Tq87p3~F`XEgFIk-`$b81YCdjej=;DE9Bdwh5^Op{yPPED%c8B3)cco;fnXMkA` zmq?)28dc1e8kS-bsGIww{J+4l>%(Ot5UteE2K-RToFBu7cLd#@1Y`#bWZM})s3f7y z25(LSm~Qz$fy27sDJ!44IUQm<38CW60Fxb?A$%zmujRxmE#hRbZ0zMw@UChA;wp_w zskX5PP8GGP1M|K5)x5r;N^Z59RgvR212|3=sY3$)%n<%lkeJE{cS5vY& z0le4?r(4fVtM`oETYOH#c9G+uCdFT!ij~ayPJm4k{0dHu;GeFI7Rhl)w&5jwcuVi? zkvdM>5?#Q&+ykco{J{ZoTPnq4BUmOIrQNzY5L?bvLr8kzjlW9yA`(0yg1;A9%XuS^ zUQ}9QRI{W`Xi|~g;-&zO&DhIa=WK`LVI;V())J+-+J{;!K%1MytXm~$A%!*o1M<#!5nhs;7miu%Bn~To&w}t%f9x(g;sc7ge z8H0nWH4XNngsj%A`QTYv-I~V9QF&M;fjE>wFYD=AKYXwlE)mr_DS|DnfH=ospRs)n z8m0`G$?eX#p|N&~1E`jv(hEPfJHUqpNDp`+%xqOJHLPr`CH^!lmV`Jlf>&AvyW`DR z!A>8(j1QOQO?*^Ti)ynGj9o?PI==10c_PCd&L-(+;{Mpru;Q;Q3u?3h{%&0b1c7~2irt>%n;10S7cvot#ES_6>)$)+^ zz?>CuMg+SWt7*T!@~s6B_rN#ztln(+Mkqh3wBy$CT8*AYVYg56=1qs-!IB6)$ z6Ae%I!Zjg$kZ*FYt)Qj8K z>4YD&Y@95Eb9>;ww)qycNinA`i_5UH6TT&-%FM97UDOu68oTn&Sd{`K{h+5xH>R#D zzaG5R^2N(=vlNFFDaG6MOl-ZPpi46RZ}!C_O(>V-pdzKrJGKIE`cU=?`EnZ^&;=_> zXwOKsEV6VsmBwOJQJHN+9@|0?d0n;Jxl6*3RInwn=axmnlans5}@xBDN^@6R2 zi?CxUeBaLNiG6u-UUk zetRcn1x)OOha#wz(!SG>1*PiWWN68Kw+|p&*-AbI@M<1@IuFJH%mQ!g{sOTMyftF% z1sJQ~`~V(}V0%g4A#t894$Au_Ls~kQa#Za1a!oCgdu3nqTnM-HOHUZ!PVgSy-){&F zG#FeChveX_5PlUvzk+x35D%ELJ}IV;tP|mTQk3qcv^gspUJGGy7hE|Tj$Pnv%w~O* z?`+xUiCxYh+PpRNv88Z&0N>FtS;1o(N&#%t@Vp%Bw5e)mYxsF5T(}*MSqTR>rhr%W b0mT0S5)@H#d7`>U00000NkvXXu0mjfEz&h@ literal 0 HcmV?d00001 diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Client.php b/lib/composer/vendor/sabre/dav/lib/DAV/Client.php new file mode 100644 index 0000000..1028a6b --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Client.php @@ -0,0 +1,485 @@ +xml->elementMap. + * It's deprecated as of version 3.0.0, and should no longer be used. + * + * @deprecated + * + * @var array + */ + public $propertyMap = []; + + /** + * Base URI. + * + * This URI will be used to resolve relative urls. + * + * @var string + */ + protected $baseUri; + + /** + * Basic authentication. + */ + const AUTH_BASIC = 1; + + /** + * Digest authentication. + */ + const AUTH_DIGEST = 2; + + /** + * NTLM authentication. + */ + const AUTH_NTLM = 4; + + /** + * Identity encoding, which basically does not nothing. + */ + const ENCODING_IDENTITY = 1; + + /** + * Deflate encoding. + */ + const ENCODING_DEFLATE = 2; + + /** + * Gzip encoding. + */ + const ENCODING_GZIP = 4; + + /** + * Sends all encoding headers. + */ + const ENCODING_ALL = 7; + + /** + * Content-encoding. + * + * @var int + */ + protected $encoding = self::ENCODING_IDENTITY; + + /** + * Constructor. + * + * Settings are provided through the 'settings' argument. The following + * settings are supported: + * + * * baseUri + * * userName (optional) + * * password (optional) + * * proxy (optional) + * * authType (optional) + * * encoding (optional) + * + * authType must be a bitmap, using self::AUTH_BASIC, self::AUTH_DIGEST + * and self::AUTH_NTLM. If you know which authentication method will be + * used, it's recommended to set it, as it will save a great deal of + * requests to 'discover' this information. + * + * Encoding is a bitmap with one of the ENCODING constants. + */ + public function __construct(array $settings) + { + if (!isset($settings['baseUri'])) { + throw new \InvalidArgumentException('A baseUri must be provided'); + } + + parent::__construct(); + + $this->baseUri = $settings['baseUri']; + + if (isset($settings['proxy'])) { + $this->addCurlSetting(CURLOPT_PROXY, $settings['proxy']); + } + + if (isset($settings['userName'])) { + $userName = $settings['userName']; + $password = isset($settings['password']) ? $settings['password'] : ''; + + if (isset($settings['authType'])) { + $curlType = 0; + if ($settings['authType'] & self::AUTH_BASIC) { + $curlType |= CURLAUTH_BASIC; + } + if ($settings['authType'] & self::AUTH_DIGEST) { + $curlType |= CURLAUTH_DIGEST; + } + if ($settings['authType'] & self::AUTH_NTLM) { + $curlType |= CURLAUTH_NTLM; + } + } else { + $curlType = CURLAUTH_BASIC | CURLAUTH_DIGEST; + } + + $this->addCurlSetting(CURLOPT_HTTPAUTH, $curlType); + $this->addCurlSetting(CURLOPT_USERPWD, $userName.':'.$password); + } + + if (isset($settings['encoding'])) { + $encoding = $settings['encoding']; + + $encodings = []; + if ($encoding & self::ENCODING_IDENTITY) { + $encodings[] = 'identity'; + } + if ($encoding & self::ENCODING_DEFLATE) { + $encodings[] = 'deflate'; + } + if ($encoding & self::ENCODING_GZIP) { + $encodings[] = 'gzip'; + } + $this->addCurlSetting(CURLOPT_ENCODING, implode(',', $encodings)); + } + + $this->addCurlSetting(CURLOPT_USERAGENT, 'sabre-dav/'.Version::VERSION.' (http://sabre.io/)'); + + $this->xml = new Xml\Service(); + // BC + $this->propertyMap = &$this->xml->elementMap; + } + + /** + * Does a PROPFIND request with filtered response returning only available properties. + * + * The list of requested properties must be specified as an array, in clark + * notation. + * + * Depth should be either 0 or 1. A depth of 1 will cause a request to be + * made to the server to also return all child resources. + * + * For depth 0, just the array of properties for the resource is returned. + * + * For depth 1, the returned array will contain a list of resource names as keys, + * and an array of properties as values. + * + * The array of properties will contain the properties as keys with their values as the value. + * Only properties that are actually returned from the server without error will be + * returned, anything else is discarded. + * + * @param 1|0 $depth + */ + public function propFind($url, array $properties, $depth = 0): array + { + $result = $this->doPropFind($url, $properties, $depth); + + // If depth was 0, we only return the top item + if (0 === $depth) { + reset($result); + $result = current($result); + + return isset($result[200]) ? $result[200] : []; + } + + $newResult = []; + foreach ($result as $href => $statusList) { + $newResult[$href] = isset($statusList[200]) ? $statusList[200] : []; + } + + return $newResult; + } + + /** + * Does a PROPFIND request with unfiltered response. + * + * The list of requested properties must be specified as an array, in clark + * notation. + * + * Depth should be either 0 or 1. A depth of 1 will cause a request to be + * made to the server to also return all child resources. + * + * For depth 0, just the multi-level array of status and properties for the resource is returned. + * + * For depth 1, the returned array will contain a list of resources as keys and + * a multi-level array containing status and properties as value. + * + * The multi-level array of status and properties is formatted the same as what is + * documented for parseMultiStatus. + * + * All properties that are actually returned from the server are returned by this method. + * + * @param 1|0 $depth + */ + public function propFindUnfiltered(string $url, array $properties, int $depth = 0): array + { + $result = $this->doPropFind($url, $properties, $depth); + + // If depth was 0, we only return the top item + if (0 === $depth) { + reset($result); + + return current($result); + } else { + return $result; + } + } + + /** + * Does a PROPFIND request. + * + * The list of requested properties must be specified as an array, in clark + * notation. + * + * Depth should be either 0 or 1. A depth of 1 will cause a request to be + * made to the server to also return all child resources. + * + * The returned array will contain a list of resources as keys and + * a multi-level array containing status and properties as value. + * + * The multi-level array of status and properties is formatted the same as what is + * documented for parseMultiStatus. + * + * @param 1|0 $depth + */ + private function doPropFind($url, array $properties, $depth = 0): array + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->formatOutput = true; + $root = $dom->createElementNS('DAV:', 'd:propfind'); + $prop = $dom->createElement('d:prop'); + + foreach ($properties as $property) { + list( + $namespace, + $elementName + ) = \Sabre\Xml\Service::parseClarkNotation($property); + + if ('DAV:' === $namespace) { + $element = $dom->createElement('d:'.$elementName); + } else { + $element = $dom->createElementNS($namespace, 'x:'.$elementName); + } + + $prop->appendChild($element); + } + + $dom->appendChild($root)->appendChild($prop); + $body = $dom->saveXML(); + + $url = $this->getAbsoluteUrl($url); + + $request = new HTTP\Request('PROPFIND', $url, [ + 'Depth' => $depth, + 'Content-Type' => 'application/xml', + ], $body); + + $response = $this->send($request); + + if ((int) $response->getStatus() >= 400) { + throw new HTTP\ClientHttpException($response); + } + + return $this->parseMultiStatus($response->getBodyAsString()); + } + + /** + * Updates a list of properties on the server. + * + * The list of properties must have clark-notation properties for the keys, + * and the actual (string) value for the value. If the value is null, an + * attempt is made to delete the property. + * + * @param string $url + * + * @return bool + */ + public function propPatch($url, array $properties) + { + $propPatch = new Xml\Request\PropPatch(); + $propPatch->properties = $properties; + $xml = $this->xml->write( + '{DAV:}propertyupdate', + $propPatch + ); + + $url = $this->getAbsoluteUrl($url); + $request = new HTTP\Request('PROPPATCH', $url, [ + 'Content-Type' => 'application/xml', + ], $xml); + $response = $this->send($request); + + if ($response->getStatus() >= 400) { + throw new HTTP\ClientHttpException($response); + } + + if (207 === $response->getStatus()) { + // If it's a 207, the request could still have failed, but the + // information is hidden in the response body. + $result = $this->parseMultiStatus($response->getBodyAsString()); + + $errorProperties = []; + foreach ($result as $href => $statusList) { + foreach ($statusList as $status => $properties) { + if ($status >= 400) { + foreach ($properties as $propName => $propValue) { + $errorProperties[] = $propName.' ('.$status.')'; + } + } + } + } + if ($errorProperties) { + throw new HTTP\ClientException('PROPPATCH failed. The following properties errored: '.implode(', ', $errorProperties)); + } + } + + return true; + } + + /** + * Performs an HTTP options request. + * + * This method returns all the features from the 'DAV:' header as an array. + * If there was no DAV header, or no contents this method will return an + * empty array. + * + * @return array + */ + public function options() + { + $request = new HTTP\Request('OPTIONS', $this->getAbsoluteUrl('')); + $response = $this->send($request); + + $dav = $response->getHeader('Dav'); + if (!$dav) { + return []; + } + + $features = explode(',', $dav); + foreach ($features as &$v) { + $v = trim($v); + } + + return $features; + } + + /** + * Performs an actual HTTP request, and returns the result. + * + * If the specified url is relative, it will be expanded based on the base + * url. + * + * The returned array contains 3 keys: + * * body - the response body + * * httpCode - a HTTP code (200, 404, etc) + * * headers - a list of response http headers. The header names have + * been lowercased. + * + * For large uploads, it's highly recommended to specify body as a stream + * resource. You can easily do this by simply passing the result of + * fopen(..., 'r'). + * + * This method will throw an exception if an HTTP error was received. Any + * HTTP status code above 399 is considered an error. + * + * Note that it is no longer recommended to use this method, use the send() + * method instead. + * + * @param string $method + * @param string $url + * @param string|resource|null $body + * + * @throws clientException, in case a curl error occurred + * + * @return array + */ + public function request($method, $url = '', $body = null, array $headers = []) + { + $url = $this->getAbsoluteUrl($url); + + $response = $this->send(new HTTP\Request($method, $url, $headers, $body)); + + return [ + 'body' => $response->getBodyAsString(), + 'statusCode' => (int) $response->getStatus(), + 'headers' => array_change_key_case($response->getHeaders()), + ]; + } + + /** + * Returns the full url based on the given url (which may be relative). All + * urls are expanded based on the base url as given by the server. + * + * @param string $url + * + * @return string + */ + public function getAbsoluteUrl($url) + { + return Uri\resolve( + $this->baseUri, + (string) $url + ); + } + + /** + * Parses a WebDAV multistatus response body. + * + * This method returns an array with the following structure + * + * [ + * 'url/to/resource' => [ + * '200' => [ + * '{DAV:}property1' => 'value1', + * '{DAV:}property2' => 'value2', + * ], + * '404' => [ + * '{DAV:}property1' => null, + * '{DAV:}property2' => null, + * ], + * ], + * 'url/to/resource2' => [ + * .. etc .. + * ] + * ] + * + * @param string $body xml body + * + * @return array + */ + public function parseMultiStatus($body) + { + $multistatus = $this->xml->expect('{DAV:}multistatus', $body); + + $result = []; + + foreach ($multistatus->getResponses() as $response) { + $result[$response->getHref()] = $response->getResponseProperties(); + } + + return $result; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Collection.php b/lib/composer/vendor/sabre/dav/lib/DAV/Collection.php new file mode 100644 index 0000000..2728bb2 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Collection.php @@ -0,0 +1,106 @@ +getChildren() as $child) { + if ($child->getName() === $name) { + return $child; + } + } + throw new Exception\NotFound('File not found: '.$name); + } + + /** + * Checks is a child-node exists. + * + * It is generally a good idea to try and override this. Usually it can be optimized. + * + * @param string $name + * + * @return bool + */ + public function childExists($name) + { + try { + $this->getChild($name); + + return true; + } catch (Exception\NotFound $e) { + return false; + } + } + + /** + * Creates a new file in the directory. + * + * Data will either be supplied as a stream resource, or in certain cases + * as a string. Keep in mind that you may have to support either. + * + * After successful creation of the file, you may choose to return the ETag + * of the new file here. + * + * The returned ETag must be surrounded by double-quotes (The quotes should + * be part of the actual string). + * + * If you cannot accurately determine the ETag, you should not return it. + * If you don't store the file exactly as-is (you're transforming it + * somehow) you should also not return an ETag. + * + * This means that if a subsequent GET to this new file does not exactly + * return the same contents of what was submitted here, you are strongly + * recommended to omit the ETag. + * + * @param string $name Name of the file + * @param resource|string $data Initial payload + * + * @return string|null + */ + public function createFile($name, $data = null) + { + throw new Exception\Forbidden('Permission denied to create file (filename '.$name.')'); + } + + /** + * Creates a new subdirectory. + * + * @param string $name + * + * @throws Exception\Forbidden + */ + public function createDirectory($name) + { + throw new Exception\Forbidden('Permission denied to create directory'); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/CorePlugin.php b/lib/composer/vendor/sabre/dav/lib/DAV/CorePlugin.php new file mode 100644 index 0000000..dbd8976 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/CorePlugin.php @@ -0,0 +1,907 @@ +server = $server; + $server->on('method:GET', [$this, 'httpGet']); + $server->on('method:OPTIONS', [$this, 'httpOptions']); + $server->on('method:HEAD', [$this, 'httpHead']); + $server->on('method:DELETE', [$this, 'httpDelete']); + $server->on('method:PROPFIND', [$this, 'httpPropFind']); + $server->on('method:PROPPATCH', [$this, 'httpPropPatch']); + $server->on('method:PUT', [$this, 'httpPut']); + $server->on('method:MKCOL', [$this, 'httpMkcol']); + $server->on('method:MOVE', [$this, 'httpMove']); + $server->on('method:COPY', [$this, 'httpCopy']); + $server->on('method:REPORT', [$this, 'httpReport']); + + $server->on('propPatch', [$this, 'propPatchProtectedPropertyCheck'], 90); + $server->on('propPatch', [$this, 'propPatchNodeUpdate'], 200); + $server->on('propFind', [$this, 'propFind']); + $server->on('propFind', [$this, 'propFindNode'], 120); + $server->on('propFind', [$this, 'propFindLate'], 200); + + $server->on('exception', [$this, 'exception']); + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'core'; + } + + /** + * This is the default implementation for the GET method. + * + * @return bool + */ + public function httpGet(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + $node = $this->server->tree->getNodeForPath($path); + + if (!$node instanceof IFile) { + return; + } + + if ('HEAD' === $request->getHeader('X-Sabre-Original-Method')) { + $body = ''; + } else { + $body = $node->get(); + + // Converting string into stream, if needed. + if (is_string($body)) { + $stream = fopen('php://temp', 'r+'); + fwrite($stream, $body); + rewind($stream); + $body = $stream; + } + } + + /* + * TODO: getetag, getlastmodified, getsize should also be used using + * this method + */ + $httpHeaders = $this->server->getHTTPHeaders($path); + + /* ContentType needs to get a default, because many webservers will otherwise + * default to text/html, and we don't want this for security reasons. + */ + if (!isset($httpHeaders['Content-Type'])) { + $httpHeaders['Content-Type'] = 'application/octet-stream'; + } + + if (isset($httpHeaders['Content-Length'])) { + $nodeSize = $httpHeaders['Content-Length']; + + // Need to unset Content-Length, because we'll handle that during figuring out the range + unset($httpHeaders['Content-Length']); + } else { + $nodeSize = null; + } + + $response->addHeaders($httpHeaders); + + $range = $this->server->getHTTPRange(); + $ifRange = $request->getHeader('If-Range'); + $ignoreRangeHeader = false; + + // If ifRange is set, and range is specified, we first need to check + // the precondition. + if ($nodeSize && $range && $ifRange) { + // if IfRange is parsable as a date we'll treat it as a DateTime + // otherwise, we must treat it as an etag. + try { + $ifRangeDate = new \DateTime($ifRange); + + // It's a date. We must check if the entity is modified since + // the specified date. + if (!isset($httpHeaders['Last-Modified'])) { + $ignoreRangeHeader = true; + } else { + $modified = new \DateTime($httpHeaders['Last-Modified']); + if ($modified > $ifRangeDate) { + $ignoreRangeHeader = true; + } + } + } catch (\Exception $e) { + // It's an entity. We can do a simple comparison. + if (!isset($httpHeaders['ETag'])) { + $ignoreRangeHeader = true; + } elseif ($httpHeaders['ETag'] !== $ifRange) { + $ignoreRangeHeader = true; + } + } + } + + // We're only going to support HTTP ranges if the backend provided a filesize + if (!$ignoreRangeHeader && $nodeSize && $range) { + // Determining the exact byte offsets + if (!is_null($range[0])) { + $start = $range[0]; + $end = $range[1] ? $range[1] : $nodeSize - 1; + if ($start >= $nodeSize) { + throw new Exception\RequestedRangeNotSatisfiable('The start offset ('.$range[0].') exceeded the size of the entity ('.$nodeSize.')'); + } + if ($end < $start) { + throw new Exception\RequestedRangeNotSatisfiable('The end offset ('.$range[1].') is lower than the start offset ('.$range[0].')'); + } + if ($end >= $nodeSize) { + $end = $nodeSize - 1; + } + } else { + $start = $nodeSize - $range[1]; + $end = $nodeSize - 1; + + if ($start < 0) { + $start = 0; + } + } + + // Streams may advertise themselves as seekable, but still not + // actually allow fseek. We'll manually go forward in the stream + // if fseek failed. + if (!stream_get_meta_data($body)['seekable'] || -1 === fseek($body, $start, SEEK_SET)) { + $consumeBlock = 8192; + for ($consumed = 0; $start - $consumed > 0;) { + if (feof($body)) { + throw new Exception\RequestedRangeNotSatisfiable('The start offset ('.$start.') exceeded the size of the entity ('.$consumed.')'); + } + $consumed += strlen(fread($body, min($start - $consumed, $consumeBlock))); + } + } + + $response->setHeader('Content-Length', $end - $start + 1); + $response->setHeader('Content-Range', 'bytes '.$start.'-'.$end.'/'.$nodeSize); + $response->setStatus(206); + $response->setBody($body); + } else { + if ($nodeSize) { + $response->setHeader('Content-Length', $nodeSize); + } + $response->setStatus(200); + $response->setBody($body); + } + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * HTTP OPTIONS. + * + * @return bool + */ + public function httpOptions(RequestInterface $request, ResponseInterface $response) + { + $methods = $this->server->getAllowedMethods($request->getPath()); + + $response->setHeader('Allow', strtoupper(implode(', ', $methods))); + $features = ['1', '3', 'extended-mkcol']; + + foreach ($this->server->getPlugins() as $plugin) { + $features = array_merge($features, $plugin->getFeatures()); + } + + $response->setHeader('DAV', implode(', ', $features)); + $response->setHeader('MS-Author-Via', 'DAV'); + $response->setHeader('Accept-Ranges', 'bytes'); + $response->setHeader('Content-Length', '0'); + $response->setStatus(200); + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * HTTP HEAD. + * + * This method is normally used to take a peak at a url, and only get the + * HTTP response headers, without the body. This is used by clients to + * determine if a remote file was changed, so they can use a local cached + * version, instead of downloading it again + * + * @return bool + */ + public function httpHead(RequestInterface $request, ResponseInterface $response) + { + // This is implemented by changing the HEAD request to a GET request, + // and telling the request handler that is doesn't need to create the body. + $subRequest = clone $request; + $subRequest->setMethod('GET'); + $subRequest->setHeader('X-Sabre-Original-Method', 'HEAD'); + + try { + $this->server->invokeMethod($subRequest, $response, false); + } catch (Exception\NotImplemented $e) { + // Some clients may do HEAD requests on collections, however, GET + // requests and HEAD requests _may_ not be defined on a collection, + // which would trigger a 501. + // This breaks some clients though, so we're transforming these + // 501s into 200s. + $response->setStatus(200); + $response->setBody(''); + $response->setHeader('Content-Type', 'text/plain'); + $response->setHeader('X-Sabre-Real-Status', $e->getHTTPCode()); + } + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * HTTP Delete. + * + * The HTTP delete method, deletes a given uri + */ + public function httpDelete(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + + if (!$this->server->emit('beforeUnbind', [$path])) { + return false; + } + $this->server->tree->delete($path); + $this->server->emit('afterUnbind', [$path]); + + $response->setStatus(204); + $response->setHeader('Content-Length', '0'); + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * WebDAV PROPFIND. + * + * This WebDAV method requests information about an uri resource, or a list of resources + * If a client wants to receive the properties for a single resource it will add an HTTP Depth: header with a 0 value + * If the value is 1, it means that it also expects a list of sub-resources (e.g.: files in a directory) + * + * The request body contains an XML data structure that has a list of properties the client understands + * The response body is also an xml document, containing information about every uri resource and the requested properties + * + * It has to return a HTTP 207 Multi-status status code + */ + public function httpPropFind(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + + $requestBody = $request->getBodyAsString(); + if (strlen($requestBody)) { + try { + $propFindXml = $this->server->xml->expect('{DAV:}propfind', $requestBody); + } catch (ParseException $e) { + throw new BadRequest($e->getMessage(), 0, $e); + } + } else { + $propFindXml = new Xml\Request\PropFind(); + $propFindXml->allProp = true; + $propFindXml->properties = []; + } + + $depth = $this->server->getHTTPDepth(1); + // The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled + if (!$this->server->enablePropfindDepthInfinity && 0 != $depth) { + $depth = 1; + } + + $newProperties = $this->server->getPropertiesIteratorForPath($path, $propFindXml->properties, $depth); + + // This is a multi-status response + $response->setStatus(207); + $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $response->setHeader('Vary', 'Brief,Prefer'); + + // Normally this header is only needed for OPTIONS responses, however.. + // iCal seems to also depend on these being set for PROPFIND. Since + // this is not harmful, we'll add it. + $features = ['1', '3', 'extended-mkcol']; + foreach ($this->server->getPlugins() as $plugin) { + $features = array_merge($features, $plugin->getFeatures()); + } + $response->setHeader('DAV', implode(', ', $features)); + + $prefer = $this->server->getHTTPPrefer(); + $minimal = 'minimal' === $prefer['return']; + + $data = $this->server->generateMultiStatus($newProperties, $minimal); + $response->setBody($data); + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * WebDAV PROPPATCH. + * + * This method is called to update properties on a Node. The request is an XML body with all the mutations. + * In this XML body it is specified which properties should be set/updated and/or deleted + * + * @return bool + */ + public function httpPropPatch(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + + try { + $propPatch = $this->server->xml->expect('{DAV:}propertyupdate', $request->getBody()); + } catch (ParseException $e) { + throw new BadRequest($e->getMessage(), 0, $e); + } + $newProperties = $propPatch->properties; + + $result = $this->server->updateProperties($path, $newProperties); + + $prefer = $this->server->getHTTPPrefer(); + $response->setHeader('Vary', 'Brief,Prefer'); + + if ('minimal' === $prefer['return']) { + // If return-minimal is specified, we only have to check if the + // request was successful, and don't need to return the + // multi-status. + $ok = true; + foreach ($result as $prop => $code) { + if ((int) $code > 299) { + $ok = false; + } + } + + if ($ok) { + $response->setStatus(204); + + return false; + } + } + + $response->setStatus(207); + $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); + + // Reorganizing the result for generateMultiStatus + $multiStatus = []; + foreach ($result as $propertyName => $code) { + if (isset($multiStatus[$code])) { + $multiStatus[$code][$propertyName] = null; + } else { + $multiStatus[$code] = [$propertyName => null]; + } + } + $multiStatus['href'] = $path; + + $response->setBody( + $this->server->generateMultiStatus([$multiStatus]) + ); + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * HTTP PUT method. + * + * This HTTP method updates a file, or creates a new one. + * + * If a new resource was created, a 201 Created status code should be returned. If an existing resource is updated, it's a 204 No Content + * + * @return bool + */ + public function httpPut(RequestInterface $request, ResponseInterface $response) + { + $body = $request->getBodyAsStream(); + $path = $request->getPath(); + + // Intercepting Content-Range + if ($request->getHeader('Content-Range')) { + /* + An origin server that allows PUT on a given target resource MUST send + a 400 (Bad Request) response to a PUT request that contains a + Content-Range header field. + + Reference: http://tools.ietf.org/html/rfc7231#section-4.3.4 + */ + throw new Exception\BadRequest('Content-Range on PUT requests are forbidden.'); + } + + // Intercepting the Finder problem + if (($expected = $request->getHeader('X-Expected-Entity-Length')) && $expected > 0) { + /* + Many webservers will not cooperate well with Finder PUT requests, + because it uses 'Chunked' transfer encoding for the request body. + + The symptom of this problem is that Finder sends files to the + server, but they arrive as 0-length files in PHP. + + If we don't do anything, the user might think they are uploading + files successfully, but they end up empty on the server. Instead, + we throw back an error if we detect this. + + The reason Finder uses Chunked, is because it thinks the files + might change as it's being uploaded, and therefore the + Content-Length can vary. + + Instead it sends the X-Expected-Entity-Length header with the size + of the file at the very start of the request. If this header is set, + but we don't get a request body we will fail the request to + protect the end-user. + */ + + // Only reading first byte + $firstByte = fread($body, 1); + if (1 !== strlen($firstByte)) { + throw new Exception\Forbidden('This server is not compatible with OS/X finder. Consider using a different WebDAV client or webserver.'); + } + + // The body needs to stay intact, so we copy everything to a + // temporary stream. + + $newBody = fopen('php://temp', 'r+'); + fwrite($newBody, $firstByte); + stream_copy_to_stream($body, $newBody); + rewind($newBody); + + $body = $newBody; + } + + if ($this->server->tree->nodeExists($path)) { + $node = $this->server->tree->getNodeForPath($path); + + // If the node is a collection, we'll deny it + if (!($node instanceof IFile)) { + throw new Exception\Conflict('PUT is not allowed on non-files.'); + } + if (!$this->server->updateFile($path, $body, $etag)) { + return false; + } + + $response->setHeader('Content-Length', '0'); + if ($etag) { + $response->setHeader('ETag', $etag); + } + $response->setStatus(204); + } else { + $etag = null; + // If we got here, the resource didn't exist yet. + if (!$this->server->createFile($path, $body, $etag)) { + // For one reason or another the file was not created. + return false; + } + + $response->setHeader('Content-Length', '0'); + if ($etag) { + $response->setHeader('ETag', $etag); + } + $response->setStatus(201); + } + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * WebDAV MKCOL. + * + * The MKCOL method is used to create a new collection (directory) on the server + * + * @return bool + */ + public function httpMkcol(RequestInterface $request, ResponseInterface $response) + { + $requestBody = $request->getBodyAsString(); + $path = $request->getPath(); + + if ($requestBody) { + $contentType = $request->getHeader('Content-Type'); + if (null === $contentType || (0 !== strpos($contentType, 'application/xml') && 0 !== strpos($contentType, 'text/xml'))) { + // We must throw 415 for unsupported mkcol bodies + throw new Exception\UnsupportedMediaType('The request body for the MKCOL request must have an xml Content-Type'); + } + + try { + $mkcol = $this->server->xml->expect('{DAV:}mkcol', $requestBody); + } catch (\Sabre\Xml\ParseException $e) { + throw new Exception\BadRequest($e->getMessage(), 0, $e); + } + + $properties = $mkcol->getProperties(); + + if (!isset($properties['{DAV:}resourcetype'])) { + throw new Exception\BadRequest('The mkcol request must include a {DAV:}resourcetype property'); + } + $resourceType = $properties['{DAV:}resourcetype']->getValue(); + unset($properties['{DAV:}resourcetype']); + } else { + $properties = []; + $resourceType = ['{DAV:}collection']; + } + + $mkcol = new MkCol($resourceType, $properties); + + $result = $this->server->createCollection($path, $mkcol); + + if (is_array($result)) { + $response->setStatus(207); + $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); + + $response->setBody( + $this->server->generateMultiStatus([$result]) + ); + } else { + $response->setHeader('Content-Length', '0'); + $response->setStatus(201); + } + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * WebDAV HTTP MOVE method. + * + * This method moves one uri to a different uri. A lot of the actual request processing is done in getCopyMoveInfo + * + * @return bool + */ + public function httpMove(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + + $moveInfo = $this->server->getCopyAndMoveInfo($request); + + if ($moveInfo['destinationExists']) { + if (!$this->server->emit('beforeUnbind', [$moveInfo['destination']])) { + return false; + } + } + if (!$this->server->emit('beforeUnbind', [$path])) { + return false; + } + if (!$this->server->emit('beforeBind', [$moveInfo['destination']])) { + return false; + } + if (!$this->server->emit('beforeMove', [$path, $moveInfo['destination']])) { + return false; + } + + if ($moveInfo['destinationExists']) { + $this->server->tree->delete($moveInfo['destination']); + $this->server->emit('afterUnbind', [$moveInfo['destination']]); + } + + $this->server->tree->move($path, $moveInfo['destination']); + + // Its important afterMove is called before afterUnbind, because it + // allows systems to transfer data from one path to another. + // PropertyStorage uses this. If afterUnbind was first, it would clean + // up all the properties before it has a chance. + $this->server->emit('afterMove', [$path, $moveInfo['destination']]); + $this->server->emit('afterUnbind', [$path]); + $this->server->emit('afterBind', [$moveInfo['destination']]); + + // If a resource was overwritten we should send a 204, otherwise a 201 + $response->setHeader('Content-Length', '0'); + $response->setStatus($moveInfo['destinationExists'] ? 204 : 201); + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * WebDAV HTTP COPY method. + * + * This method copies one uri to a different uri, and works much like the MOVE request + * A lot of the actual request processing is done in getCopyMoveInfo + * + * @return bool + */ + public function httpCopy(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + + $copyInfo = $this->server->getCopyAndMoveInfo($request); + + if (!$this->server->emit('beforeBind', [$copyInfo['destination']])) { + return false; + } + if (!$this->server->emit('beforeCopy', [$path, $copyInfo['destination']])) { + return false; + } + + if ($copyInfo['destinationExists']) { + if (!$this->server->emit('beforeUnbind', [$copyInfo['destination']])) { + return false; + } + $this->server->tree->delete($copyInfo['destination']); + } + + $this->server->tree->copy($path, $copyInfo['destination']); + $this->server->emit('afterCopy', [$path, $copyInfo['destination']]); + $this->server->emit('afterBind', [$copyInfo['destination']]); + + // If a resource was overwritten we should send a 204, otherwise a 201 + $response->setHeader('Content-Length', '0'); + $response->setStatus($copyInfo['destinationExists'] ? 204 : 201); + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * HTTP REPORT method implementation. + * + * Although the REPORT method is not part of the standard WebDAV spec (it's from rfc3253) + * It's used in a lot of extensions, so it made sense to implement it into the core. + * + * @return bool + */ + public function httpReport(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + + $result = $this->server->xml->parse( + $request->getBody(), + $request->getUrl(), + $rootElementName + ); + + if ($this->server->emit('report', [$rootElementName, $result, $path])) { + // If emit returned true, it means the report was not supported + throw new Exception\ReportNotSupported(); + } + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * This method is called during property updates. + * + * Here we check if a user attempted to update a protected property and + * ensure that the process fails if this is the case. + * + * @param string $path + */ + public function propPatchProtectedPropertyCheck($path, PropPatch $propPatch) + { + // Comparing the mutation list to the list of protected properties. + $mutations = $propPatch->getMutations(); + + $protected = array_intersect( + $this->server->protectedProperties, + array_keys($mutations) + ); + + if ($protected) { + $propPatch->setResultCode($protected, 403); + } + } + + /** + * This method is called during property updates. + * + * Here we check if a node implements IProperties and let the node handle + * updating of (some) properties. + * + * @param string $path + */ + public function propPatchNodeUpdate($path, PropPatch $propPatch) + { + // This should trigger a 404 if the node doesn't exist. + $node = $this->server->tree->getNodeForPath($path); + + if ($node instanceof IProperties) { + $node->propPatch($propPatch); + } + } + + /** + * This method is called when properties are retrieved. + * + * Here we add all the default properties. + */ + public function propFind(PropFind $propFind, INode $node) + { + $propFind->handle('{DAV:}getlastmodified', function () use ($node) { + $lm = $node->getLastModified(); + if ($lm) { + return new Xml\Property\GetLastModified($lm); + } + }); + + if ($node instanceof IFile) { + $propFind->handle('{DAV:}getcontentlength', [$node, 'getSize']); + $propFind->handle('{DAV:}getetag', [$node, 'getETag']); + $propFind->handle('{DAV:}getcontenttype', [$node, 'getContentType']); + } + + if ($node instanceof IQuota) { + $quotaInfo = null; + $propFind->handle('{DAV:}quota-used-bytes', function () use (&$quotaInfo, $node) { + $quotaInfo = $node->getQuotaInfo(); + + return $quotaInfo[0]; + }); + $propFind->handle('{DAV:}quota-available-bytes', function () use (&$quotaInfo, $node) { + if (!$quotaInfo) { + $quotaInfo = $node->getQuotaInfo(); + } + + return $quotaInfo[1]; + }); + } + + $propFind->handle('{DAV:}supported-report-set', function () use ($propFind) { + $reports = []; + foreach ($this->server->getPlugins() as $plugin) { + $reports = array_merge($reports, $plugin->getSupportedReportSet($propFind->getPath())); + } + + return new Xml\Property\SupportedReportSet($reports); + }); + $propFind->handle('{DAV:}resourcetype', function () use ($node) { + return new Xml\Property\ResourceType($this->server->getResourceTypeForNode($node)); + }); + $propFind->handle('{DAV:}supported-method-set', function () use ($propFind) { + return new Xml\Property\SupportedMethodSet( + $this->server->getAllowedMethods($propFind->getPath()) + ); + }); + } + + /** + * Fetches properties for a node. + * + * This event is called a bit later, so plugins have a chance first to + * populate the result. + */ + public function propFindNode(PropFind $propFind, INode $node) + { + if ($node instanceof IProperties && $propertyNames = $propFind->get404Properties()) { + $nodeProperties = $node->getProperties($propertyNames); + foreach ($nodeProperties as $propertyName => $propertyValue) { + $propFind->set($propertyName, $propertyValue, 200); + } + } + } + + /** + * This method is called when properties are retrieved. + * + * This specific handler is called very late in the process, because we + * want other systems to first have a chance to handle the properties. + */ + public function propFindLate(PropFind $propFind, INode $node) + { + $propFind->handle('{http://calendarserver.org/ns/}getctag', function () use ($propFind) { + // If we already have a sync-token from the current propFind + // request, we can re-use that. + $val = $propFind->get('{http://sabredav.org/ns}sync-token'); + if ($val) { + return $val; + } + + $val = $propFind->get('{DAV:}sync-token'); + if ($val && is_scalar($val)) { + return $val; + } + if ($val && $val instanceof Xml\Property\Href) { + return substr($val->getHref(), strlen(Sync\Plugin::SYNCTOKEN_PREFIX)); + } + + // If we got here, the earlier two properties may simply not have + // been part of the earlier request. We're going to fetch them. + $result = $this->server->getProperties($propFind->getPath(), [ + '{http://sabredav.org/ns}sync-token', + '{DAV:}sync-token', + ]); + + if (isset($result['{http://sabredav.org/ns}sync-token'])) { + return $result['{http://sabredav.org/ns}sync-token']; + } + if (isset($result['{DAV:}sync-token'])) { + $val = $result['{DAV:}sync-token']; + if (is_scalar($val)) { + return $val; + } elseif ($val instanceof Xml\Property\Href) { + return substr($val->getHref(), strlen(Sync\Plugin::SYNCTOKEN_PREFIX)); + } + } + }); + } + + /** + * Listens for exception events, and automatically logs them. + * + * @param Exception $e + */ + public function exception($e) + { + $logLevel = \Psr\Log\LogLevel::CRITICAL; + if ($e instanceof \Sabre\DAV\Exception) { + // If it's a standard sabre/dav exception, it means we have a http + // status code available. + $code = $e->getHTTPCode(); + + if ($code >= 400 && $code < 500) { + // user error + $logLevel = \Psr\Log\LogLevel::INFO; + } else { + // Server-side error. We mark it's as an error, but it's not + // critical. + $logLevel = \Psr\Log\LogLevel::ERROR; + } + } + + $this->server->getLogger()->log( + $logLevel, + 'Uncaught exception', + [ + 'exception' => $e, + ] + ); + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'The Core plugin provides a lot of the basic functionality required by WebDAV, such as a default implementation for all HTTP and WebDAV methods.', + 'link' => null, + ]; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Exception.php b/lib/composer/vendor/sabre/dav/lib/DAV/Exception.php new file mode 100644 index 0000000..9fc1d16 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Exception.php @@ -0,0 +1,50 @@ +lock) { + $error = $errorNode->ownerDocument->createElementNS('DAV:', 'd:no-conflicting-lock'); + $errorNode->appendChild($error); + $error->appendChild($errorNode->ownerDocument->createElementNS('DAV:', 'd:href', $this->lock->uri)); + } + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Exception/Forbidden.php b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/Forbidden.php new file mode 100644 index 0000000..2f882c3 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/Forbidden.php @@ -0,0 +1,29 @@ +ownerDocument->createElementNS('DAV:', 'd:valid-resourcetype'); + $errorNode->appendChild($error); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Exception/InvalidSyncToken.php b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/InvalidSyncToken.php new file mode 100644 index 0000000..f28d20f --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/InvalidSyncToken.php @@ -0,0 +1,34 @@ +ownerDocument->createElementNS('DAV:', 'd:valid-sync-token'); + $errorNode->appendChild($error); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Exception/LengthRequired.php b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/LengthRequired.php new file mode 100644 index 0000000..9d26fcb --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/LengthRequired.php @@ -0,0 +1,30 @@ +ownerDocument->createElementNS('DAV:', 'd:lock-token-matches-request-uri'); + $errorNode->appendChild($error); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Exception/Locked.php b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/Locked.php new file mode 100644 index 0000000..24fad70 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/Locked.php @@ -0,0 +1,68 @@ +lock = $lock; + } + + /** + * Returns the HTTP statuscode for this exception. + * + * @return int + */ + public function getHTTPCode() + { + return 423; + } + + /** + * This method allows the exception to include additional information into the WebDAV error response. + */ + public function serialize(DAV\Server $server, \DOMElement $errorNode) + { + if ($this->lock) { + $error = $errorNode->ownerDocument->createElementNS('DAV:', 'd:lock-token-submitted'); + $errorNode->appendChild($error); + + $href = $errorNode->ownerDocument->createElementNS('DAV:', 'd:href'); + $href->appendChild($errorNode->ownerDocument->createTextNode($this->lock->uri)); + $error->appendChild( + $href + ); + } + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php new file mode 100644 index 0000000..dbf42ed --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php @@ -0,0 +1,46 @@ +getAllowedMethods($server->getRequestUri()); + + return [ + 'Allow' => strtoupper(implode(', ', $methods)), + ]; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Exception/NotAuthenticated.php b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/NotAuthenticated.php new file mode 100644 index 0000000..0a5ba9b --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/NotAuthenticated.php @@ -0,0 +1,30 @@ +header = $header; + } + + /** + * Returns the HTTP statuscode for this exception. + * + * @return int + */ + public function getHTTPCode() + { + return 412; + } + + /** + * This method allows the exception to include additional information into the WebDAV error response. + */ + public function serialize(DAV\Server $server, \DOMElement $errorNode) + { + if ($this->header) { + $prop = $errorNode->ownerDocument->createElement('s:header'); + $prop->nodeValue = $this->header; + $errorNode->appendChild($prop); + } + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Exception/ReportNotSupported.php b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/ReportNotSupported.php new file mode 100644 index 0000000..a483838 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/ReportNotSupported.php @@ -0,0 +1,28 @@ +ownerDocument->createElementNS('DAV:', 'd:supported-report'); + $errorNode->appendChild($error); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Exception/RequestedRangeNotSatisfiable.php b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/RequestedRangeNotSatisfiable.php new file mode 100644 index 0000000..6ccb5b8 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/RequestedRangeNotSatisfiable.php @@ -0,0 +1,30 @@ + + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class ServiceUnavailable extends DAV\Exception +{ + /** + * Returns the HTTP statuscode for this exception. + * + * @return int + */ + public function getHTTPCode() + { + return 503; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Exception/TooManyMatches.php b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/TooManyMatches.php new file mode 100644 index 0000000..ef6f502 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/TooManyMatches.php @@ -0,0 +1,34 @@ +ownerDocument->createElementNS('DAV:', 'd:number-of-matches-within-limits'); + $errorNode->appendChild($error); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Exception/UnsupportedMediaType.php b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/UnsupportedMediaType.php new file mode 100644 index 0000000..bc9da30 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Exception/UnsupportedMediaType.php @@ -0,0 +1,30 @@ +path.'/'.$name; + file_put_contents($newPath, $data); + clearstatcache(true, $newPath); + } + + /** + * Creates a new subdirectory. + * + * @param string $name + */ + public function createDirectory($name) + { + $newPath = $this->path.'/'.$name; + mkdir($newPath); + clearstatcache(true, $newPath); + } + + /** + * Returns a specific child node, referenced by its name. + * + * This method must throw DAV\Exception\NotFound if the node does not + * exist. + * + * @param string $name + * + * @throws DAV\Exception\NotFound + * + * @return DAV\INode + */ + public function getChild($name) + { + $path = $this->path.'/'.$name; + + if (!file_exists($path)) { + throw new DAV\Exception\NotFound('File with name '.$path.' could not be located'); + } + if (is_dir($path)) { + return new self($path); + } else { + return new File($path); + } + } + + /** + * Returns an array with all the child nodes. + * + * @return DAV\INode[] + */ + public function getChildren() + { + $nodes = []; + $iterator = new \FilesystemIterator( + $this->path, + \FilesystemIterator::CURRENT_AS_SELF + | \FilesystemIterator::SKIP_DOTS + ); + foreach ($iterator as $entry) { + $nodes[] = $this->getChild($entry->getFilename()); + } + + return $nodes; + } + + /** + * Checks if a child exists. + * + * @param string $name + * + * @return bool + */ + public function childExists($name) + { + $path = $this->path.'/'.$name; + + return file_exists($path); + } + + /** + * Deletes all files in this directory, and then itself. + */ + public function delete() + { + foreach ($this->getChildren() as $child) { + $child->delete(); + } + rmdir($this->path); + } + + /** + * Returns available diskspace information. + * + * @return array + */ + public function getQuotaInfo() + { + $absolute = realpath($this->path); + + return [ + disk_total_space($absolute) - disk_free_space($absolute), + disk_free_space($absolute), + ]; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/FS/File.php b/lib/composer/vendor/sabre/dav/lib/DAV/FS/File.php new file mode 100644 index 0000000..b78a801 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/FS/File.php @@ -0,0 +1,87 @@ +path, $data); + clearstatcache(true, $this->path); + } + + /** + * Returns the data. + * + * @return resource + */ + public function get() + { + return fopen($this->path, 'r'); + } + + /** + * Delete the current file. + */ + public function delete() + { + unlink($this->path); + } + + /** + * Returns the size of the node, in bytes. + * + * @return int + */ + public function getSize() + { + return filesize($this->path); + } + + /** + * Returns the ETag for a file. + * + * An ETag is a unique identifier representing the current version of the file. If the file changes, the ETag MUST change. + * The ETag is an arbitrary string, but MUST be surrounded by double-quotes. + * + * Return null if the ETag can not effectively be determined + * + * @return mixed + */ + public function getETag() + { + return '"'.sha1( + fileinode($this->path). + filesize($this->path). + filemtime($this->path) + ).'"'; + } + + /** + * Returns the mime-type for a file. + * + * If null is returned, we'll assume application/octet-stream + * + * @return mixed + */ + public function getContentType() + { + return null; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/FS/Node.php b/lib/composer/vendor/sabre/dav/lib/DAV/FS/Node.php new file mode 100644 index 0000000..32aa747 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/FS/Node.php @@ -0,0 +1,96 @@ +path = $path; + $this->overrideName = $overrideName; + } + + /** + * Returns the name of the node. + * + * @return string + */ + public function getName() + { + if ($this->overrideName) { + return $this->overrideName; + } + + list(, $name) = Uri\split($this->path); + + return $name; + } + + /** + * Renames the node. + * + * @param string $name The new name + */ + public function setName($name) + { + if ($this->overrideName) { + throw new Forbidden('This node cannot be renamed'); + } + + list($parentPath) = Uri\split($this->path); + list(, $newName) = Uri\split($name); + + $newPath = $parentPath.'/'.$newName; + rename($this->path, $newPath); + + $this->path = $newPath; + } + + /** + * Returns the last modification time, as a unix timestamp. + * + * @return int + */ + public function getLastModified() + { + return filemtime($this->path); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/FSExt/Directory.php b/lib/composer/vendor/sabre/dav/lib/DAV/FSExt/Directory.php new file mode 100644 index 0000000..d6aea00 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/FSExt/Directory.php @@ -0,0 +1,212 @@ +path.'/'.$name; + file_put_contents($newPath, $data); + clearstatcache(true, $newPath); + + return '"'.sha1( + fileinode($newPath). + filesize($newPath). + filemtime($newPath) + ).'"'; + } + + /** + * Creates a new subdirectory. + * + * @param string $name + */ + public function createDirectory($name) + { + // We're not allowing dots + if ('.' == $name || '..' == $name) { + throw new DAV\Exception\Forbidden('Permission denied to . and ..'); + } + $newPath = $this->path.'/'.$name; + mkdir($newPath); + clearstatcache(true, $newPath); + } + + /** + * Returns a specific child node, referenced by its name. + * + * This method must throw Sabre\DAV\Exception\NotFound if the node does not + * exist. + * + * @param string $name + * + * @throws DAV\Exception\NotFound + * + * @return DAV\INode + */ + public function getChild($name) + { + $path = $this->path.'/'.$name; + + if (!file_exists($path)) { + throw new DAV\Exception\NotFound('File could not be located'); + } + if ('.' == $name || '..' == $name) { + throw new DAV\Exception\Forbidden('Permission denied to . and ..'); + } + if (is_dir($path)) { + return new self($path); + } else { + return new File($path); + } + } + + /** + * Checks if a child exists. + * + * @param string $name + * + * @return bool + */ + public function childExists($name) + { + if ('.' == $name || '..' == $name) { + throw new DAV\Exception\Forbidden('Permission denied to . and ..'); + } + $path = $this->path.'/'.$name; + + return file_exists($path); + } + + /** + * Returns an array with all the child nodes. + * + * @return DAV\INode[] + */ + public function getChildren() + { + $nodes = []; + $iterator = new \FilesystemIterator( + $this->path, + \FilesystemIterator::CURRENT_AS_SELF + | \FilesystemIterator::SKIP_DOTS + ); + + foreach ($iterator as $entry) { + $nodes[] = $this->getChild($entry->getFilename()); + } + + return $nodes; + } + + /** + * Deletes all files in this directory, and then itself. + * + * @return bool + */ + public function delete() + { + // Deleting all children + foreach ($this->getChildren() as $child) { + $child->delete(); + } + + // Removing the directory itself + rmdir($this->path); + + return true; + } + + /** + * Returns available diskspace information. + * + * @return array + */ + public function getQuotaInfo() + { + $total = disk_total_space(realpath($this->path)); + $free = disk_free_space(realpath($this->path)); + + return [ + $total - $free, + $free, + ]; + } + + /** + * Moves a node into this collection. + * + * It is up to the implementors to: + * 1. Create the new resource. + * 2. Remove the old resource. + * 3. Transfer any properties or other data. + * + * Generally you should make very sure that your collection can easily move + * the move. + * + * If you don't, just return false, which will trigger sabre/dav to handle + * the move itself. If you return true from this function, the assumption + * is that the move was successful. + * + * @param string $targetName new local file/collection name + * @param string $sourcePath Full path to source node + * @param DAV\INode $sourceNode Source node itself + * + * @return bool + */ + public function moveInto($targetName, $sourcePath, DAV\INode $sourceNode) + { + // We only support FSExt\Directory or FSExt\File objects, so + // anything else we want to quickly reject. + if (!$sourceNode instanceof self && !$sourceNode instanceof File) { + return false; + } + + // PHP allows us to access protected properties from other objects, as + // long as they are defined in a class that has a shared inheritance + // with the current class. + return rename($sourceNode->path, $this->path.'/'.$targetName); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/FSExt/File.php b/lib/composer/vendor/sabre/dav/lib/DAV/FSExt/File.php new file mode 100644 index 0000000..74849b5 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/FSExt/File.php @@ -0,0 +1,153 @@ +path, $data); + clearstatcache(true, $this->path); + + return $this->getETag(); + } + + /** + * Updates the file based on a range specification. + * + * The first argument is the data, which is either a readable stream + * resource or a string. + * + * The second argument is the type of update we're doing. + * This is either: + * * 1. append (default) + * * 2. update based on a start byte + * * 3. update based on an end byte + *; + * The third argument is the start or end byte. + * + * After a successful put operation, you may choose to return an ETag. The + * ETAG must always be surrounded by double-quotes. These quotes must + * appear in the actual string you're returning. + * + * Clients may use the ETag from a PUT request to later on make sure that + * when they update the file, the contents haven't changed in the mean + * time. + * + * @param resource|string $data + * @param int $rangeType + * @param int $offset + * + * @return string|null + */ + public function patch($data, $rangeType, $offset = null) + { + switch ($rangeType) { + case 1: + $f = fopen($this->path, 'a'); + break; + case 2: + $f = fopen($this->path, 'c'); + fseek($f, $offset); + break; + case 3: + $f = fopen($this->path, 'c'); + fseek($f, $offset, SEEK_END); + break; + default: + $f = fopen($this->path, 'a'); + break; + } + if (is_string($data)) { + fwrite($f, $data); + } else { + stream_copy_to_stream($data, $f); + } + fclose($f); + clearstatcache(true, $this->path); + + return $this->getETag(); + } + + /** + * Returns the data. + * + * @return resource + */ + public function get() + { + return fopen($this->path, 'r'); + } + + /** + * Delete the current file. + * + * @return bool + */ + public function delete() + { + return unlink($this->path); + } + + /** + * Returns the ETag for a file. + * + * An ETag is a unique identifier representing the current version of the file. If the file changes, the ETag MUST change. + * The ETag is an arbitrary string, but MUST be surrounded by double-quotes. + * + * Return null if the ETag can not effectively be determined + * + * @return string|null + */ + public function getETag() + { + return '"'.sha1( + fileinode($this->path). + filesize($this->path). + filemtime($this->path) + ).'"'; + } + + /** + * Returns the mime-type for a file. + * + * If null is returned, we'll assume application/octet-stream + * + * @return string|null + */ + public function getContentType() + { + return null; + } + + /** + * Returns the size of the file, in bytes. + * + * @return int + */ + public function getSize() + { + return filesize($this->path); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/File.php b/lib/composer/vendor/sabre/dav/lib/DAV/File.php new file mode 100644 index 0000000..daf83aa --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/File.php @@ -0,0 +1,93 @@ +locksFile = $locksFile; + } + + /** + * Returns a list of Sabre\DAV\Locks\LockInfo objects. + * + * This method should return all the locks for a particular uri, including + * locks that might be set on a parent uri. + * + * If returnChildLocks is set to true, this method should also look for + * any locks in the subtree of the uri for locks. + * + * @param string $uri + * @param bool $returnChildLocks + * + * @return array + */ + public function getLocks($uri, $returnChildLocks) + { + $newLocks = []; + + $locks = $this->getData(); + + foreach ($locks as $lock) { + if ($lock->uri === $uri || + //deep locks on parents + (0 != $lock->depth && 0 === strpos($uri, $lock->uri.'/')) || + + // locks on children + ($returnChildLocks && (0 === strpos($lock->uri, $uri.'/')))) { + $newLocks[] = $lock; + } + } + + // Checking if we can remove any of these locks + foreach ($newLocks as $k => $lock) { + if (time() > $lock->timeout + $lock->created) { + unset($newLocks[$k]); + } + } + + return $newLocks; + } + + /** + * Locks a uri. + * + * @param string $uri + * + * @return bool + */ + public function lock($uri, LockInfo $lockInfo) + { + // We're making the lock timeout 30 minutes + $lockInfo->timeout = 1800; + $lockInfo->created = time(); + $lockInfo->uri = $uri; + + $locks = $this->getData(); + + foreach ($locks as $k => $lock) { + if ( + ($lock->token == $lockInfo->token) || + (time() > $lock->timeout + $lock->created) + ) { + unset($locks[$k]); + } + } + $locks[] = $lockInfo; + $this->putData($locks); + + return true; + } + + /** + * Removes a lock from a uri. + * + * @param string $uri + * + * @return bool + */ + public function unlock($uri, LockInfo $lockInfo) + { + $locks = $this->getData(); + foreach ($locks as $k => $lock) { + if ($lock->token == $lockInfo->token) { + unset($locks[$k]); + $this->putData($locks); + + return true; + } + } + + return false; + } + + /** + * Loads the lockdata from the filesystem. + * + * @return array + */ + protected function getData() + { + if (!file_exists($this->locksFile)) { + return []; + } + + // opening up the file, and creating a shared lock + $handle = fopen($this->locksFile, 'r'); + flock($handle, LOCK_SH); + + // Reading data until the eof + $data = stream_get_contents($handle); + + // We're all good + flock($handle, LOCK_UN); + fclose($handle); + + // Unserializing and checking if the resource file contains data for this file + $data = unserialize($data); + if (!$data) { + return []; + } + + return $data; + } + + /** + * Saves the lockdata. + */ + protected function putData(array $newData) + { + // opening up the file, and creating an exclusive lock + $handle = fopen($this->locksFile, 'a+'); + flock($handle, LOCK_EX); + + // We can only truncate and rewind once the lock is acquired. + ftruncate($handle, 0); + rewind($handle); + + fwrite($handle, serialize($newData)); + flock($handle, LOCK_UN); + fclose($handle); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Locks/Backend/PDO.php b/lib/composer/vendor/sabre/dav/lib/DAV/Locks/Backend/PDO.php new file mode 100644 index 0000000..3f425f9 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Locks/Backend/PDO.php @@ -0,0 +1,172 @@ +pdo = $pdo; + } + + /** + * Returns a list of Sabre\DAV\Locks\LockInfo objects. + * + * This method should return all the locks for a particular uri, including + * locks that might be set on a parent uri. + * + * If returnChildLocks is set to true, this method should also look for + * any locks in the subtree of the uri for locks. + * + * @param string $uri + * @param bool $returnChildLocks + * + * @return array + */ + public function getLocks($uri, $returnChildLocks) + { + // NOTE: the following 10 lines or so could be easily replaced by + // pure sql. MySQL's non-standard string concatenation prevents us + // from doing this though. + $query = 'SELECT owner, token, timeout, created, scope, depth, uri FROM '.$this->tableName.' WHERE (created > (? - timeout)) AND ((uri = ?)'; + $params = [time(), $uri]; + + // We need to check locks for every part in the uri. + $uriParts = explode('/', $uri); + + // We already covered the last part of the uri + array_pop($uriParts); + + $currentPath = ''; + + foreach ($uriParts as $part) { + if ($currentPath) { + $currentPath .= '/'; + } + $currentPath .= $part; + + $query .= ' OR (depth!=0 AND uri = ?)'; + $params[] = $currentPath; + } + + if ($returnChildLocks) { + $query .= ' OR (uri LIKE ?)'; + $params[] = $uri.'/%'; + } + $query .= ')'; + + $stmt = $this->pdo->prepare($query); + $stmt->execute($params); + $result = $stmt->fetchAll(); + + $lockList = []; + foreach ($result as $row) { + $lockInfo = new LockInfo(); + $lockInfo->owner = $row['owner']; + $lockInfo->token = $row['token']; + $lockInfo->timeout = $row['timeout']; + $lockInfo->created = $row['created']; + $lockInfo->scope = $row['scope']; + $lockInfo->depth = $row['depth']; + $lockInfo->uri = $row['uri']; + $lockList[] = $lockInfo; + } + + return $lockList; + } + + /** + * Locks a uri. + * + * @param string $uri + * + * @return bool + */ + public function lock($uri, LockInfo $lockInfo) + { + // We're making the lock timeout 30 minutes + $lockInfo->timeout = 30 * 60; + $lockInfo->created = time(); + $lockInfo->uri = $uri; + + $locks = $this->getLocks($uri, false); + $exists = false; + foreach ($locks as $lock) { + if ($lock->token == $lockInfo->token) { + $exists = true; + } + } + + if ($exists) { + $stmt = $this->pdo->prepare('UPDATE '.$this->tableName.' SET owner = ?, timeout = ?, scope = ?, depth = ?, uri = ?, created = ? WHERE token = ?'); + $stmt->execute([ + $lockInfo->owner, + $lockInfo->timeout, + $lockInfo->scope, + $lockInfo->depth, + $uri, + $lockInfo->created, + $lockInfo->token, + ]); + } else { + $stmt = $this->pdo->prepare('INSERT INTO '.$this->tableName.' (owner,timeout,scope,depth,uri,created,token) VALUES (?,?,?,?,?,?,?)'); + $stmt->execute([ + $lockInfo->owner, + $lockInfo->timeout, + $lockInfo->scope, + $lockInfo->depth, + $uri, + $lockInfo->created, + $lockInfo->token, + ]); + } + + return true; + } + + /** + * Removes a lock from a uri. + * + * @param string $uri + * + * @return bool + */ + public function unlock($uri, LockInfo $lockInfo) + { + $stmt = $this->pdo->prepare('DELETE FROM '.$this->tableName.' WHERE uri = ? AND token = ?'); + $stmt->execute([$uri, $lockInfo->token]); + + return 1 === $stmt->rowCount(); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Locks/LockInfo.php b/lib/composer/vendor/sabre/dav/lib/DAV/Locks/LockInfo.php new file mode 100644 index 0000000..df82275 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Locks/LockInfo.php @@ -0,0 +1,82 @@ +addPlugin($lockPlugin); + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Plugin extends DAV\ServerPlugin +{ + /** + * locksBackend. + * + * @var Backend\BackendInterface + */ + protected $locksBackend; + + /** + * server. + * + * @var DAV\Server + */ + protected $server; + + /** + * __construct. + */ + public function __construct(Backend\BackendInterface $locksBackend) + { + $this->locksBackend = $locksBackend; + } + + /** + * Initializes the plugin. + * + * This method is automatically called by the Server class after addPlugin. + */ + public function initialize(DAV\Server $server) + { + $this->server = $server; + + $this->server->xml->elementMap['{DAV:}lockinfo'] = 'Sabre\\DAV\\Xml\\Request\\Lock'; + + $server->on('method:LOCK', [$this, 'httpLock']); + $server->on('method:UNLOCK', [$this, 'httpUnlock']); + $server->on('validateTokens', [$this, 'validateTokens']); + $server->on('propFind', [$this, 'propFind']); + $server->on('afterUnbind', [$this, 'afterUnbind']); + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using Sabre\DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'locks'; + } + + /** + * This method is called after most properties have been found + * it allows us to add in any Lock-related properties. + */ + public function propFind(DAV\PropFind $propFind, DAV\INode $node) + { + $propFind->handle('{DAV:}supportedlock', function () { + return new DAV\Xml\Property\SupportedLock(); + }); + $propFind->handle('{DAV:}lockdiscovery', function () use ($propFind) { + return new DAV\Xml\Property\LockDiscovery( + $this->getLocks($propFind->getPath()) + ); + }); + } + + /** + * Use this method to tell the server this plugin defines additional + * HTTP methods. + * + * This method is passed a uri. It should only return HTTP methods that are + * available for the specified uri. + * + * @param string $uri + * + * @return array + */ + public function getHTTPMethods($uri) + { + return ['LOCK', 'UNLOCK']; + } + + /** + * Returns a list of features for the HTTP OPTIONS Dav: header. + * + * In this case this is only the number 2. The 2 in the Dav: header + * indicates the server supports locks. + * + * @return array + */ + public function getFeatures() + { + return [2]; + } + + /** + * Returns all lock information on a particular uri. + * + * This function should return an array with Sabre\DAV\Locks\LockInfo objects. If there are no locks on a file, return an empty array. + * + * Additionally there is also the possibility of locks on parent nodes, so we'll need to traverse every part of the tree + * If the $returnChildLocks argument is set to true, we'll also traverse all the children of the object + * for any possible locks and return those as well. + * + * @param string $uri + * @param bool $returnChildLocks + * + * @return array + */ + public function getLocks($uri, $returnChildLocks = false) + { + return $this->locksBackend->getLocks($uri, $returnChildLocks); + } + + /** + * Locks an uri. + * + * The WebDAV lock request can be operated to either create a new lock on a file, or to refresh an existing lock + * If a new lock is created, a full XML body should be supplied, containing information about the lock such as the type + * of lock (shared or exclusive) and the owner of the lock + * + * If a lock is to be refreshed, no body should be supplied and there should be a valid If header containing the lock + * + * Additionally, a lock can be requested for a non-existent file. In these case we're obligated to create an empty file as per RFC4918:S7.3 + * + * @return bool + */ + public function httpLock(RequestInterface $request, ResponseInterface $response) + { + $uri = $request->getPath(); + + $existingLocks = $this->getLocks($uri); + + if ($body = $request->getBodyAsString()) { + // This is a new lock request + + $existingLock = null; + // Checking if there's already non-shared locks on the uri. + foreach ($existingLocks as $existingLock) { + if (LockInfo::EXCLUSIVE === $existingLock->scope) { + throw new DAV\Exception\ConflictingLock($existingLock); + } + } + + $lockInfo = $this->parseLockRequest($body); + $lockInfo->depth = $this->server->getHTTPDepth(); + $lockInfo->uri = $uri; + if ($existingLock && LockInfo::SHARED != $lockInfo->scope) { + throw new DAV\Exception\ConflictingLock($existingLock); + } + } else { + // Gonna check if this was a lock refresh. + $existingLocks = $this->getLocks($uri); + $conditions = $this->server->getIfConditions($request); + $found = null; + + foreach ($existingLocks as $existingLock) { + foreach ($conditions as $condition) { + foreach ($condition['tokens'] as $token) { + if ($token['token'] === 'opaquelocktoken:'.$existingLock->token) { + $found = $existingLock; + break 3; + } + } + } + } + + // If none were found, this request is in error. + if (is_null($found)) { + if ($existingLocks) { + throw new DAV\Exception\Locked(reset($existingLocks)); + } else { + throw new DAV\Exception\BadRequest('An xml body is required for lock requests'); + } + } + + // This must have been a lock refresh + $lockInfo = $found; + + // The resource could have been locked through another uri. + if ($uri != $lockInfo->uri) { + $uri = $lockInfo->uri; + } + } + + if ($timeout = $this->getTimeoutHeader()) { + $lockInfo->timeout = $timeout; + } + + $newFile = false; + + // If we got this far.. we should go check if this node actually exists. If this is not the case, we need to create it first + try { + $this->server->tree->getNodeForPath($uri); + + // We need to call the beforeWriteContent event for RFC3744 + // Edit: looks like this is not used, and causing problems now. + // + // See Issue 222 + // $this->server->emit('beforeWriteContent',array($uri)); + } catch (DAV\Exception\NotFound $e) { + // It didn't, lets create it + $this->server->createFile($uri, fopen('php://memory', 'r')); + $newFile = true; + } + + $this->lockNode($uri, $lockInfo); + + $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $response->setHeader('Lock-Token', 'token.'>'); + $response->setStatus($newFile ? 201 : 200); + $response->setBody($this->generateLockResponse($lockInfo)); + + // Returning false will interrupt the event chain and mark this method + // as 'handled'. + return false; + } + + /** + * Unlocks a uri. + * + * This WebDAV method allows you to remove a lock from a node. The client should provide a valid locktoken through the Lock-token http header + * The server should return 204 (No content) on success + */ + public function httpUnlock(RequestInterface $request, ResponseInterface $response) + { + $lockToken = $request->getHeader('Lock-Token'); + + // If the locktoken header is not supplied, we need to throw a bad request exception + if (!$lockToken) { + throw new DAV\Exception\BadRequest('No lock token was supplied'); + } + $path = $request->getPath(); + $locks = $this->getLocks($path); + + // Windows sometimes forgets to include < and > in the Lock-Token + // header + if ('<' !== $lockToken[0]) { + $lockToken = '<'.$lockToken.'>'; + } + + foreach ($locks as $lock) { + if ('token.'>' == $lockToken) { + $this->unlockNode($path, $lock); + $response->setHeader('Content-Length', '0'); + $response->setStatus(204); + + // Returning false will break the method chain, and mark the + // method as 'handled'. + return false; + } + } + + // If we got here, it means the locktoken was invalid + throw new DAV\Exception\LockTokenMatchesRequestUri(); + } + + /** + * This method is called after a node is deleted. + * + * We use this event to clean up any locks that still exist on the node. + * + * @param string $path + */ + public function afterUnbind($path) + { + $locks = $this->getLocks($path, $includeChildren = true); + foreach ($locks as $lock) { + // don't delete a lock on a parent dir + if (0 !== strpos($lock->uri, $path)) { + continue; + } + $this->unlockNode($path, $lock); + } + } + + /** + * Locks a uri. + * + * All the locking information is supplied in the lockInfo object. The object has a suggested timeout, but this can be safely ignored + * It is important that if the existing timeout is ignored, the property is overwritten, as this needs to be sent back to the client + * + * @param string $uri + * + * @return bool + */ + public function lockNode($uri, LockInfo $lockInfo) + { + if (!$this->server->emit('beforeLock', [$uri, $lockInfo])) { + return; + } + + return $this->locksBackend->lock($uri, $lockInfo); + } + + /** + * Unlocks a uri. + * + * This method removes a lock from a uri. It is assumed all the supplied information is correct and verified + * + * @param string $uri + * + * @return bool + */ + public function unlockNode($uri, LockInfo $lockInfo) + { + if (!$this->server->emit('beforeUnlock', [$uri, $lockInfo])) { + return; + } + + return $this->locksBackend->unlock($uri, $lockInfo); + } + + /** + * Returns the contents of the HTTP Timeout header. + * + * The method formats the header into an integer. + * + * @return int + */ + public function getTimeoutHeader() + { + $header = $this->server->httpRequest->getHeader('Timeout'); + + if ($header) { + if (0 === stripos($header, 'second-')) { + $header = (int) (substr($header, 7)); + } elseif (0 === stripos($header, 'infinite')) { + $header = LockInfo::TIMEOUT_INFINITE; + } else { + throw new DAV\Exception\BadRequest('Invalid HTTP timeout header'); + } + } else { + $header = 0; + } + + return $header; + } + + /** + * Generates the response for successful LOCK requests. + * + * @return string + */ + protected function generateLockResponse(LockInfo $lockInfo) + { + return $this->server->xml->write('{DAV:}prop', [ + '{DAV:}lockdiscovery' => new DAV\Xml\Property\LockDiscovery([$lockInfo]), + ], $this->server->getBaseUri()); + } + + /** + * The validateTokens event is triggered before every request. + * + * It's a moment where this plugin can check all the supplied lock tokens + * in the If: header, and check if they are valid. + * + * In addition, it will also ensure that it checks any missing lokens that + * must be present in the request, and reject requests without the proper + * tokens. + * + * @param mixed $conditions + */ + public function validateTokens(RequestInterface $request, &$conditions) + { + // First we need to gather a list of locks that must be satisfied. + $mustLocks = []; + $method = $request->getMethod(); + + // Methods not in that list are operations that doesn't alter any + // resources, and we don't need to check the lock-states for. + switch ($method) { + case 'DELETE': + $mustLocks = array_merge($mustLocks, $this->getLocks( + $request->getPath(), + true + )); + break; + case 'MKCOL': + case 'MKCALENDAR': + case 'PROPPATCH': + case 'PUT': + case 'PATCH': + $mustLocks = array_merge($mustLocks, $this->getLocks( + $request->getPath(), + false + )); + break; + case 'MOVE': + $mustLocks = array_merge($mustLocks, $this->getLocks( + $request->getPath(), + true + )); + $mustLocks = array_merge($mustLocks, $this->getLocks( + $this->server->calculateUri($request->getHeader('Destination')), + false + )); + break; + case 'COPY': + $mustLocks = array_merge($mustLocks, $this->getLocks( + $this->server->calculateUri($request->getHeader('Destination')), + false + )); + break; + case 'LOCK': + //Temporary measure.. figure out later why this is needed + // Here we basically ignore all incoming tokens... + foreach ($conditions as $ii => $condition) { + foreach ($condition['tokens'] as $jj => $token) { + $conditions[$ii]['tokens'][$jj]['validToken'] = true; + } + } + + return; + } + + // It's possible that there's identical locks, because of shared + // parents. We're removing the duplicates here. + $tmp = []; + foreach ($mustLocks as $lock) { + $tmp[$lock->token] = $lock; + } + $mustLocks = array_values($tmp); + + foreach ($conditions as $kk => $condition) { + foreach ($condition['tokens'] as $ii => $token) { + // Lock tokens always start with opaquelocktoken: + if ('opaquelocktoken:' !== substr($token['token'], 0, 16)) { + continue; + } + + $checkToken = substr($token['token'], 16); + // Looping through our list with locks. + foreach ($mustLocks as $jj => $mustLock) { + if ($mustLock->token == $checkToken) { + // We have a match! + // Removing this one from mustlocks + unset($mustLocks[$jj]); + + // Marking the condition as valid. + $conditions[$kk]['tokens'][$ii]['validToken'] = true; + + // Advancing to the next token + continue 2; + } + } + + // If we got here, it means that there was a + // lock-token, but it was not in 'mustLocks'. + // + // This is an edge-case, as it could mean that token + // was specified with a url that was not 'required' to + // check. So we're doing one extra lookup to make sure + // we really don't know this token. + // + // This also gets triggered when the user specified a + // lock-token that was expired. + $oddLocks = $this->getLocks($condition['uri']); + foreach ($oddLocks as $oddLock) { + if ($oddLock->token === $checkToken) { + // We have a hit! + $conditions[$kk]['tokens'][$ii]['validToken'] = true; + continue 2; + } + } + + // If we get all the way here, the lock-token was + // really unknown. + } + } + + // If there's any locks left in the 'mustLocks' array, it means that + // the resource was locked and we must block it. + if ($mustLocks) { + throw new DAV\Exception\Locked(reset($mustLocks)); + } + } + + /** + * Parses a webdav lock xml body, and returns a new Sabre\DAV\Locks\LockInfo object. + * + * @param string $body + * + * @return LockInfo + */ + protected function parseLockRequest($body) + { + $result = $this->server->xml->expect( + '{DAV:}lockinfo', + $body + ); + + $lockInfo = new LockInfo(); + + $lockInfo->owner = $result->owner; + $lockInfo->token = DAV\UUIDUtil::getUUID(); + $lockInfo->scope = $result->scope; + + return $lockInfo; + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'The locks plugin turns this server into a class-2 WebDAV server and adds support for LOCK and UNLOCK', + 'link' => 'http://sabre.io/dav/locks/', + ]; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/MkCol.php b/lib/composer/vendor/sabre/dav/lib/DAV/MkCol.php new file mode 100644 index 0000000..f3c5ea5 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/MkCol.php @@ -0,0 +1,71 @@ +resourceType = $resourceType; + parent::__construct($mutations); + } + + /** + * Returns the resourcetype of the new collection. + * + * @return string[] + */ + public function getResourceType() + { + return $this->resourceType; + } + + /** + * Returns true or false if the MKCOL operation has at least the specified + * resource type. + * + * If the resourcetype is specified as an array, all resourcetypes are + * checked. + * + * @param string|string[] $resourceType + * + * @return bool + */ + public function hasResourceType($resourceType) + { + return 0 === count(array_diff((array) $resourceType, $this->resourceType)); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Mount/Plugin.php b/lib/composer/vendor/sabre/dav/lib/DAV/Mount/Plugin.php new file mode 100644 index 0000000..b7f4851 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Mount/Plugin.php @@ -0,0 +1,78 @@ +server = $server; + $this->server->on('method:GET', [$this, 'httpGet'], 90); + } + + /** + * 'beforeMethod' event handles. This event handles intercepts GET requests ending + * with ?mount. + * + * @return bool + */ + public function httpGet(RequestInterface $request, ResponseInterface $response) + { + $queryParams = $request->getQueryParameters(); + if (!array_key_exists('mount', $queryParams)) { + return; + } + + $currentUri = $request->getAbsoluteUrl(); + + // Stripping off everything after the ? + list($currentUri) = explode('?', $currentUri); + + $this->davMount($response, $currentUri); + + // Returning false to break the event chain + return false; + } + + /** + * Generates the davmount response. + * + * @param string $uri absolute uri + */ + public function davMount(ResponseInterface $response, $uri) + { + $response->setStatus(200); + $response->setHeader('Content-Type', 'application/davmount+xml'); + ob_start(); + echo '', "\n"; + echo "\n"; + echo ' ', htmlspecialchars($uri, ENT_NOQUOTES, 'UTF-8'), "\n"; + echo ''; + $response->setBody(ob_get_clean()); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Node.php b/lib/composer/vendor/sabre/dav/lib/DAV/Node.php new file mode 100644 index 0000000..948060d --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Node.php @@ -0,0 +1,51 @@ +addPlugin($patchPlugin); + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Jean-Tiare LE BIGOT (http://www.jtlebi.fr/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Plugin extends DAV\ServerPlugin +{ + const RANGE_APPEND = 1; + const RANGE_START = 2; + const RANGE_END = 3; + + /** + * Reference to server. + * + * @var DAV\Server + */ + protected $server; + + /** + * Initializes the plugin. + * + * This method is automatically called by the Server class after addPlugin. + */ + public function initialize(DAV\Server $server) + { + $this->server = $server; + $server->on('method:PATCH', [$this, 'httpPatch']); + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'partialupdate'; + } + + /** + * Use this method to tell the server this plugin defines additional + * HTTP methods. + * + * This method is passed a uri. It should only return HTTP methods that are + * available for the specified uri. + * + * We claim to support PATCH method (partirl update) if and only if + * - the node exist + * - the node implements our partial update interface + * + * @param string $uri + * + * @return array + */ + public function getHTTPMethods($uri) + { + $tree = $this->server->tree; + + if ($tree->nodeExists($uri)) { + $node = $tree->getNodeForPath($uri); + if ($node instanceof IPatchSupport) { + return ['PATCH']; + } + } + + return []; + } + + /** + * Returns a list of features for the HTTP OPTIONS Dav: header. + * + * @return array + */ + public function getFeatures() + { + return ['sabredav-partialupdate']; + } + + /** + * Patch an uri. + * + * The WebDAV patch request can be used to modify only a part of an + * existing resource. If the resource does not exist yet and the first + * offset is not 0, the request fails + */ + public function httpPatch(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + + // Get the node. Will throw a 404 if not found + $node = $this->server->tree->getNodeForPath($path); + if (!$node instanceof IPatchSupport) { + throw new DAV\Exception\MethodNotAllowed('The target resource does not support the PATCH method.'); + } + + $range = $this->getHTTPUpdateRange($request); + + if (!$range) { + throw new DAV\Exception\BadRequest('No valid "X-Update-Range" found in the headers'); + } + + $contentType = strtolower( + (string) $request->getHeader('Content-Type') + ); + + if ('application/x-sabredav-partialupdate' != $contentType) { + throw new DAV\Exception\UnsupportedMediaType('Unknown Content-Type header "'.$contentType.'"'); + } + + $len = $this->server->httpRequest->getHeader('Content-Length'); + if (!$len) { + throw new DAV\Exception\LengthRequired('A Content-Length header is required'); + } + switch ($range[0]) { + case self::RANGE_START: + // Calculate the end-range if it doesn't exist. + if (!$range[2]) { + $range[2] = $range[1] + $len - 1; + } else { + if ($range[2] < $range[1]) { + throw new DAV\Exception\RequestedRangeNotSatisfiable('The end offset ('.$range[2].') is lower than the start offset ('.$range[1].')'); + } + if ($range[2] - $range[1] + 1 != $len) { + throw new DAV\Exception\RequestedRangeNotSatisfiable('Actual data length ('.$len.') is not consistent with begin ('.$range[1].') and end ('.$range[2].') offsets'); + } + } + break; + } + + if (!$this->server->emit('beforeWriteContent', [$path, $node, null])) { + return; + } + + $body = $this->server->httpRequest->getBody(); + + $etag = $node->patch($body, $range[0], isset($range[1]) ? $range[1] : null); + + $this->server->emit('afterWriteContent', [$path, $node]); + + $response->setHeader('Content-Length', '0'); + if ($etag) { + $response->setHeader('ETag', $etag); + } + $response->setStatus(204); + + // Breaks the event chain + return false; + } + + /** + * Returns the HTTP custom range update header. + * + * This method returns null if there is no well-formed HTTP range request + * header. It returns array(1) if it was an append request, array(2, + * $start, $end) if it's a start and end range, lastly it's array(3, + * $endoffset) if the offset was negative, and should be calculated from + * the end of the file. + * + * Examples: + * + * null - invalid + * [1] - append + * [2,10,15] - update bytes 10, 11, 12, 13, 14, 15 + * [2,10,null] - update bytes 10 until the end of the patch body + * [3,-5] - update from 5 bytes from the end of the file. + * + * @return array|null + */ + public function getHTTPUpdateRange(RequestInterface $request) + { + $range = $request->getHeader('X-Update-Range'); + if (is_null($range)) { + return null; + } + + // Matching "Range: bytes=1234-5678: both numbers are optional + + if (!preg_match('/^(append)|(?:bytes=([0-9]+)-([0-9]*))|(?:bytes=(-[0-9]+))$/i', $range, $matches)) { + return null; + } + + if ('append' === $matches[1]) { + return [self::RANGE_APPEND]; + } elseif (strlen($matches[2]) > 0) { + return [self::RANGE_START, (int) $matches[2], (int) $matches[3] ?: null]; + } else { + return [self::RANGE_END, (int) $matches[4]]; + } + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/PropFind.php b/lib/composer/vendor/sabre/dav/lib/DAV/PropFind.php new file mode 100644 index 0000000..e9ffb07 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/PropFind.php @@ -0,0 +1,335 @@ +path = $path; + $this->properties = $properties; + $this->depth = $depth; + $this->requestType = $requestType; + + if (self::ALLPROPS === $requestType) { + $this->properties = [ + '{DAV:}getlastmodified', + '{DAV:}getcontentlength', + '{DAV:}resourcetype', + '{DAV:}quota-used-bytes', + '{DAV:}quota-available-bytes', + '{DAV:}getetag', + '{DAV:}getcontenttype', + ]; + } + + foreach ($this->properties as $propertyName) { + // Seeding properties with 404's. + $this->result[$propertyName] = [404, null]; + } + $this->itemsLeft = count($this->result); + } + + /** + * Handles a specific property. + * + * This method checks whether the specified property was requested in this + * PROPFIND request, and if so, it will call the callback and use the + * return value for it's value. + * + * Example: + * + * $propFind->handle('{DAV:}displayname', function() { + * return 'hello'; + * }); + * + * Note that handle will only work the first time. If null is returned, the + * value is ignored. + * + * It's also possible to not pass a callback, but immediately pass a value + * + * @param string $propertyName + * @param mixed $valueOrCallBack + */ + public function handle($propertyName, $valueOrCallBack) + { + if ($this->itemsLeft && isset($this->result[$propertyName]) && 404 === $this->result[$propertyName][0]) { + if (is_callable($valueOrCallBack)) { + $value = $valueOrCallBack(); + } else { + $value = $valueOrCallBack; + } + if (!is_null($value)) { + --$this->itemsLeft; + $this->result[$propertyName] = [200, $value]; + } + } + } + + /** + * Sets the value of the property. + * + * If status is not supplied, the status will default to 200 for non-null + * properties, and 404 for null properties. + * + * @param string $propertyName + * @param mixed $value + * @param int $status + */ + public function set($propertyName, $value, $status = null) + { + if (is_null($status)) { + $status = is_null($value) ? 404 : 200; + } + // If this is an ALLPROPS request and the property is + // unknown, add it to the result; else ignore it: + if (!isset($this->result[$propertyName])) { + if (self::ALLPROPS === $this->requestType) { + $this->result[$propertyName] = [$status, $value]; + } + + return; + } + if (404 !== $status && 404 === $this->result[$propertyName][0]) { + --$this->itemsLeft; + } elseif (404 === $status && 404 !== $this->result[$propertyName][0]) { + ++$this->itemsLeft; + } + $this->result[$propertyName] = [$status, $value]; + } + + /** + * Returns the current value for a property. + * + * @param string $propertyName + * + * @return mixed + */ + public function get($propertyName) + { + return isset($this->result[$propertyName]) ? $this->result[$propertyName][1] : null; + } + + /** + * Returns the current status code for a property name. + * + * If the property does not appear in the list of requested properties, + * null will be returned. + * + * @param string $propertyName + * + * @return int|null + */ + public function getStatus($propertyName) + { + return isset($this->result[$propertyName]) ? $this->result[$propertyName][0] : null; + } + + /** + * Updates the path for this PROPFIND. + * + * @param string $path + */ + public function setPath($path) + { + $this->path = $path; + } + + /** + * Returns the path this PROPFIND request is for. + * + * @return string + */ + public function getPath() + { + return $this->path; + } + + /** + * Returns the depth of this propfind request. + * + * @return int + */ + public function getDepth() + { + return $this->depth; + } + + /** + * Updates the depth of this propfind request. + * + * @param int $depth + */ + public function setDepth($depth) + { + $this->depth = $depth; + } + + /** + * Returns all propertynames that have a 404 status, and thus don't have a + * value yet. + * + * @return array + */ + public function get404Properties() + { + if (0 === $this->itemsLeft) { + return []; + } + $result = []; + foreach ($this->result as $propertyName => $stuff) { + if (404 === $stuff[0]) { + $result[] = $propertyName; + } + } + + return $result; + } + + /** + * Returns the full list of requested properties. + * + * This returns just their names, not a status or value. + * + * @return array + */ + public function getRequestedProperties() + { + return $this->properties; + } + + /** + * Returns true if this was an '{DAV:}allprops' request. + * + * @return bool + */ + public function isAllProps() + { + return self::ALLPROPS === $this->requestType; + } + + /** + * Returns a result array that's often used in multistatus responses. + * + * The array uses status codes as keys, and property names and value pairs + * as the value of the top array.. such as : + * + * [ + * 200 => [ '{DAV:}displayname' => 'foo' ], + * ] + * + * @return array + */ + public function getResultForMultiStatus() + { + $r = [ + 200 => [], + 404 => [], + ]; + foreach ($this->result as $propertyName => $info) { + if (!isset($r[$info[0]])) { + $r[$info[0]] = [$propertyName => $info[1]]; + } else { + $r[$info[0]][$propertyName] = $info[1]; + } + } + // Removing the 404's for multi-status requests. + if (self::ALLPROPS === $this->requestType) { + unset($r[404]); + } + + return $r; + } + + /** + * The path that we're fetching properties for. + * + * @var string + */ + protected $path; + + /** + * The Depth of the request. + * + * 0 means only the current item. 1 means the current item + its children. + * It can also be DEPTH_INFINITY if this is enabled in the server. + * + * @var int + */ + protected $depth = 0; + + /** + * The type of request. See the TYPE constants. + */ + protected $requestType; + + /** + * A list of requested properties. + * + * @var array + */ + protected $properties = []; + + /** + * The result of the operation. + * + * The keys in this array are property names. + * The values are an array with two elements: the http status code and then + * optionally a value. + * + * Example: + * + * [ + * "{DAV:}owner" : [404], + * "{DAV:}displayname" : [200, "Admin"] + * ] + * + * @var array + */ + protected $result = []; + + /** + * This is used as an internal counter for the number of properties that do + * not yet have a value. + * + * @var int + */ + protected $itemsLeft; +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/PropPatch.php b/lib/composer/vendor/sabre/dav/lib/DAV/PropPatch.php new file mode 100644 index 0000000..092909d --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/PropPatch.php @@ -0,0 +1,337 @@ +mutations = $mutations; + } + + /** + * Call this function if you wish to handle updating certain properties. + * For instance, your class may be responsible for handling updates for the + * {DAV:}displayname property. + * + * In that case, call this method with the first argument + * "{DAV:}displayname" and a second argument that's a method that does the + * actual updating. + * + * It's possible to specify more than one property as an array. + * + * The callback must return a boolean or an it. If the result is true, the + * operation was considered successful. If it's false, it's consided + * failed. + * + * If the result is an integer, we'll use that integer as the http status + * code associated with the operation. + * + * @param string|string[] $properties + */ + public function handle($properties, callable $callback) + { + $usedProperties = []; + foreach ((array) $properties as $propertyName) { + if (array_key_exists($propertyName, $this->mutations) && !isset($this->result[$propertyName])) { + $usedProperties[] = $propertyName; + // HTTP Accepted + $this->result[$propertyName] = 202; + } + } + + // Only registering if there's any unhandled properties. + if (!$usedProperties) { + return; + } + $this->propertyUpdateCallbacks[] = [ + // If the original argument to this method was a string, we need + // to also make sure that it stays that way, so the commit function + // knows how to format the arguments to the callback. + is_string($properties) ? $properties : $usedProperties, + $callback, + ]; + } + + /** + * Call this function if you wish to handle _all_ properties that haven't + * been handled by anything else yet. Note that you effectively claim with + * this that you promise to process _all_ properties that are coming in. + */ + public function handleRemaining(callable $callback) + { + $properties = $this->getRemainingMutations(); + if (!$properties) { + // Nothing to do, don't register callback + return; + } + + foreach ($properties as $propertyName) { + // HTTP Accepted + $this->result[$propertyName] = 202; + + $this->propertyUpdateCallbacks[] = [ + $properties, + $callback, + ]; + } + } + + /** + * Sets the result code for one or more properties. + * + * @param string|string[] $properties + * @param int $resultCode + */ + public function setResultCode($properties, $resultCode) + { + foreach ((array) $properties as $propertyName) { + $this->result[$propertyName] = $resultCode; + } + + if ($resultCode >= 400) { + $this->failed = true; + } + } + + /** + * Sets the result code for all properties that did not have a result yet. + * + * @param int $resultCode + */ + public function setRemainingResultCode($resultCode) + { + $this->setResultCode( + $this->getRemainingMutations(), + $resultCode + ); + } + + /** + * Returns the list of properties that don't have a result code yet. + * + * This method returns a list of property names, but not its values. + * + * @return string[] + */ + public function getRemainingMutations() + { + $remaining = []; + foreach ($this->mutations as $propertyName => $propValue) { + if (!isset($this->result[$propertyName])) { + $remaining[] = $propertyName; + } + } + + return $remaining; + } + + /** + * Returns the list of properties that don't have a result code yet. + * + * This method returns list of properties and their values. + * + * @return array + */ + public function getRemainingValues() + { + $remaining = []; + foreach ($this->mutations as $propertyName => $propValue) { + if (!isset($this->result[$propertyName])) { + $remaining[$propertyName] = $propValue; + } + } + + return $remaining; + } + + /** + * Performs the actual update, and calls all callbacks. + * + * This method returns true or false depending on if the operation was + * successful. + * + * @return bool + */ + public function commit() + { + // First we validate if every property has a handler + foreach ($this->mutations as $propertyName => $value) { + if (!isset($this->result[$propertyName])) { + $this->failed = true; + $this->result[$propertyName] = 403; + } + } + + foreach ($this->propertyUpdateCallbacks as $callbackInfo) { + if ($this->failed) { + break; + } + if (is_string($callbackInfo[0])) { + $this->doCallbackSingleProp($callbackInfo[0], $callbackInfo[1]); + } else { + $this->doCallbackMultiProp($callbackInfo[0], $callbackInfo[1]); + } + } + + /* + * If anywhere in this operation updating a property failed, we must + * update all other properties accordingly. + */ + if ($this->failed) { + foreach ($this->result as $propertyName => $status) { + if (202 === $status) { + // Failed dependency + $this->result[$propertyName] = 424; + } + } + } + + return !$this->failed; + } + + /** + * Executes a property callback with the single-property syntax. + * + * @param string $propertyName + */ + private function doCallBackSingleProp($propertyName, callable $callback) + { + $result = $callback($this->mutations[$propertyName]); + if (is_bool($result)) { + if ($result) { + if (is_null($this->mutations[$propertyName])) { + // Delete + $result = 204; + } else { + // Update + $result = 200; + } + } else { + // Fail + $result = 403; + } + } + if (!is_int($result)) { + throw new UnexpectedValueException('A callback sent to handle() did not return an int or a bool'); + } + $this->result[$propertyName] = $result; + if ($result >= 400) { + $this->failed = true; + } + } + + /** + * Executes a property callback with the multi-property syntax. + */ + private function doCallBackMultiProp(array $propertyList, callable $callback) + { + $argument = []; + foreach ($propertyList as $propertyName) { + $argument[$propertyName] = $this->mutations[$propertyName]; + } + + $result = $callback($argument); + + if (is_array($result)) { + foreach ($propertyList as $propertyName) { + if (!isset($result[$propertyName])) { + $resultCode = 500; + } else { + $resultCode = $result[$propertyName]; + } + if ($resultCode >= 400) { + $this->failed = true; + } + $this->result[$propertyName] = $resultCode; + } + } elseif (true === $result) { + // Success + foreach ($argument as $propertyName => $propertyValue) { + $this->result[$propertyName] = is_null($propertyValue) ? 204 : 200; + } + } elseif (false === $result) { + // Fail :( + $this->failed = true; + foreach ($propertyList as $propertyName) { + $this->result[$propertyName] = 403; + } + } else { + throw new UnexpectedValueException('A callback sent to handle() did not return an array or a bool'); + } + } + + /** + * Returns the result of the operation. + * + * @return array + */ + public function getResult() + { + return $this->result; + } + + /** + * Returns the full list of mutations. + * + * @return array + */ + public function getMutations() + { + return $this->mutations; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/BackendInterface.php b/lib/composer/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/BackendInterface.php new file mode 100644 index 0000000..64a8825 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/BackendInterface.php @@ -0,0 +1,75 @@ +isAllProps(). + * + * @param string $path + */ + public function propFind($path, PropFind $propFind); + + /** + * Updates properties for a path. + * + * This method received a PropPatch object, which contains all the + * information about the update. + * + * Usually you would want to call 'handleRemaining' on this object, to get; + * a list of all properties that need to be stored. + * + * @param string $path + */ + public function propPatch($path, PropPatch $propPatch); + + /** + * This method is called after a node is deleted. + * + * This allows a backend to clean up all associated properties. + * + * The delete method will get called once for the deletion of an entire + * tree. + * + * @param string $path + */ + public function delete($path); + + /** + * This method is called after a successful MOVE. + * + * This should be used to migrate all properties from one path to another. + * Note that entire collections may be moved, so ensure that all properties + * for children are also moved along. + * + * @param string $source + * @param string $destination + */ + public function move($source, $destination); +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php b/lib/composer/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php new file mode 100644 index 0000000..8960331 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php @@ -0,0 +1,224 @@ +pdo = $pdo; + } + + /** + * Fetches properties for a path. + * + * This method received a PropFind object, which contains all the + * information about the properties that need to be fetched. + * + * Usually you would just want to call 'get404Properties' on this object, + * as this will give you the _exact_ list of properties that need to be + * fetched, and haven't yet. + * + * However, you can also support the 'allprops' property here. In that + * case, you should check for $propFind->isAllProps(). + * + * @param string $path + */ + public function propFind($path, PropFind $propFind) + { + if (!$propFind->isAllProps() && 0 === count($propFind->get404Properties())) { + return; + } + + $query = 'SELECT name, value, valuetype FROM '.$this->tableName.' WHERE path = ?'; + $stmt = $this->pdo->prepare($query); + $stmt->execute([$path]); + + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + if ('resource' === gettype($row['value'])) { + $row['value'] = stream_get_contents($row['value']); + } + switch ($row['valuetype']) { + case null: + case self::VT_STRING: + $propFind->set($row['name'], $row['value']); + break; + case self::VT_XML: + $propFind->set($row['name'], new Complex($row['value'])); + break; + case self::VT_OBJECT: + $propFind->set($row['name'], unserialize($row['value'])); + break; + } + } + } + + /** + * Updates properties for a path. + * + * This method received a PropPatch object, which contains all the + * information about the update. + * + * Usually you would want to call 'handleRemaining' on this object, to get; + * a list of all properties that need to be stored. + * + * @param string $path + */ + public function propPatch($path, PropPatch $propPatch) + { + $propPatch->handleRemaining(function ($properties) use ($path) { + if ('pgsql' === $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME)) { + $updateSql = <<tableName} (path, name, valuetype, value) +VALUES (:path, :name, :valuetype, :value) +ON CONFLICT (path, name) +DO UPDATE SET valuetype = :valuetype, value = :value +SQL; + } else { + $updateSql = <<tableName} (path, name, valuetype, value) +VALUES (:path, :name, :valuetype, :value) +SQL; + } + + $updateStmt = $this->pdo->prepare($updateSql); + $deleteStmt = $this->pdo->prepare('DELETE FROM '.$this->tableName.' WHERE path = ? AND name = ?'); + + foreach ($properties as $name => $value) { + if (!is_null($value)) { + if (is_scalar($value)) { + $valueType = self::VT_STRING; + } elseif ($value instanceof Complex) { + $valueType = self::VT_XML; + $value = $value->getXml(); + } else { + $valueType = self::VT_OBJECT; + $value = serialize($value); + } + + $updateStmt->bindParam('path', $path, \PDO::PARAM_STR); + $updateStmt->bindParam('name', $name, \PDO::PARAM_STR); + $updateStmt->bindParam('valuetype', $valueType, \PDO::PARAM_INT); + $updateStmt->bindParam('value', $value, \PDO::PARAM_LOB); + + $updateStmt->execute(); + } else { + $deleteStmt->execute([$path, $name]); + } + } + + return true; + }); + } + + /** + * This method is called after a node is deleted. + * + * This allows a backend to clean up all associated properties. + * + * The delete method will get called once for the deletion of an entire + * tree. + * + * @param string $path + */ + public function delete($path) + { + $stmt = $this->pdo->prepare('DELETE FROM '.$this->tableName." WHERE path = ? OR path LIKE ? ESCAPE '='"); + $childPath = strtr( + $path, + [ + '=' => '==', + '%' => '=%', + '_' => '=_', + ] + ).'/%'; + + $stmt->execute([$path, $childPath]); + } + + /** + * This method is called after a successful MOVE. + * + * This should be used to migrate all properties from one path to another. + * Note that entire collections may be moved, so ensure that all properties + * for children are also moved along. + * + * @param string $source + * @param string $destination + */ + public function move($source, $destination) + { + // I don't know a way to write this all in a single sql query that's + // also compatible across db engines, so we're letting PHP do all the + // updates. Much slower, but it should still be pretty fast in most + // cases. + $select = $this->pdo->prepare('SELECT id, path FROM '.$this->tableName.' WHERE path = ? OR path LIKE ?'); + $select->execute([$source, $source.'/%']); + + $update = $this->pdo->prepare('UPDATE '.$this->tableName.' SET path = ? WHERE id = ?'); + while ($row = $select->fetch(\PDO::FETCH_ASSOC)) { + // Sanity check. SQL may select too many records, such as records + // with different cases. + if ($row['path'] !== $source && 0 !== strpos($row['path'], $source.'/')) { + continue; + } + + $trailingPart = substr($row['path'], strlen($source) + 1); + $newPath = $destination; + if ($trailingPart) { + $newPath .= '/'.$trailingPart; + } + $update->execute([$newPath, $row['id']]); + } + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/PropertyStorage/Plugin.php b/lib/composer/vendor/sabre/dav/lib/DAV/PropertyStorage/Plugin.php new file mode 100644 index 0000000..da47ec9 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/PropertyStorage/Plugin.php @@ -0,0 +1,176 @@ +backend = $backend; + } + + /** + * This initializes the plugin. + * + * This function is called by Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + */ + public function initialize(Server $server) + { + $server->on('propFind', [$this, 'propFind'], 130); + $server->on('propPatch', [$this, 'propPatch'], 300); + $server->on('afterMove', [$this, 'afterMove']); + $server->on('afterUnbind', [$this, 'afterUnbind']); + } + + /** + * Called during PROPFIND operations. + * + * If there's any requested properties that don't have a value yet, this + * plugin will look in the property storage backend to find them. + */ + public function propFind(PropFind $propFind, INode $node) + { + $path = $propFind->getPath(); + $pathFilter = $this->pathFilter; + if ($pathFilter && !$pathFilter($path)) { + return; + } + $this->backend->propFind($propFind->getPath(), $propFind); + } + + /** + * Called during PROPPATCH operations. + * + * If there's any updated properties that haven't been stored, the + * propertystorage backend can handle it. + * + * @param string $path + */ + public function propPatch($path, PropPatch $propPatch) + { + $pathFilter = $this->pathFilter; + if ($pathFilter && !$pathFilter($path)) { + return; + } + $this->backend->propPatch($path, $propPatch); + } + + /** + * Called after a node is deleted. + * + * This allows the backend to clean up any properties still in the + * database. + * + * @param string $path + */ + public function afterUnbind($path) + { + $pathFilter = $this->pathFilter; + if ($pathFilter && !$pathFilter($path)) { + return; + } + $this->backend->delete($path); + } + + /** + * Called after a node is moved. + * + * This allows the backend to move all the associated properties. + * + * @param string $source + * @param string $destination + */ + public function afterMove($source, $destination) + { + $pathFilter = $this->pathFilter; + if ($pathFilter && !$pathFilter($source)) { + return; + } + // If the destination is filtered, afterUnbind will handle cleaning up + // the properties. + if ($pathFilter && !$pathFilter($destination)) { + return; + } + + $this->backend->move($source, $destination); + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using \Sabre\DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'property-storage'; + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'This plugin allows any arbitrary WebDAV property to be set on any resource.', + 'link' => 'http://sabre.io/dav/property-storage/', + ]; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Server.php b/lib/composer/vendor/sabre/dav/lib/DAV/Server.php new file mode 100644 index 0000000..3133e54 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Server.php @@ -0,0 +1,1682 @@ + '{DAV:}collection', + ]; + + /** + * This property allows the usage of Depth: infinity on PROPFIND requests. + * + * By default Depth: infinity is treated as Depth: 1. Allowing Depth: + * infinity is potentially risky, as it allows a single client to do a full + * index of the webdav server, which is an easy DoS attack vector. + * + * Only turn this on if you know what you're doing. + * + * @var bool + */ + public $enablePropfindDepthInfinity = false; + + /** + * Reference to the XML utility object. + * + * @var Xml\Service + */ + public $xml; + + /** + * If this setting is turned off, SabreDAV's version number will be hidden + * from various places. + * + * Some people feel this is a good security measure. + * + * @var bool + */ + public static $exposeVersion = true; + + /** + * If this setting is turned on, any multi status response on any PROPFIND will be streamed to the output buffer. + * This will be beneficial for large result sets which will no longer consume a large amount of memory as well as + * send back data to the client earlier. + * + * @var bool + */ + public static $streamMultiStatus = false; + + /** + * Sets up the server. + * + * If a Sabre\DAV\Tree object is passed as an argument, it will + * use it as the directory tree. If a Sabre\DAV\INode is passed, it + * will create a Sabre\DAV\Tree and use the node as the root. + * + * If nothing is passed, a Sabre\DAV\SimpleCollection is created in + * a Sabre\DAV\Tree. + * + * If an array is passed, we automatically create a root node, and use + * the nodes in the array as top-level children. + * + * @param Tree|INode|array|null $treeOrNode The tree object + * + * @throws Exception + */ + public function __construct($treeOrNode = null, ?HTTP\Sapi $sapi = null) + { + if ($treeOrNode instanceof Tree) { + $this->tree = $treeOrNode; + } elseif ($treeOrNode instanceof INode) { + $this->tree = new Tree($treeOrNode); + } elseif (is_array($treeOrNode)) { + $root = new SimpleCollection('root', $treeOrNode); + $this->tree = new Tree($root); + } elseif (is_null($treeOrNode)) { + $root = new SimpleCollection('root'); + $this->tree = new Tree($root); + } else { + throw new Exception('Invalid argument passed to constructor. Argument must either be an instance of Sabre\\DAV\\Tree, Sabre\\DAV\\INode, an array or null'); + } + + $this->xml = new Xml\Service(); + $this->sapi = $sapi ?? new HTTP\Sapi(); + $this->httpResponse = new HTTP\Response(); + $this->httpRequest = $this->sapi->getRequest(); + $this->addPlugin(new CorePlugin()); + } + + /** + * Starts the DAV Server. + */ + public function start() + { + try { + // If nginx (pre-1.2) is used as a proxy server, and SabreDAV as an + // origin, we must make sure we send back HTTP/1.0 if this was + // requested. + // This is mainly because nginx doesn't support Chunked Transfer + // Encoding, and this forces the webserver SabreDAV is running on, + // to buffer entire responses to calculate Content-Length. + $this->httpResponse->setHTTPVersion($this->httpRequest->getHTTPVersion()); + + // Setting the base url + $this->httpRequest->setBaseUrl($this->getBaseUri()); + $this->invokeMethod($this->httpRequest, $this->httpResponse); + } catch (\Throwable $e) { + try { + $this->emit('exception', [$e]); + } catch (\Exception $ignore) { + } + $DOM = new \DOMDocument('1.0', 'utf-8'); + $DOM->formatOutput = true; + + $error = $DOM->createElementNS('DAV:', 'd:error'); + $error->setAttribute('xmlns:s', self::NS_SABREDAV); + $DOM->appendChild($error); + + $h = function ($v) { + return htmlspecialchars((string) $v, ENT_NOQUOTES, 'UTF-8'); + }; + + if (self::$exposeVersion) { + $error->appendChild($DOM->createElement('s:sabredav-version', $h(Version::VERSION))); + } + + $error->appendChild($DOM->createElement('s:exception', $h(get_class($e)))); + $error->appendChild($DOM->createElement('s:message', $h($e->getMessage()))); + if ($this->debugExceptions) { + $error->appendChild($DOM->createElement('s:file', $h($e->getFile()))); + $error->appendChild($DOM->createElement('s:line', $h($e->getLine()))); + $error->appendChild($DOM->createElement('s:code', $h($e->getCode()))); + $error->appendChild($DOM->createElement('s:stacktrace', $h($e->getTraceAsString()))); + } + + if ($this->debugExceptions) { + $previous = $e; + while ($previous = $previous->getPrevious()) { + $xPrevious = $DOM->createElement('s:previous-exception'); + $xPrevious->appendChild($DOM->createElement('s:exception', $h(get_class($previous)))); + $xPrevious->appendChild($DOM->createElement('s:message', $h($previous->getMessage()))); + $xPrevious->appendChild($DOM->createElement('s:file', $h($previous->getFile()))); + $xPrevious->appendChild($DOM->createElement('s:line', $h($previous->getLine()))); + $xPrevious->appendChild($DOM->createElement('s:code', $h($previous->getCode()))); + $xPrevious->appendChild($DOM->createElement('s:stacktrace', $h($previous->getTraceAsString()))); + $error->appendChild($xPrevious); + } + } + + if ($e instanceof Exception) { + $httpCode = $e->getHTTPCode(); + $e->serialize($this, $error); + $headers = $e->getHTTPHeaders($this); + } else { + $httpCode = 500; + $headers = []; + } + $headers['Content-Type'] = 'application/xml; charset=utf-8'; + + $this->httpResponse->setStatus($httpCode); + $this->httpResponse->setHeaders($headers); + $this->httpResponse->setBody($DOM->saveXML()); + $this->sapi->sendResponse($this->httpResponse); + } + } + + /** + * Alias of start(). + * + * @deprecated + */ + public function exec() + { + $this->start(); + } + + /** + * Sets the base server uri. + * + * @param string $uri + */ + public function setBaseUri($uri) + { + // If the baseUri does not end with a slash, we must add it + if ('/' !== $uri[strlen($uri) - 1]) { + $uri .= '/'; + } + + $this->baseUri = $uri; + } + + /** + * Returns the base responding uri. + * + * @return string + */ + public function getBaseUri() + { + if (is_null($this->baseUri)) { + $this->baseUri = $this->guessBaseUri(); + } + + return $this->baseUri; + } + + /** + * This method attempts to detect the base uri. + * Only the PATH_INFO variable is considered. + * + * If this variable is not set, the root (/) is assumed. + * + * @return string + */ + public function guessBaseUri() + { + $pathInfo = $this->httpRequest->getRawServerValue('PATH_INFO'); + $uri = $this->httpRequest->getRawServerValue('REQUEST_URI'); + + // If PATH_INFO is found, we can assume it's accurate. + if (!empty($pathInfo)) { + // We need to make sure we ignore the QUERY_STRING part + if ($pos = strpos($uri, '?')) { + $uri = substr($uri, 0, $pos); + } + + // PATH_INFO is only set for urls, such as: /example.php/path + // in that case PATH_INFO contains '/path'. + // Note that REQUEST_URI is percent encoded, while PATH_INFO is + // not, Therefore they are only comparable if we first decode + // REQUEST_INFO as well. + $decodedUri = HTTP\decodePath($uri); + + // A simple sanity check: + if (substr($decodedUri, strlen($decodedUri) - strlen($pathInfo)) === $pathInfo) { + $baseUri = substr($decodedUri, 0, strlen($decodedUri) - strlen($pathInfo)); + + return rtrim($baseUri, '/').'/'; + } + + throw new Exception('The REQUEST_URI ('.$uri.') did not end with the contents of PATH_INFO ('.$pathInfo.'). This server might be misconfigured.'); + } + + // The last fallback is that we're just going to assume the server root. + return '/'; + } + + /** + * Adds a plugin to the server. + * + * For more information, console the documentation of Sabre\DAV\ServerPlugin + */ + public function addPlugin(ServerPlugin $plugin) + { + $this->plugins[$plugin->getPluginName()] = $plugin; + $plugin->initialize($this); + } + + /** + * Returns an initialized plugin by it's name. + * + * This function returns null if the plugin was not found. + * + * @param string $name + * + * @return ServerPlugin + */ + public function getPlugin($name) + { + if (isset($this->plugins[$name])) { + return $this->plugins[$name]; + } + + return null; + } + + /** + * Returns all plugins. + * + * @return array + */ + public function getPlugins() + { + return $this->plugins; + } + + /** + * Returns the PSR-3 logger object. + * + * @return LoggerInterface + */ + public function getLogger() + { + if (!$this->logger) { + $this->logger = new NullLogger(); + } + + return $this->logger; + } + + /** + * Handles a http request, and execute a method based on its name. + * + * @param bool $sendResponse whether to send the HTTP response to the DAV client + */ + public function invokeMethod(RequestInterface $request, ResponseInterface $response, $sendResponse = true) + { + $method = $request->getMethod(); + + if (!$this->emit('beforeMethod:'.$method, [$request, $response])) { + return; + } + + if (self::$exposeVersion) { + $response->setHeader('X-Sabre-Version', Version::VERSION); + } + + $this->transactionType = strtolower($method); + + if (!$this->checkPreconditions($request, $response)) { + $this->sapi->sendResponse($response); + + return; + } + + if ($this->emit('method:'.$method, [$request, $response])) { + $exMessage = 'There was no plugin in the system that was willing to handle this '.$method.' method.'; + if ('GET' === $method) { + $exMessage .= ' Enable the Browser plugin to get a better result here.'; + } + + // Unsupported method + throw new Exception\NotImplemented($exMessage); + } + + if (!$this->emit('afterMethod:'.$method, [$request, $response])) { + return; + } + + if (null === $response->getStatus()) { + throw new Exception('No subsystem set a valid HTTP status code. Something must have interrupted the request without providing further detail.'); + } + if ($sendResponse) { + $this->sapi->sendResponse($response); + $this->emit('afterResponse', [$request, $response]); + } + } + + // {{{ HTTP/WebDAV protocol helpers + + /** + * Returns an array with all the supported HTTP methods for a specific uri. + * + * @param string $path + * + * @return array + */ + public function getAllowedMethods($path) + { + $methods = [ + 'OPTIONS', + 'GET', + 'HEAD', + 'DELETE', + 'PROPFIND', + 'PUT', + 'PROPPATCH', + 'COPY', + 'MOVE', + 'REPORT', + ]; + + // The MKCOL is only allowed on an unmapped uri + try { + $this->tree->getNodeForPath($path); + } catch (Exception\NotFound $e) { + $methods[] = 'MKCOL'; + } + + // We're also checking if any of the plugins register any new methods + foreach ($this->plugins as $plugin) { + $methods = array_merge($methods, $plugin->getHTTPMethods($path)); + } + array_unique($methods); + + return $methods; + } + + /** + * Gets the uri for the request, keeping the base uri into consideration. + * + * @return string + */ + public function getRequestUri() + { + return $this->calculateUri($this->httpRequest->getUrl()); + } + + /** + * Turns a URI such as the REQUEST_URI into a local path. + * + * This method: + * * strips off the base path + * * normalizes the path + * * uri-decodes the path + * + * @param string $uri + * + * @throws Exception\Forbidden A permission denied exception is thrown whenever there was an attempt to supply a uri outside of the base uri + * + * @return string + */ + public function calculateUri($uri) + { + if ('' != $uri && '/' != $uri[0] && strpos($uri, '://')) { + $uri = parse_url($uri, PHP_URL_PATH); + } + + $uri = Uri\normalize(preg_replace('|/+|', '/', $uri)); + $baseUri = Uri\normalize($this->getBaseUri()); + + if (0 === strpos($uri, $baseUri)) { + return trim(HTTP\decodePath(substr($uri, strlen($baseUri))), '/'); + + // A special case, if the baseUri was accessed without a trailing + // slash, we'll accept it as well. + } elseif ($uri.'/' === $baseUri) { + return ''; + } else { + throw new Exception\Forbidden('Requested uri ('.$uri.') is out of base uri ('.$this->getBaseUri().')'); + } + } + + /** + * Returns the HTTP depth header. + * + * This method returns the contents of the HTTP depth request header. If the depth header was 'infinity' it will return the Sabre\DAV\Server::DEPTH_INFINITY object + * It is possible to supply a default depth value, which is used when the depth header has invalid content, or is completely non-existent + * + * @param mixed $default + * + * @return int + */ + public function getHTTPDepth($default = self::DEPTH_INFINITY) + { + // If its not set, we'll grab the default + $depth = $this->httpRequest->getHeader('Depth'); + + if (is_null($depth)) { + return $default; + } + + if ('infinity' == $depth) { + return self::DEPTH_INFINITY; + } + + // If its an unknown value. we'll grab the default + if (!ctype_digit($depth)) { + return $default; + } + + return (int) $depth; + } + + /** + * Returns the HTTP range header. + * + * This method returns null if there is no well-formed HTTP range request + * header or array($start, $end). + * + * The first number is the offset of the first byte in the range. + * The second number is the offset of the last byte in the range. + * + * If the second offset is null, it should be treated as the offset of the last byte of the entity + * If the first offset is null, the second offset should be used to retrieve the last x bytes of the entity + * + * @return int[]|null + */ + public function getHTTPRange() + { + $range = $this->httpRequest->getHeader('range'); + if (is_null($range)) { + return null; + } + + // Matching "Range: bytes=1234-5678: both numbers are optional + + if (!preg_match('/^bytes=([0-9]*)-([0-9]*)$/i', $range, $matches)) { + return null; + } + + if ('' === $matches[1] && '' === $matches[2]) { + return null; + } + + return [ + '' !== $matches[1] ? (int) $matches[1] : null, + '' !== $matches[2] ? (int) $matches[2] : null, + ]; + } + + /** + * Returns the HTTP Prefer header information. + * + * The prefer header is defined in: + * http://tools.ietf.org/html/draft-snell-http-prefer-14 + * + * This method will return an array with options. + * + * Currently, the following options may be returned: + * [ + * 'return-asynch' => true, + * 'return-minimal' => true, + * 'return-representation' => true, + * 'wait' => 30, + * 'strict' => true, + * 'lenient' => true, + * ] + * + * This method also supports the Brief header, and will also return + * 'return-minimal' if the brief header was set to 't'. + * + * For the boolean options, false will be returned if the headers are not + * specified. For the integer options it will be 'null'. + * + * @return array + */ + public function getHTTPPrefer() + { + $result = [ + // can be true or false + 'respond-async' => false, + // Could be set to 'representation' or 'minimal'. + 'return' => null, + // Used as a timeout, is usually a number. + 'wait' => null, + // can be 'strict' or 'lenient'. + 'handling' => false, + ]; + + if ($prefer = $this->httpRequest->getHeader('Prefer')) { + $result = array_merge( + $result, + HTTP\parsePrefer($prefer) + ); + } elseif ('t' == $this->httpRequest->getHeader('Brief')) { + $result['return'] = 'minimal'; + } + + return $result; + } + + /** + * Returns information about Copy and Move requests. + * + * This function is created to help getting information about the source and the destination for the + * WebDAV MOVE and COPY HTTP request. It also validates a lot of information and throws proper exceptions + * + * The returned value is an array with the following keys: + * * destination - Destination path + * * destinationExists - Whether or not the destination is an existing url (and should therefore be overwritten) + * + * @throws Exception\BadRequest upon missing or broken request headers + * @throws Exception\UnsupportedMediaType when trying to copy into a + * non-collection + * @throws Exception\PreconditionFailed if overwrite is set to false, but + * the destination exists + * @throws Exception\Forbidden when source and destination paths are + * identical + * @throws Exception\Conflict when trying to copy a node into its own + * subtree + * + * @return array + */ + public function getCopyAndMoveInfo(RequestInterface $request) + { + // Collecting the relevant HTTP headers + if (!$request->getHeader('Destination')) { + throw new Exception\BadRequest('The destination header was not supplied'); + } + $destination = $this->calculateUri($request->getHeader('Destination')); + $overwrite = $request->getHeader('Overwrite'); + if (!$overwrite) { + $overwrite = 'T'; + } + if ('T' == strtoupper($overwrite)) { + $overwrite = true; + } elseif ('F' == strtoupper($overwrite)) { + $overwrite = false; + } + // We need to throw a bad request exception, if the header was invalid + else { + throw new Exception\BadRequest('The HTTP Overwrite header should be either T or F'); + } + list($destinationDir) = Uri\split($destination); + + try { + $destinationParent = $this->tree->getNodeForPath($destinationDir); + if (!($destinationParent instanceof ICollection)) { + throw new Exception\UnsupportedMediaType('The destination node is not a collection'); + } + } catch (Exception\NotFound $e) { + // If the destination parent node is not found, we throw a 409 + throw new Exception\Conflict('The destination node is not found'); + } + + try { + $destinationNode = $this->tree->getNodeForPath($destination); + + // If this succeeded, it means the destination already exists + // we'll need to throw precondition failed in case overwrite is false + if (!$overwrite) { + throw new Exception\PreconditionFailed('The destination node already exists, and the overwrite header is set to false', 'Overwrite'); + } + } catch (Exception\NotFound $e) { + // Destination didn't exist, we're all good + $destinationNode = false; + } + + $requestPath = $request->getPath(); + if ($destination === $requestPath) { + throw new Exception\Forbidden('Source and destination uri are identical.'); + } + if (substr($destination, 0, strlen($requestPath) + 1) === $requestPath.'/') { + throw new Exception\Conflict('The destination may not be part of the same subtree as the source path.'); + } + + // These are the three relevant properties we need to return + return [ + 'destination' => $destination, + 'destinationExists' => (bool) $destinationNode, + 'destinationNode' => $destinationNode, + ]; + } + + /** + * Returns a list of properties for a path. + * + * This is a simplified version getPropertiesForPath. If you aren't + * interested in status codes, but you just want to have a flat list of + * properties, use this method. + * + * Please note though that any problems related to retrieving properties, + * such as permission issues will just result in an empty array being + * returned. + * + * @param string $path + * @param array $propertyNames + * + * @return array + */ + public function getProperties($path, $propertyNames) + { + $result = $this->getPropertiesForPath($path, $propertyNames, 0); + if (isset($result[0][200])) { + return $result[0][200]; + } else { + return []; + } + } + + /** + * A kid-friendly way to fetch properties for a node's children. + * + * The returned array will be indexed by the path of the of child node. + * Only properties that are actually found will be returned. + * + * The parent node will not be returned. + * + * @param string $path + * @param array $propertyNames + * + * @return array + */ + public function getPropertiesForChildren($path, $propertyNames) + { + $result = []; + foreach ($this->getPropertiesForPath($path, $propertyNames, 1) as $k => $row) { + // Skipping the parent path + if (0 === $k) { + continue; + } + + $result[$row['href']] = $row[200]; + } + + return $result; + } + + /** + * Returns a list of HTTP headers for a particular resource. + * + * The generated http headers are based on properties provided by the + * resource. The method basically provides a simple mapping between + * DAV property and HTTP header. + * + * The headers are intended to be used for HEAD and GET requests. + * + * @param string $path + * + * @return array + */ + public function getHTTPHeaders($path) + { + $propertyMap = [ + '{DAV:}getcontenttype' => 'Content-Type', + '{DAV:}getcontentlength' => 'Content-Length', + '{DAV:}getlastmodified' => 'Last-Modified', + '{DAV:}getetag' => 'ETag', + ]; + + $properties = $this->getProperties($path, array_keys($propertyMap)); + + $headers = []; + foreach ($propertyMap as $property => $header) { + if (!isset($properties[$property])) { + continue; + } + + if (is_scalar($properties[$property])) { + $headers[$header] = $properties[$property]; + + // GetLastModified gets special cased + } elseif ($properties[$property] instanceof Xml\Property\GetLastModified) { + $headers[$header] = HTTP\toDate($properties[$property]->getTime()); + } + } + + return $headers; + } + + /** + * Small helper to support PROPFIND with DEPTH_INFINITY. + * + * @param array $yieldFirst + * + * @return \Traversable + */ + private function generatePathNodes(PropFind $propFind, ?array $yieldFirst = null) + { + if (null !== $yieldFirst) { + yield $yieldFirst; + } + $newDepth = $propFind->getDepth(); + $path = $propFind->getPath(); + + if (self::DEPTH_INFINITY !== $newDepth) { + --$newDepth; + } + + $propertyNames = $propFind->getRequestedProperties(); + $propFindType = !$propFind->isAllProps() ? PropFind::NORMAL : PropFind::ALLPROPS; + + foreach ($this->tree->getChildren($path) as $childNode) { + if ('' !== $path) { + $subPath = $path.'/'.$childNode->getName(); + } else { + $subPath = $childNode->getName(); + } + $subPropFind = new PropFind($subPath, $propertyNames, $newDepth, $propFindType); + + yield [ + $subPropFind, + $childNode, + ]; + + if ((self::DEPTH_INFINITY === $newDepth || $newDepth >= 1) && $childNode instanceof ICollection) { + foreach ($this->generatePathNodes($subPropFind) as $subItem) { + yield $subItem; + } + } + } + } + + /** + * Returns a list of properties for a given path. + * + * The path that should be supplied should have the baseUrl stripped out + * The list of properties should be supplied in Clark notation. If the list is empty + * 'allprops' is assumed. + * + * If a depth of 1 is requested child elements will also be returned. + * + * @param string $path + * @param array $propertyNames + * @param int $depth + * + * @return array + * + * @deprecated Use getPropertiesIteratorForPath() instead (as it's more memory efficient) + * @see getPropertiesIteratorForPath() + */ + public function getPropertiesForPath($path, $propertyNames = [], $depth = 0) + { + return iterator_to_array($this->getPropertiesIteratorForPath($path, $propertyNames, $depth)); + } + + /** + * Returns a list of properties for a given path. + * + * The path that should be supplied should have the baseUrl stripped out + * The list of properties should be supplied in Clark notation. If the list is empty + * 'allprops' is assumed. + * + * If a depth of 1 is requested child elements will also be returned. + * + * @param string $path + * @param array $propertyNames + * @param int $depth + * + * @return \Iterator + */ + public function getPropertiesIteratorForPath($path, $propertyNames = [], $depth = 0) + { + // The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled + if (!$this->enablePropfindDepthInfinity && 0 != $depth) { + $depth = 1; + } + + $path = trim($path, '/'); + + $propFindType = $propertyNames ? PropFind::NORMAL : PropFind::ALLPROPS; + $propFind = new PropFind($path, (array) $propertyNames, $depth, $propFindType); + + $parentNode = $this->tree->getNodeForPath($path); + + $propFindRequests = [[ + $propFind, + $parentNode, + ]]; + + if (($depth > 0 || self::DEPTH_INFINITY === $depth) && $parentNode instanceof ICollection) { + $propFindRequests = $this->generatePathNodes(clone $propFind, current($propFindRequests)); + } + + foreach ($propFindRequests as $propFindRequest) { + list($propFind, $node) = $propFindRequest; + $r = $this->getPropertiesByNode($propFind, $node); + if ($r) { + $result = $propFind->getResultForMultiStatus(); + $result['href'] = $propFind->getPath(); + + // WebDAV recommends adding a slash to the path, if the path is + // a collection. + // Furthermore, iCal also demands this to be the case for + // principals. This is non-standard, but we support it. + $resourceType = $this->getResourceTypeForNode($node); + if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) { + $result['href'] .= '/'; + } + yield $result; + } + } + } + + /** + * Returns a list of properties for a list of paths. + * + * The path that should be supplied should have the baseUrl stripped out + * The list of properties should be supplied in Clark notation. If the list is empty + * 'allprops' is assumed. + * + * The result is returned as an array, with paths for it's keys. + * The result may be returned out of order. + * + * @return array + */ + public function getPropertiesForMultiplePaths(array $paths, array $propertyNames = []) + { + $result = [ + ]; + + $nodes = $this->tree->getMultipleNodes($paths); + + foreach ($nodes as $path => $node) { + $propFind = new PropFind($path, $propertyNames); + $r = $this->getPropertiesByNode($propFind, $node); + if ($r) { + $result[$path] = $propFind->getResultForMultiStatus(); + $result[$path]['href'] = $path; + + $resourceType = $this->getResourceTypeForNode($node); + if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) { + $result[$path]['href'] .= '/'; + } + } + } + + return $result; + } + + /** + * Determines all properties for a node. + * + * This method tries to grab all properties for a node. This method is used + * internally getPropertiesForPath and a few others. + * + * It could be useful to call this, if you already have an instance of your + * target node and simply want to run through the system to get a correct + * list of properties. + * + * @return bool + */ + public function getPropertiesByNode(PropFind $propFind, INode $node) + { + return $this->emit('propFind', [$propFind, $node]); + } + + /** + * This method is invoked by sub-systems creating a new file. + * + * Currently this is done by HTTP PUT and HTTP LOCK (in the Locks_Plugin). + * It was important to get this done through a centralized function, + * allowing plugins to intercept this using the beforeCreateFile event. + * + * This method will return true if the file was actually created + * + * @param string $uri + * @param resource $data + * @param string $etag + * + * @return bool + */ + public function createFile($uri, $data, &$etag = null) + { + list($dir, $name) = Uri\split($uri); + + if (!$this->emit('beforeBind', [$uri])) { + return false; + } + + try { + $parent = $this->tree->getNodeForPath($dir); + } catch (Exception\NotFound $e) { + throw new Exception\Conflict('Files cannot be created in non-existent collections'); + } + + if (!$parent instanceof ICollection) { + throw new Exception\Conflict('Files can only be created as children of collections'); + } + + // It is possible for an event handler to modify the content of the + // body, before it gets written. If this is the case, $modified + // should be set to true. + // + // If $modified is true, we must not send back an ETag. + $modified = false; + if (!$this->emit('beforeCreateFile', [$uri, &$data, $parent, &$modified])) { + return false; + } + + $etag = $parent->createFile($name, $data); + + if ($modified) { + $etag = null; + } + + $this->tree->markDirty($dir.'/'.$name); + + $this->emit('afterBind', [$uri]); + $this->emit('afterCreateFile', [$uri, $parent]); + + return true; + } + + /** + * This method is invoked by sub-systems updating a file. + * + * This method will return true if the file was actually updated + * + * @param string $uri + * @param resource $data + * @param string $etag + * + * @return bool + */ + public function updateFile($uri, $data, &$etag = null) + { + $node = $this->tree->getNodeForPath($uri); + + // It is possible for an event handler to modify the content of the + // body, before it gets written. If this is the case, $modified + // should be set to true. + // + // If $modified is true, we must not send back an ETag. + $modified = false; + if (!$this->emit('beforeWriteContent', [$uri, $node, &$data, &$modified])) { + return false; + } + + $etag = $node->put($data); + if ($modified) { + $etag = null; + } + $this->emit('afterWriteContent', [$uri, $node]); + + return true; + } + + /** + * This method is invoked by sub-systems creating a new directory. + * + * @param string $uri + */ + public function createDirectory($uri) + { + $this->createCollection($uri, new MkCol(['{DAV:}collection'], [])); + } + + /** + * Use this method to create a new collection. + * + * @param string $uri The new uri + * + * @return array|null + */ + public function createCollection($uri, MkCol $mkCol) + { + list($parentUri, $newName) = Uri\split($uri); + + // Making sure the parent exists + try { + $parent = $this->tree->getNodeForPath($parentUri); + } catch (Exception\NotFound $e) { + throw new Exception\Conflict('Parent node does not exist'); + } + + // Making sure the parent is a collection + if (!$parent instanceof ICollection) { + throw new Exception\Conflict('Parent node is not a collection'); + } + + // Making sure the child does not already exist + try { + $parent->getChild($newName); + + // If we got here.. it means there's already a node on that url, and we need to throw a 405 + throw new Exception\MethodNotAllowed('The resource you tried to create already exists'); + } catch (Exception\NotFound $e) { + // NotFound is the expected behavior. + } + + if (!$this->emit('beforeBind', [$uri])) { + return; + } + + if ($parent instanceof IExtendedCollection) { + /* + * If the parent is an instance of IExtendedCollection, it means that + * we can pass the MkCol object directly as it may be able to store + * properties immediately. + */ + $parent->createExtendedCollection($newName, $mkCol); + } else { + /* + * If the parent is a standard ICollection, it means only + * 'standard' collections can be created, so we should fail any + * MKCOL operation that carries extra resourcetypes. + */ + if (count($mkCol->getResourceType()) > 1) { + throw new Exception\InvalidResourceType('The {DAV:}resourcetype you specified is not supported here.'); + } + + $parent->createDirectory($newName); + } + + // If there are any properties that have not been handled/stored, + // we ask the 'propPatch' event to handle them. This will allow for + // example the propertyStorage system to store properties upon MKCOL. + if ($mkCol->getRemainingMutations()) { + $this->emit('propPatch', [$uri, $mkCol]); + } + $success = $mkCol->commit(); + + if (!$success) { + $result = $mkCol->getResult(); + + $formattedResult = [ + 'href' => $uri, + ]; + + foreach ($result as $propertyName => $status) { + if (!isset($formattedResult[$status])) { + $formattedResult[$status] = []; + } + $formattedResult[$status][$propertyName] = null; + } + + return $formattedResult; + } + + $this->tree->markDirty($parentUri); + $this->emit('afterBind', [$uri]); + $this->emit('afterCreateCollection', [$uri]); + } + + /** + * This method updates a resource's properties. + * + * The properties array must be a list of properties. Array-keys are + * property names in clarknotation, array-values are it's values. + * If a property must be deleted, the value should be null. + * + * Note that this request should either completely succeed, or + * completely fail. + * + * The response is an array with properties for keys, and http status codes + * as their values. + * + * @param string $path + * + * @return array + */ + public function updateProperties($path, array $properties) + { + $propPatch = new PropPatch($properties); + $this->emit('propPatch', [$path, $propPatch]); + $propPatch->commit(); + + return $propPatch->getResult(); + } + + /** + * This method checks the main HTTP preconditions. + * + * Currently these are: + * * If-Match + * * If-None-Match + * * If-Modified-Since + * * If-Unmodified-Since + * + * The method will return true if all preconditions are met + * The method will return false, or throw an exception if preconditions + * failed. If false is returned the operation should be aborted, and + * the appropriate HTTP response headers are already set. + * + * Normally this method will throw 412 Precondition Failed for failures + * related to If-None-Match, If-Match and If-Unmodified Since. It will + * set the status to 304 Not Modified for If-Modified_since. + * + * @return bool + */ + public function checkPreconditions(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + $node = null; + $lastMod = null; + $etag = null; + + if ($ifMatch = $request->getHeader('If-Match')) { + // If-Match contains an entity tag. Only if the entity-tag + // matches we are allowed to make the request succeed. + // If the entity-tag is '*' we are only allowed to make the + // request succeed if a resource exists at that url. + try { + $node = $this->tree->getNodeForPath($path); + } catch (Exception\NotFound $e) { + throw new Exception\PreconditionFailed('An If-Match header was specified and the resource did not exist', 'If-Match'); + } + + // Only need to check entity tags if they are not * + if ('*' !== $ifMatch) { + // There can be multiple ETags + $ifMatch = explode(',', $ifMatch); + $haveMatch = false; + foreach ($ifMatch as $ifMatchItem) { + // Stripping any extra spaces + $ifMatchItem = trim($ifMatchItem, ' '); + + $etag = $node instanceof IFile ? $node->getETag() : null; + if ($etag === $ifMatchItem) { + $haveMatch = true; + } else { + // Evolution has a bug where it sometimes prepends the " + // with a \. This is our workaround. + if (str_replace('\\"', '"', $ifMatchItem) === $etag) { + $haveMatch = true; + } + } + } + if (!$haveMatch) { + if ($etag) { + $response->setHeader('ETag', $etag); + } + throw new Exception\PreconditionFailed('An If-Match header was specified, but none of the specified ETags matched.', 'If-Match'); + } + } + } + + if ($ifNoneMatch = $request->getHeader('If-None-Match')) { + // The If-None-Match header contains an ETag. + // Only if the ETag does not match the current ETag, the request will succeed + // The header can also contain *, in which case the request + // will only succeed if the entity does not exist at all. + $nodeExists = true; + if (!$node) { + try { + $node = $this->tree->getNodeForPath($path); + } catch (Exception\NotFound $e) { + $nodeExists = false; + } + } + if ($nodeExists) { + $haveMatch = false; + if ('*' === $ifNoneMatch) { + $haveMatch = true; + } else { + // There might be multiple ETags + $ifNoneMatch = explode(',', $ifNoneMatch); + $etag = $node instanceof IFile ? $node->getETag() : null; + + foreach ($ifNoneMatch as $ifNoneMatchItem) { + // Stripping any extra spaces + $ifNoneMatchItem = trim($ifNoneMatchItem, ' '); + + if ($etag === $ifNoneMatchItem) { + $haveMatch = true; + } + } + } + + if ($haveMatch) { + if ($etag) { + $response->setHeader('ETag', $etag); + } + if ('GET' === $request->getMethod()) { + $response->setStatus(304); + + return false; + } else { + throw new Exception\PreconditionFailed('An If-None-Match header was specified, but the ETag matched (or * was specified).', 'If-None-Match'); + } + } + } + } + + if (!$ifNoneMatch && ($ifModifiedSince = $request->getHeader('If-Modified-Since'))) { + // The If-Modified-Since header contains a date. We + // will only return the entity if it has been changed since + // that date. If it hasn't been changed, we return a 304 + // header + // Note that this header only has to be checked if there was no If-None-Match header + // as per the HTTP spec. + $date = HTTP\parseDate($ifModifiedSince); + + if ($date) { + if (is_null($node)) { + $node = $this->tree->getNodeForPath($path); + } + $lastMod = $node->getLastModified(); + if ($lastMod) { + $lastMod = new \DateTime('@'.$lastMod); + if ($lastMod <= $date) { + $response->setStatus(304); + $response->setHeader('Last-Modified', HTTP\toDate($lastMod)); + + return false; + } + } + } + } + + if ($ifUnmodifiedSince = $request->getHeader('If-Unmodified-Since')) { + // The If-Unmodified-Since will allow allow the request if the + // entity has not changed since the specified date. + $date = HTTP\parseDate($ifUnmodifiedSince); + + // We must only check the date if it's valid + if ($date) { + if (is_null($node)) { + $node = $this->tree->getNodeForPath($path); + } + $lastMod = $node->getLastModified(); + if ($lastMod) { + $lastMod = new \DateTime('@'.$lastMod); + if ($lastMod > $date) { + throw new Exception\PreconditionFailed('An If-Unmodified-Since header was specified, but the entity has been changed since the specified date.', 'If-Unmodified-Since'); + } + } + } + } + + // Now the hardest, the If: header. The If: header can contain multiple + // urls, ETags and so-called 'state tokens'. + // + // Examples of state tokens include lock-tokens (as defined in rfc4918) + // and sync-tokens (as defined in rfc6578). + // + // The only proper way to deal with these, is to emit events, that a + // Sync and Lock plugin can pick up. + $ifConditions = $this->getIfConditions($request); + + foreach ($ifConditions as $kk => $ifCondition) { + foreach ($ifCondition['tokens'] as $ii => $token) { + $ifConditions[$kk]['tokens'][$ii]['validToken'] = false; + } + } + + // Plugins are responsible for validating all the tokens. + // If a plugin deemed a token 'valid', it will set 'validToken' to + // true. + $this->emit('validateTokens', [$request, &$ifConditions]); + + // Now we're going to analyze the result. + + // Every ifCondition needs to validate to true, so we exit as soon as + // we have an invalid condition. + foreach ($ifConditions as $ifCondition) { + $uri = $ifCondition['uri']; + $tokens = $ifCondition['tokens']; + + // We only need 1 valid token for the condition to succeed. + foreach ($tokens as $token) { + $tokenValid = $token['validToken'] || !$token['token']; + + $etagValid = false; + if (!$token['etag']) { + $etagValid = true; + } + // Checking the ETag, only if the token was already deemed + // valid and there is one. + if ($token['etag'] && $tokenValid) { + // The token was valid, and there was an ETag. We must + // grab the current ETag and check it. + $node = $this->tree->getNodeForPath($uri); + $etagValid = $node instanceof IFile && $node->getETag() == $token['etag']; + } + + if (($tokenValid && $etagValid) ^ $token['negate']) { + // Both were valid, so we can go to the next condition. + continue 2; + } + } + + // If we ended here, it means there was no valid ETag + token + // combination found for the current condition. This means we fail! + throw new Exception\PreconditionFailed('Failed to find a valid token/etag combination for '.$uri, 'If'); + } + + return true; + } + + /** + * This method is created to extract information from the WebDAV HTTP 'If:' header. + * + * The If header can be quite complex, and has a bunch of features. We're using a regex to extract all relevant information + * The function will return an array, containing structs with the following keys + * + * * uri - the uri the condition applies to. + * * tokens - The lock token. another 2 dimensional array containing 3 elements + * + * Example 1: + * + * If: () + * + * Would result in: + * + * [ + * [ + * 'uri' => '/request/uri', + * 'tokens' => [ + * [ + * [ + * 'negate' => false, + * 'token' => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2', + * 'etag' => "" + * ] + * ] + * ], + * ] + * ] + * + * Example 2: + * + * If: (Not ["Im An ETag"]) (["Another ETag"]) (Not ["Path2 ETag"]) + * + * Would result in: + * + * [ + * [ + * 'uri' => 'path', + * 'tokens' => [ + * [ + * [ + * 'negate' => true, + * 'token' => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2', + * 'etag' => '"Im An ETag"' + * ], + * [ + * 'negate' => false, + * 'token' => '', + * 'etag' => '"Another ETag"' + * ] + * ] + * ], + * ], + * [ + * 'uri' => 'path2', + * 'tokens' => [ + * [ + * [ + * 'negate' => true, + * 'token' => '', + * 'etag' => '"Path2 ETag"' + * ] + * ] + * ], + * ], + * ] + * + * @return array + */ + public function getIfConditions(RequestInterface $request) + { + $header = $request->getHeader('If'); + if (!$header) { + return []; + } + + $matches = []; + + $regex = '/(?:\<(?P.*?)\>\s)?\((?PNot\s)?(?:\<(?P[^\>]*)\>)?(?:\s?)(?:\[(?P[^\]]*)\])?\)/im'; + preg_match_all($regex, $header, $matches, PREG_SET_ORDER); + + $conditions = []; + + foreach ($matches as $match) { + // If there was no uri specified in this match, and there were + // already conditions parsed, we add the condition to the list of + // conditions for the previous uri. + if (!$match['uri'] && count($conditions)) { + $conditions[count($conditions) - 1]['tokens'][] = [ + 'negate' => $match['not'] ? true : false, + 'token' => $match['token'], + 'etag' => isset($match['etag']) ? $match['etag'] : '', + ]; + } else { + if (!$match['uri']) { + $realUri = $request->getPath(); + } else { + $realUri = $this->calculateUri($match['uri']); + } + + $conditions[] = [ + 'uri' => $realUri, + 'tokens' => [ + [ + 'negate' => $match['not'] ? true : false, + 'token' => $match['token'], + 'etag' => isset($match['etag']) ? $match['etag'] : '', + ], + ], + ]; + } + } + + return $conditions; + } + + /** + * Returns an array with resourcetypes for a node. + * + * @return array + */ + public function getResourceTypeForNode(INode $node) + { + $result = []; + foreach ($this->resourceTypeMapping as $className => $resourceType) { + if ($node instanceof $className) { + $result[] = $resourceType; + } + } + + return $result; + } + + // }}} + // {{{ XML Readers & Writers + + /** + * Returns a callback generating a WebDAV propfind response body based on a list of nodes. + * + * If 'strip404s' is set to true, all 404 responses will be removed. + * + * @param array|\Traversable $fileProperties The list with nodes + * @param bool $strip404s + * + * @return callable|string + */ + public function generateMultiStatus($fileProperties, $strip404s = false) + { + $this->emit('beforeMultiStatus', [&$fileProperties]); + + $w = $this->xml->getWriter(); + if (self::$streamMultiStatus) { + return function () use ($fileProperties, $strip404s, $w) { + $w->openUri('php://output'); + $this->writeMultiStatus($w, $fileProperties, $strip404s); + $w->flush(); + }; + } + $w->openMemory(); + $this->writeMultiStatus($w, $fileProperties, $strip404s); + + return $w->outputMemory(); + } + + /** + * @param $fileProperties + */ + private function writeMultiStatus(Writer $w, $fileProperties, bool $strip404s) + { + $w->contextUri = $this->baseUri; + $w->startDocument(); + + $w->startElement('{DAV:}multistatus'); + + foreach ($fileProperties as $entry) { + $href = $entry['href']; + unset($entry['href']); + if ($strip404s) { + unset($entry[404]); + } + $response = new Xml\Element\Response( + ltrim($href, '/'), + $entry + ); + $w->write([ + 'name' => '{DAV:}response', + 'value' => $response, + ]); + } + $w->endElement(); + $w->endDocument(); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/ServerPlugin.php b/lib/composer/vendor/sabre/dav/lib/DAV/ServerPlugin.php new file mode 100644 index 0000000..70acb01 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/ServerPlugin.php @@ -0,0 +1,105 @@ + $this->getPluginName(), + 'description' => null, + 'link' => null, + ]; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Sharing/ISharedNode.php b/lib/composer/vendor/sabre/dav/lib/DAV/Sharing/ISharedNode.php new file mode 100644 index 0000000..a746ac7 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Sharing/ISharedNode.php @@ -0,0 +1,69 @@ +server = $server; + + $server->xml->elementMap['{DAV:}share-resource'] = 'Sabre\\DAV\\Xml\\Request\\ShareResource'; + + array_push( + $server->protectedProperties, + '{DAV:}share-mode' + ); + + $server->on('method:POST', [$this, 'httpPost']); + $server->on('propFind', [$this, 'propFind']); + $server->on('getSupportedPrivilegeSet', [$this, 'getSupportedPrivilegeSet']); + $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']); + $server->on('onBrowserPostAction', [$this, 'browserPostAction']); + } + + /** + * Updates the list of sharees on a shared resource. + * + * The sharees array is a list of people that are to be added modified + * or removed in the list of shares. + * + * @param string $path + * @param Sharee[] $sharees + */ + public function shareResource($path, array $sharees) + { + $node = $this->server->tree->getNodeForPath($path); + + if (!$node instanceof ISharedNode) { + throw new Forbidden('Sharing is not allowed on this node'); + } + + // Getting ACL info + $acl = $this->server->getPlugin('acl'); + + // If there's no ACL support, we allow everything + if ($acl) { + $acl->checkPrivileges($path, '{DAV:}share'); + } + + foreach ($sharees as $sharee) { + // We're going to attempt to get a local principal uri for a share + // href by emitting the getPrincipalByUri event. + $principal = null; + $this->server->emit('getPrincipalByUri', [$sharee->href, &$principal]); + $sharee->principal = $principal; + } + $node->updateInvites($sharees); + } + + /** + * This event is triggered when properties are requested for nodes. + * + * This allows us to inject any sharings-specific properties. + */ + public function propFind(PropFind $propFind, INode $node) + { + if ($node instanceof ISharedNode) { + $propFind->handle('{DAV:}share-access', function () use ($node) { + return new Property\ShareAccess($node->getShareAccess()); + }); + $propFind->handle('{DAV:}invite', function () use ($node) { + return new Property\Invite($node->getInvites()); + }); + $propFind->handle('{DAV:}share-resource-uri', function () use ($node) { + return new Property\Href($node->getShareResourceUri()); + }); + } + } + + /** + * We intercept this to handle POST requests on shared resources. + * + * @return bool|null + */ + public function httpPost(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + $contentType = $request->getHeader('Content-Type'); + if (null === $contentType) { + return; + } + + // We're only interested in the davsharing content type. + if (false === strpos($contentType, 'application/davsharing+xml')) { + return; + } + + $message = $this->server->xml->parse( + $request->getBody(), + $request->getUrl(), + $documentType + ); + + switch ($documentType) { + case '{DAV:}share-resource': + $this->shareResource($path, $message->sharees); + $response->setStatus(200); + // Adding this because sending a response body may cause issues, + // and I wanted some type of indicator the response was handled. + $response->setHeader('X-Sabre-Status', 'everything-went-well'); + + // Breaking the event chain + return false; + + default: + throw new BadRequest('Unexpected document type: '.$documentType.' for this Content-Type'); + } + } + + /** + * This method is triggered whenever a subsystem requests the privileges + * hat are supported on a particular node. + * + * We need to add a number of privileges for scheduling purposes. + */ + public function getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet) + { + if ($node instanceof ISharedNode) { + $supportedPrivilegeSet['{DAV:}share'] = [ + 'abstract' => false, + 'aggregates' => [], + ]; + } + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'This plugin implements WebDAV resource sharing', + 'link' => 'https://github.com/evert/webdav-sharing', + ]; + } + + /** + * This method is used to generate HTML output for the + * DAV\Browser\Plugin. + * + * @param string $output + * @param string $path + * + * @return bool|null + */ + public function htmlActionsPanel(INode $node, &$output, $path) + { + if (!$node instanceof ISharedNode) { + return; + } + + $aclPlugin = $this->server->getPlugin('acl'); + if ($aclPlugin) { + if (!$aclPlugin->checkPrivileges($path, '{DAV:}share', \Sabre\DAVACL\Plugin::R_PARENT, false)) { + // Sharing is not permitted, we will not draw this interface. + return; + } + } + + $output .= '

+

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/composer/vendor/sabre/dav/lib/DAV/SimpleCollection.php b/lib/composer/vendor/sabre/dav/lib/DAV/SimpleCollection.php new file mode 100644 index 0000000..3cd14d9 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/SimpleCollection.php @@ -0,0 +1,109 @@ +name = $name; + foreach ($children as $key => $child) { + if (is_string($child)) { + $child = new SimpleFile($key, $child); + } elseif (is_array($child)) { + $child = new self($key, $child); + } elseif (!$child instanceof INode) { + throw new InvalidArgumentException('Children must be specified as strings, arrays or instances of Sabre\DAV\INode'); + } + $this->addChild($child); + } + } + + /** + * Adds a new childnode to this collection. + */ + public function addChild(INode $child) + { + $this->children[$child->getName()] = $child; + } + + /** + * Returns the name of the collection. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Returns a child object, by its name. + * + * This method makes use of the getChildren method to grab all the child nodes, and compares the name. + * Generally its wise to override this, as this can usually be optimized + * + * This method must throw Sabre\DAV\Exception\NotFound if the node does not + * exist. + * + * @param string $name + * + * @throws Exception\NotFound + * + * @return INode + */ + public function getChild($name) + { + if (isset($this->children[$name])) { + return $this->children[$name]; + } + throw new Exception\NotFound('File not found: '.$name.' in \''.$this->getName().'\''); + } + + /** + * Returns a list of children for this collection. + * + * @return INode[] + */ + public function getChildren() + { + return array_values($this->children); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/SimpleFile.php b/lib/composer/vendor/sabre/dav/lib/DAV/SimpleFile.php new file mode 100644 index 0000000..ca808b6 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/SimpleFile.php @@ -0,0 +1,118 @@ +name = $name; + $this->contents = $contents; + $this->mimeType = $mimeType; + } + + /** + * Returns the node name for this file. + * + * This name is used to construct the url. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Returns the data. + * + * This method may either return a string or a readable stream resource + * + * @return mixed + */ + public function get() + { + return $this->contents; + } + + /** + * Returns the size of the file, in bytes. + * + * @return int + */ + public function getSize() + { + return strlen($this->contents); + } + + /** + * Returns the ETag for a file. + * + * An ETag is a unique identifier representing the current version of the file. If the file changes, the ETag MUST change. + * The ETag is an arbitrary string, but MUST be surrounded by double-quotes. + * + * Return null if the ETag can not effectively be determined + * + * @return string + */ + public function getETag() + { + return '"'.sha1($this->contents).'"'; + } + + /** + * Returns the mime-type for a file. + * + * If null is returned, we'll assume application/octet-stream + * + * @return string + */ + public function getContentType() + { + return $this->mimeType; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/StringUtil.php b/lib/composer/vendor/sabre/dav/lib/DAV/StringUtil.php new file mode 100644 index 0000000..edfb7fa --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/StringUtil.php @@ -0,0 +1,86 @@ + 'The current synctoken', + * 'added' => [ + * 'new.txt', + * ], + * 'modified' => [ + * 'modified.txt', + * ], + * 'deleted' => array( + * 'foo.php.bak', + * 'old.txt' + * ) + * ]; + * + * The syncToken property should reflect the *current* syncToken of the + * collection, as reported getSyncToken(). This is needed here too, to + * ensure the operation is atomic. + * + * If the syncToken is specified as null, this is an initial sync, and all + * members should be reported. + * + * The modified property is an array of nodenames that have changed since + * the last token. + * + * The deleted property is an array with nodenames, that have been deleted + * from collection. + * + * The second argument is basically the 'depth' of the report. If it's 1, + * you only have to report changes that happened only directly in immediate + * descendants. If it's 2, it should also include changes from the nodes + * below the child collections. (grandchildren) + * + * The third (optional) argument allows a client to specify how many + * results should be returned at most. If the limit is not specified, it + * should be treated as infinite. + * + * If the limit (infinite or not) is higher than you're willing to return, + * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. + * + * If the syncToken is expired (due to data cleanup) or unknown, you must + * return null. + * + * The limit is 'suggestive'. You are free to ignore it. + * + * @param string $syncToken + * @param int $syncLevel + * @param int $limit + * + * @return array|null + */ + public function getChanges($syncToken, $syncLevel, $limit = null); +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Sync/Plugin.php b/lib/composer/vendor/sabre/dav/lib/DAV/Sync/Plugin.php new file mode 100644 index 0000000..8609f75 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Sync/Plugin.php @@ -0,0 +1,249 @@ +server = $server; + $server->xml->elementMap['{DAV:}sync-collection'] = 'Sabre\\DAV\\Xml\\Request\\SyncCollectionReport'; + + $self = $this; + + $server->on('report', function ($reportName, $dom, $uri) use ($self) { + if ('{DAV:}sync-collection' === $reportName) { + $this->server->transactionType = 'report-sync-collection'; + $self->syncCollection($uri, $dom); + + return false; + } + }); + + $server->on('propFind', [$this, 'propFind']); + $server->on('validateTokens', [$this, 'validateTokens']); + } + + /** + * Returns a list of reports this plugin supports. + * + * This will be used in the {DAV:}supported-report-set property. + * Note that you still need to subscribe to the 'report' event to actually + * implement them + * + * @param string $uri + * + * @return array + */ + public function getSupportedReportSet($uri) + { + $node = $this->server->tree->getNodeForPath($uri); + if ($node instanceof ISyncCollection && $node->getSyncToken()) { + return [ + '{DAV:}sync-collection', + ]; + } + + return []; + } + + /** + * This method handles the {DAV:}sync-collection HTTP REPORT. + * + * @param string $uri + */ + public function syncCollection($uri, SyncCollectionReport $report) + { + // Getting the data + $node = $this->server->tree->getNodeForPath($uri); + if (!$node instanceof ISyncCollection) { + throw new DAV\Exception\ReportNotSupported('The {DAV:}sync-collection REPORT is not supported on this url.'); + } + $token = $node->getSyncToken(); + if (!$token) { + throw new DAV\Exception\ReportNotSupported('No sync information is available at this node'); + } + + $syncToken = $report->syncToken; + if (!is_null($syncToken)) { + // Sync-token must start with our prefix + if (self::SYNCTOKEN_PREFIX !== substr($syncToken, 0, strlen(self::SYNCTOKEN_PREFIX))) { + throw new DAV\Exception\InvalidSyncToken('Invalid or unknown sync token'); + } + + $syncToken = substr($syncToken, strlen(self::SYNCTOKEN_PREFIX)); + } + $changeInfo = $node->getChanges($syncToken, $report->syncLevel, $report->limit); + + if (is_null($changeInfo)) { + throw new DAV\Exception\InvalidSyncToken('Invalid or unknown sync token'); + } + + if (!array_key_exists('result_truncated', $changeInfo)) { + $changeInfo['result_truncated'] = false; + } + + // Encoding the response + $this->sendSyncCollectionResponse( + $changeInfo['syncToken'], + $uri, + $changeInfo['added'], + $changeInfo['modified'], + $changeInfo['deleted'], + $report->properties, + $changeInfo['result_truncated'] + ); + } + + /** + * Sends the response to a sync-collection request. + * + * @param string $syncToken + * @param string $collectionUrl + */ + protected function sendSyncCollectionResponse($syncToken, $collectionUrl, array $added, array $modified, array $deleted, array $properties, bool $resultTruncated = false) + { + $fullPaths = []; + + // Pre-fetching children, if this is possible. + foreach (array_merge($added, $modified) as $item) { + $fullPath = $collectionUrl.'/'.$item; + $fullPaths[] = $fullPath; + } + + $responses = []; + foreach ($this->server->getPropertiesForMultiplePaths($fullPaths, $properties) as $fullPath => $props) { + // The 'Property_Response' class is responsible for generating a + // single {DAV:}response xml element. + $responses[] = new DAV\Xml\Element\Response($fullPath, $props); + } + + // Deleted items also show up as 'responses'. They have no properties, + // and a single {DAV:}status element set as 'HTTP/1.1 404 Not Found'. + foreach ($deleted as $item) { + $fullPath = $collectionUrl.'/'.$item; + $responses[] = new DAV\Xml\Element\Response($fullPath, [], 404); + } + if ($resultTruncated) { + $responses[] = new DAV\Xml\Element\Response($collectionUrl.'/', [], 507); + } + + $multiStatus = new DAV\Xml\Response\MultiStatus($responses, self::SYNCTOKEN_PREFIX.$syncToken); + + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setBody( + $this->server->xml->write('{DAV:}multistatus', $multiStatus, $this->server->getBaseUri()) + ); + } + + /** + * This method is triggered whenever properties are requested for a node. + * We intercept this to see if we must return a {DAV:}sync-token. + */ + public function propFind(DAV\PropFind $propFind, DAV\INode $node) + { + $propFind->handle('{DAV:}sync-token', function () use ($node) { + if (!$node instanceof ISyncCollection || !$token = $node->getSyncToken()) { + return; + } + + return self::SYNCTOKEN_PREFIX.$token; + }); + } + + /** + * The validateTokens event is triggered before every request. + * + * It's a moment where this plugin can check all the supplied lock tokens + * in the If: header, and check if they are valid. + * + * @param array $conditions + */ + public function validateTokens(RequestInterface $request, &$conditions) + { + foreach ($conditions as $kk => $condition) { + foreach ($condition['tokens'] as $ii => $token) { + // Sync-tokens must always start with our designated prefix. + if (self::SYNCTOKEN_PREFIX !== substr($token['token'], 0, strlen(self::SYNCTOKEN_PREFIX))) { + continue; + } + + // Checking if the token is a match. + $node = $this->server->tree->getNodeForPath($condition['uri']); + + if ( + $node instanceof ISyncCollection && + $node->getSyncToken() == substr($token['token'], strlen(self::SYNCTOKEN_PREFIX)) + ) { + $conditions[$kk]['tokens'][$ii]['validToken'] = true; + } + } + } + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds support for WebDAV Collection Sync (rfc6578)', + 'link' => 'http://sabre.io/dav/sync/', + ]; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/TemporaryFileFilterPlugin.php b/lib/composer/vendor/sabre/dav/lib/DAV/TemporaryFileFilterPlugin.php new file mode 100644 index 0000000..9f8ec5b --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/TemporaryFileFilterPlugin.php @@ -0,0 +1,298 @@ +dataDir = $dataDir; + } + + /** + * Initialize the plugin. + * + * This is called automatically be the Server class after this plugin is + * added with Sabre\DAV\Server::addPlugin() + */ + public function initialize(Server $server) + { + $this->server = $server; + $server->on('beforeMethod:*', [$this, 'beforeMethod']); + $server->on('beforeCreateFile', [$this, 'beforeCreateFile']); + } + + /** + * This method is called before any HTTP method handler. + * + * This method intercepts any GET, DELETE, PUT and PROPFIND calls to + * filenames that are known to match the 'temporary file' regex. + * + * @return bool + */ + public function beforeMethod(RequestInterface $request, ResponseInterface $response) + { + if (!$tempLocation = $this->isTempFile($request->getPath())) { + return; + } + + switch ($request->getMethod()) { + case 'GET': + return $this->httpGet($request, $response, $tempLocation); + case 'PUT': + return $this->httpPut($request, $response, $tempLocation); + case 'PROPFIND': + return $this->httpPropfind($request, $response, $tempLocation); + case 'DELETE': + return $this->httpDelete($request, $response, $tempLocation); + } + + return; + } + + /** + * This method is invoked if some subsystem creates a new file. + * + * This is used to deal with HTTP LOCK requests which create a new + * file. + * + * @param string $uri + * @param resource $data + * @param bool $modified should be set to true, if this event handler + * changed &$data + * + * @return bool + */ + public function beforeCreateFile($uri, $data, ICollection $parent, $modified) + { + if ($tempPath = $this->isTempFile($uri)) { + $hR = $this->server->httpResponse; + $hR->setHeader('X-Sabre-Temp', 'true'); + file_put_contents($tempPath, $data); + + return false; + } + + return; + } + + /** + * This method will check if the url matches the temporary file pattern + * if it does, it will return an path based on $this->dataDir for the + * temporary file storage. + * + * @param string $path + * + * @return bool|string + */ + protected function isTempFile($path) + { + // We're only interested in the basename. + list(, $tempPath) = Uri\split($path); + + if (null === $tempPath) { + return false; + } + + foreach ($this->temporaryFilePatterns as $tempFile) { + if (preg_match($tempFile, $tempPath)) { + return $this->getDataDir().'/sabredav_'.md5($path).'.tempfile'; + } + } + + return false; + } + + /** + * This method handles the GET method for temporary files. + * If the file doesn't exist, it will return false which will kick in + * the regular system for the GET method. + * + * @param string $tempLocation + * + * @return bool + */ + public function httpGet(RequestInterface $request, ResponseInterface $hR, $tempLocation) + { + if (!file_exists($tempLocation)) { + return; + } + + $hR->setHeader('Content-Type', 'application/octet-stream'); + $hR->setHeader('Content-Length', filesize($tempLocation)); + $hR->setHeader('X-Sabre-Temp', 'true'); + $hR->setStatus(200); + $hR->setBody(fopen($tempLocation, 'r')); + + return false; + } + + /** + * This method handles the PUT method. + * + * @param string $tempLocation + * + * @return bool + */ + public function httpPut(RequestInterface $request, ResponseInterface $hR, $tempLocation) + { + $hR->setHeader('X-Sabre-Temp', 'true'); + + $newFile = !file_exists($tempLocation); + + if (!$newFile && ($this->server->httpRequest->getHeader('If-None-Match'))) { + throw new Exception\PreconditionFailed('The resource already exists, and an If-None-Match header was supplied'); + } + + file_put_contents($tempLocation, $this->server->httpRequest->getBody()); + $hR->setStatus($newFile ? 201 : 200); + + return false; + } + + /** + * This method handles the DELETE method. + * + * If the file didn't exist, it will return false, which will make the + * standard HTTP DELETE handler kick in. + * + * @param string $tempLocation + * + * @return bool + */ + public function httpDelete(RequestInterface $request, ResponseInterface $hR, $tempLocation) + { + if (!file_exists($tempLocation)) { + return; + } + + unlink($tempLocation); + $hR->setHeader('X-Sabre-Temp', 'true'); + $hR->setStatus(204); + + return false; + } + + /** + * This method handles the PROPFIND method. + * + * It's a very lazy method, it won't bother checking the request body + * for which properties were requested, and just sends back a default + * set of properties. + * + * @param string $tempLocation + * + * @return bool + */ + public function httpPropfind(RequestInterface $request, ResponseInterface $hR, $tempLocation) + { + if (!file_exists($tempLocation)) { + return; + } + + $hR->setHeader('X-Sabre-Temp', 'true'); + $hR->setStatus(207); + $hR->setHeader('Content-Type', 'application/xml; charset=utf-8'); + + $properties = [ + 'href' => $request->getPath(), + 200 => [ + '{DAV:}getlastmodified' => new Xml\Property\GetLastModified(filemtime($tempLocation)), + '{DAV:}getcontentlength' => filesize($tempLocation), + '{DAV:}resourcetype' => new Xml\Property\ResourceType(null), + '{'.Server::NS_SABREDAV.'}tempFile' => true, + ], + ]; + + $data = $this->server->generateMultiStatus([$properties]); + $hR->setBody($data); + + return false; + } + + /** + * This method returns the directory where the temporary files should be stored. + * + * @return string + */ + protected function getDataDir() + { + return $this->dataDir; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Tree.php b/lib/composer/vendor/sabre/dav/lib/DAV/Tree.php new file mode 100644 index 0000000..1483e1b --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Tree.php @@ -0,0 +1,342 @@ +rootNode = $rootNode; + } + + /** + * Returns the INode object for the requested path. + * + * @param string $path + * + * @return INode + */ + public function getNodeForPath($path) + { + $path = trim($path, '/'); + if (isset($this->cache[$path])) { + return $this->cache[$path]; + } + + // Is it the root node? + if (!strlen($path)) { + return $this->rootNode; + } + + $node = $this->rootNode; + + // look for any cached parent and collect the parts below the parent + $parts = []; + $remainingPath = $path; + do { + list($remainingPath, $baseName) = Uri\split($remainingPath); + array_unshift($parts, $baseName); + + if (isset($this->cache[$remainingPath])) { + $node = $this->cache[$remainingPath]; + break; + } + } while ('' !== $remainingPath); + + while (count($parts)) { + if (!($node instanceof ICollection)) { + throw new Exception\NotFound('Could not find node at path: '.$path); + } + + if ($node instanceof INodeByPath) { + $targetNode = $node->getNodeForPath(implode('/', $parts)); + if ($targetNode instanceof Node) { + $node = $targetNode; + break; + } + } + + $part = array_shift($parts); + if ('' !== $part) { + $node = $node->getChild($part); + } + } + + $this->cache[$path] = $node; + + return $node; + } + + /** + * This function allows you to check if a node exists. + * + * Implementors of this class should override this method to make + * it cheaper. + * + * @param string $path + * + * @return bool + */ + public function nodeExists($path) + { + try { + // The root always exists + if ('' === $path) { + return true; + } + + list($parent, $base) = Uri\split($path); + + $parentNode = $this->getNodeForPath($parent); + if (!$parentNode instanceof ICollection) { + return false; + } + + return $parentNode->childExists($base); + } catch (Exception\NotFound $e) { + return false; + } + } + + /** + * Copies a file from path to another. + * + * @param string $sourcePath The source location + * @param string $destinationPath The full destination path + */ + public function copy($sourcePath, $destinationPath) + { + $sourceNode = $this->getNodeForPath($sourcePath); + + // grab the dirname and basename components + list($destinationDir, $destinationName) = Uri\split($destinationPath); + + $destinationParent = $this->getNodeForPath($destinationDir); + // Check if the target can handle the copy itself. If not, we do it ourselves. + if (!$destinationParent instanceof ICopyTarget || !$destinationParent->copyInto($destinationName, $sourcePath, $sourceNode)) { + $this->copyNode($sourceNode, $destinationParent, $destinationName); + } + + $this->markDirty($destinationDir); + } + + /** + * Moves a file from one location to another. + * + * @param string $sourcePath The path to the file which should be moved + * @param string $destinationPath The full destination path, so not just the destination parent node + */ + public function move($sourcePath, $destinationPath) + { + list($sourceDir) = Uri\split($sourcePath); + list($destinationDir, $destinationName) = Uri\split($destinationPath); + + if ($sourceDir === $destinationDir) { + // If this is a 'local' rename, it means we can just trigger a rename. + $sourceNode = $this->getNodeForPath($sourcePath); + $sourceNode->setName($destinationName); + } else { + $newParentNode = $this->getNodeForPath($destinationDir); + $moveSuccess = false; + if ($newParentNode instanceof IMoveTarget) { + // The target collection may be able to handle the move + $sourceNode = $this->getNodeForPath($sourcePath); + $moveSuccess = $newParentNode->moveInto($destinationName, $sourcePath, $sourceNode); + } + if (!$moveSuccess) { + $this->copy($sourcePath, $destinationPath); + $this->getNodeForPath($sourcePath)->delete(); + } + } + $this->markDirty($sourceDir); + $this->markDirty($destinationDir); + } + + /** + * Deletes a node from the tree. + * + * @param string $path + */ + public function delete($path) + { + $node = $this->getNodeForPath($path); + $node->delete(); + + list($parent) = Uri\split($path); + $this->markDirty($parent); + } + + /** + * Returns a list of childnodes for a given path. + * + * @param string $path + * + * @return \Traversable + */ + public function getChildren($path) + { + $node = $this->getNodeForPath($path); + $basePath = trim($path, '/'); + if ('' !== $basePath) { + $basePath .= '/'; + } + + foreach ($node->getChildren() as $child) { + $this->cache[$basePath.$child->getName()] = $child; + yield $child; + } + } + + /** + * This method is called with every tree update. + * + * Examples of tree updates are: + * * node deletions + * * node creations + * * copy + * * move + * * renaming nodes + * + * If Tree classes implement a form of caching, this will allow + * them to make sure caches will be expired. + * + * If a path is passed, it is assumed that the entire subtree is dirty + * + * @param string $path + */ + public function markDirty($path) + { + // We don't care enough about sub-paths + // flushing the entire cache + $path = trim($path, '/'); + foreach ($this->cache as $nodePath => $node) { + if ('' === $path || $nodePath == $path || 0 === strpos((string) $nodePath, $path.'/')) { + unset($this->cache[$nodePath]); + } + } + } + + /** + * This method tells the tree system to pre-fetch and cache a list of + * children of a single parent. + * + * There are a bunch of operations in the WebDAV stack that request many + * children (based on uris), and sometimes fetching many at once can + * optimize this. + * + * This method returns an array with the found nodes. It's keys are the + * original paths. The result may be out of order. + * + * @param array $paths list of nodes that must be fetched + * + * @return array + */ + public function getMultipleNodes($paths) + { + // Finding common parents + $parents = []; + foreach ($paths as $path) { + list($parent, $node) = Uri\split($path); + if (!isset($parents[$parent])) { + $parents[$parent] = [$node]; + } else { + $parents[$parent][] = $node; + } + } + + $result = []; + + foreach ($parents as $parent => $children) { + $parentNode = $this->getNodeForPath($parent); + if ($parentNode instanceof IMultiGet) { + foreach ($parentNode->getMultipleChildren($children) as $childNode) { + $fullPath = $parent.'/'.$childNode->getName(); + $result[$fullPath] = $childNode; + $this->cache[$fullPath] = $childNode; + } + } else { + foreach ($children as $child) { + $fullPath = $parent.'/'.$child; + $result[$fullPath] = $this->getNodeForPath($fullPath); + } + } + } + + return $result; + } + + /** + * copyNode. + * + * @param string $destinationName + */ + protected function copyNode(INode $source, ICollection $destinationParent, $destinationName = null) + { + if ('' === (string) $destinationName) { + $destinationName = $source->getName(); + } + + $destination = null; + + if ($source instanceof IFile) { + $data = $source->get(); + + // If the body was a string, we need to convert it to a stream + if (is_string($data)) { + $stream = fopen('php://temp', 'r+'); + fwrite($stream, $data); + rewind($stream); + $data = $stream; + } + $destinationParent->createFile($destinationName, $data); + $destination = $destinationParent->getChild($destinationName); + } elseif ($source instanceof ICollection) { + $destinationParent->createDirectory($destinationName); + + $destination = $destinationParent->getChild($destinationName); + foreach ($source->getChildren() as $child) { + $this->copyNode($child, $destination); + } + } + if ($source instanceof IProperties && $destination instanceof IProperties) { + $props = $source->getProperties([]); + $propPatch = new PropPatch($props); + $destination->propPatch($propPatch); + $propPatch->commit(); + } + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/UUIDUtil.php b/lib/composer/vendor/sabre/dav/lib/DAV/UUIDUtil.php new file mode 100644 index 0000000..8c36e1b --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/UUIDUtil.php @@ -0,0 +1,66 @@ +value array. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Prop implements XmlDeserializable +{ + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + // If there's no children, we don't do anything. + if ($reader->isEmptyElement) { + $reader->next(); + + return []; + } + + $values = []; + + $reader->read(); + do { + if (Reader::ELEMENT === $reader->nodeType) { + $clark = $reader->getClark(); + $values[$clark] = self::parseCurrentElement($reader)['value']; + } else { + $reader->read(); + } + } while (Reader::END_ELEMENT !== $reader->nodeType); + + $reader->read(); + + return $values; + } + + /** + * This function behaves similar to Sabre\Xml\Reader::parseCurrentElement, + * but instead of creating deep xml array structures, it will turn any + * top-level element it doesn't recognize into either a string, or an + * XmlFragment class. + * + * This method returns arn array with 2 properties: + * * name - A clark-notation XML element name. + * * value - The parsed value. + * + * @return array + */ + private static function parseCurrentElement(Reader $reader) + { + $name = $reader->getClark(); + + if (array_key_exists($name, $reader->elementMap)) { + $deserializer = $reader->elementMap[$name]; + if (is_subclass_of($deserializer, 'Sabre\\Xml\\XmlDeserializable')) { + $value = call_user_func([$deserializer, 'xmlDeserialize'], $reader); + } elseif (is_callable($deserializer)) { + $value = call_user_func($deserializer, $reader); + } else { + $type = gettype($deserializer); + if ('string' === $type) { + $type .= ' ('.$deserializer.')'; + } elseif ('object' === $type) { + $type .= ' ('.get_class($deserializer).')'; + } + throw new \LogicException('Could not use this type as a deserializer: '.$type); + } + } else { + $value = Complex::xmlDeserialize($reader); + } + + return [ + 'name' => $name, + 'value' => $value, + ]; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Element/Response.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Element/Response.php new file mode 100644 index 0000000..df92914 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Element/Response.php @@ -0,0 +1,267 @@ +href = $href; + $this->responseProperties = $responseProperties; + $this->httpStatus = $httpStatus; + } + + /** + * Returns the url. + * + * @return string + */ + public function getHref() + { + return $this->href; + } + + /** + * Returns the httpStatus value. + * + * @return string + */ + public function getHttpStatus() + { + return $this->httpStatus; + } + + /** + * Returns the property list. + * + * @return array + */ + public function getResponseProperties() + { + return $this->responseProperties; + } + + /** + * The serialize method is called during xml writing. + * + * It should use the $writer argument to encode this object into XML. + * + * Important note: it is not needed to create the parent element. The + * parent element is already created, and we only have to worry about + * attributes, child elements and text (if any). + * + * Important note 2: If you are writing any new elements, you are also + * responsible for closing them. + */ + public function xmlSerialize(Writer $writer) + { + /* + * Accordingly to the RFC the element looks like: + * + * + * So the response + * - MUST contain a href and + * - EITHER a status and additional href(s) + * OR one or more propstat(s) + */ + $writer->writeElement('{DAV:}href', $writer->contextUri.\Sabre\HTTP\encodePath($this->getHref())); + + $empty = true; + $httpStatus = $this->getHTTPStatus(); + + // Add propstat elements + foreach ($this->getResponseProperties() as $status => $properties) { + // Skipping empty lists + if (!$properties || (!is_int($status) && !ctype_digit($status))) { + continue; + } + $empty = false; + $writer->startElement('{DAV:}propstat'); + $writer->writeElement('{DAV:}prop', $properties); + $writer->writeElement('{DAV:}status', 'HTTP/1.1 '.$status.' '.\Sabre\HTTP\Response::$statusCodes[$status]); + $writer->endElement(); // {DAV:}propstat + } + + // The WebDAV spec only allows the status element on responses _without_ a propstat + if ($empty) { + if (null !== $httpStatus) { + $writer->writeElement('{DAV:}status', 'HTTP/1.1 '.$httpStatus.' '.\Sabre\HTTP\Response::$statusCodes[$httpStatus]); + } else { + /* + * The WebDAV spec _requires_ at least one DAV:propstat to appear for + * every DAV:response if there is no status. + * In some circumstances however, there are no properties to encode. + * + * In those cases we MUST specify at least one DAV:propstat anyway, with + * no properties. + */ + $writer->writeElement('{DAV:}propstat', [ + '{DAV:}prop' => [], + '{DAV:}status' => 'HTTP/1.1 418 '.\Sabre\HTTP\Response::$statusCodes[418], + ]); + } + } + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $reader->pushContext(); + + $reader->elementMap['{DAV:}propstat'] = 'Sabre\\Xml\\Element\\KeyValue'; + + // We are overriding the parser for {DAV:}prop. This deserializer is + // almost identical to the one for Sabre\Xml\Element\KeyValue. + // + // The difference is that if there are any child-elements inside of + // {DAV:}prop, that have no value, normally any deserializers are + // called. But we don't want this, because a singular element without + // child-elements implies 'no value' in {DAV:}prop, so we want to skip + // deserializers and just set null for those. + $reader->elementMap['{DAV:}prop'] = function (Reader $reader) { + if ($reader->isEmptyElement) { + $reader->next(); + + return []; + } + + if (!$reader->read()) { + $reader->next(); + + return []; + } + + if (Reader::END_ELEMENT === $reader->nodeType) { + $reader->next(); + + return []; + } + + $values = []; + + do { + if (Reader::ELEMENT === $reader->nodeType) { + $clark = $reader->getClark(); + + if ($reader->isEmptyElement) { + $values[$clark] = null; + $reader->next(); + } else { + $values[$clark] = $reader->parseCurrentElement()['value']; + } + } else { + if (!$reader->read()) { + break; + } + } + } while (Reader::END_ELEMENT !== $reader->nodeType); + + $reader->read(); + + return $values; + }; + $elems = $reader->parseInnerTree(); + $reader->popContext(); + + $href = null; + $propertyLists = []; + $statusCode = null; + + foreach ($elems as $elem) { + switch ($elem['name']) { + case '{DAV:}href': + $href = $elem['value']; + break; + case '{DAV:}propstat': + $status = $elem['value']['{DAV:}status']; + list(, $status) = explode(' ', $status, 3); + $properties = isset($elem['value']['{DAV:}prop']) ? $elem['value']['{DAV:}prop'] : []; + if ($properties) { + $propertyLists[$status] = $properties; + } + break; + case '{DAV:}status': + list(, $statusCode) = explode(' ', $elem['value'], 3); + break; + } + } + + return new self($href, $propertyLists, $statusCode); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Element/Sharee.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Element/Sharee.php new file mode 100644 index 0000000..33564d8 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Element/Sharee.php @@ -0,0 +1,189 @@ + $v) { + if (property_exists($this, $k)) { + $this->$k = $v; + } else { + throw new \InvalidArgumentException('Unknown property: '.$k); + } + } + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + $writer->write([ + new Href($this->href), + '{DAV:}prop' => $this->properties, + '{DAV:}share-access' => new ShareAccess($this->access), + ]); + switch ($this->inviteStatus) { + case Plugin::INVITE_NORESPONSE: + $writer->writeElement('{DAV:}invite-noresponse'); + break; + case Plugin::INVITE_ACCEPTED: + $writer->writeElement('{DAV:}invite-accepted'); + break; + case Plugin::INVITE_DECLINED: + $writer->writeElement('{DAV:}invite-declined'); + break; + case Plugin::INVITE_INVALID: + $writer->writeElement('{DAV:}invite-invalid'); + break; + } + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + // Temporarily override configuration + $reader->pushContext(); + $reader->elementMap['{DAV:}share-access'] = 'Sabre\DAV\Xml\Property\ShareAccess'; + $reader->elementMap['{DAV:}prop'] = 'Sabre\Xml\Deserializer\keyValue'; + + $elems = Deserializer\keyValue($reader, 'DAV:'); + + // Restore previous configuration + $reader->popContext(); + + $sharee = new self(); + if (!isset($elems['href'])) { + throw new BadRequest('Every {DAV:}sharee must have a {DAV:}href child-element'); + } + $sharee->href = $elems['href']; + + if (isset($elems['prop'])) { + $sharee->properties = $elems['prop']; + } + if (isset($elems['comment'])) { + $sharee->comment = $elems['comment']; + } + if (!isset($elems['share-access'])) { + throw new BadRequest('Every {DAV:}sharee must have a {DAV:}share-access child element'); + } + $sharee->access = $elems['share-access']->getValue(); + + return $sharee; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/Complex.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/Complex.php new file mode 100644 index 0000000..787d30d --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/Complex.php @@ -0,0 +1,87 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $xml = $reader->readInnerXml(); + + if (Reader::ELEMENT === $reader->nodeType && $reader->isEmptyElement) { + // Easy! + $reader->next(); + + return null; + } + // Now we have a copy of the inner xml, we need to traverse it to get + // all the strings. If there's no non-string data, we just return the + // string, otherwise we return an instance of this class. + $reader->read(); + + $nonText = false; + $text = ''; + + while (true) { + switch ($reader->nodeType) { + case Reader::ELEMENT: + $nonText = true; + $reader->next(); + continue 2; + case Reader::TEXT: + case Reader::CDATA: + $text .= $reader->value; + break; + case Reader::END_ELEMENT: + break 2; + } + $reader->read(); + } + + // Make sure we advance the cursor one step further. + $reader->read(); + + if ($nonText) { + $new = new self($xml); + + return $new; + } else { + return $text; + } + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/GetLastModified.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/GetLastModified.php new file mode 100644 index 0000000..efc15c2 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/GetLastModified.php @@ -0,0 +1,103 @@ +time = clone $time; + } else { + $this->time = new DateTime('@'.$time); + } + + // Setting timezone to UTC + $this->time->setTimezone(new DateTimeZone('UTC')); + } + + /** + * getTime. + * + * @return DateTime + */ + public function getTime() + { + return $this->time; + } + + /** + * The serialize method is called during xml writing. + * + * It should use the $writer argument to encode this object into XML. + * + * Important note: it is not needed to create the parent element. The + * parent element is already created, and we only have to worry about + * attributes, child elements and text (if any). + * + * Important note 2: If you are writing any new elements, you are also + * responsible for closing them. + */ + public function xmlSerialize(Writer $writer) + { + $writer->write( + HTTP\toDate($this->time) + ); + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * Important note 2: You are responsible for advancing the reader to the + * next element. Not doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + return new self(new DateTime($reader->parseInnerTree())); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/Href.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/Href.php new file mode 100644 index 0000000..d4e43da --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/Href.php @@ -0,0 +1,166 @@ +hrefs = $hrefs; + } + + /** + * Returns the first Href. + * + * @return string|null + */ + public function getHref() + { + return $this->hrefs[0] ?? null; + } + + /** + * Returns the hrefs as an array. + * + * @return array + */ + public function getHrefs() + { + return $this->hrefs; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + foreach ($this->getHrefs() as $href) { + $href = Uri\resolve($writer->contextUri, $href); + $writer->writeElement('{DAV:}href', $href); + } + } + + /** + * Generate html representation for this value. + * + * The html output is 100% trusted, and no effort is being made to sanitize + * it. It's up to the implementor to sanitize user provided values. + * + * The output must be in UTF-8. + * + * The baseUri parameter is a url to the root of the application, and can + * be used to construct local links. + * + * @return string + */ + public function toHtml(HtmlOutputHelper $html) + { + $links = []; + foreach ($this->getHrefs() as $href) { + $links[] = $html->link($href); + } + + return implode('
', $links); + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $hrefs = []; + foreach ((array) $reader->parseInnerTree() as $elem) { + if ('{DAV:}href' !== $elem['name']) { + continue; + } + + $hrefs[] = $elem['value']; + } + if ($hrefs) { + return new self($hrefs); + } + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/Invite.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/Invite.php new file mode 100644 index 0000000..e3f0a61 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/Invite.php @@ -0,0 +1,66 @@ +sharees = $sharees; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + foreach ($this->sharees as $sharee) { + $writer->writeElement('{DAV:}sharee', $sharee); + } + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/LocalHref.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/LocalHref.php new file mode 100644 index 0000000..cb79497 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/LocalHref.php @@ -0,0 +1,48 @@ +locks = $locks; + } + + /** + * The serialize method is called during xml writing. + * + * It should use the $writer argument to encode this object into XML. + * + * Important note: it is not needed to create the parent element. The + * parent element is already created, and we only have to worry about + * attributes, child elements and text (if any). + * + * Important note 2: If you are writing any new elements, you are also + * responsible for closing them. + */ + public function xmlSerialize(Writer $writer) + { + foreach ($this->locks as $lock) { + $writer->startElement('{DAV:}activelock'); + + $writer->startElement('{DAV:}lockscope'); + if (LockInfo::SHARED === $lock->scope) { + $writer->writeElement('{DAV:}shared'); + } else { + $writer->writeElement('{DAV:}exclusive'); + } + + $writer->endElement(); // {DAV:}lockscope + + $writer->startElement('{DAV:}locktype'); + $writer->writeElement('{DAV:}write'); + $writer->endElement(); // {DAV:}locktype + + if (!self::$hideLockRoot) { + $writer->startElement('{DAV:}lockroot'); + $writer->writeElement('{DAV:}href', $writer->contextUri.$lock->uri); + $writer->endElement(); // {DAV:}lockroot + } + $writer->writeElement('{DAV:}depth', (DAV\Server::DEPTH_INFINITY == $lock->depth ? 'infinity' : $lock->depth)); + $writer->writeElement('{DAV:}timeout', (LockInfo::TIMEOUT_INFINITE === $lock->timeout ? 'Infinite' : 'Second-'.$lock->timeout)); + + // optional according to https://tools.ietf.org/html/rfc4918#section-6.5 + if (null !== $lock->token && '' !== $lock->token) { + $writer->startElement('{DAV:}locktoken'); + $writer->writeElement('{DAV:}href', 'opaquelocktoken:'.$lock->token); + $writer->endElement(); // {DAV:}locktoken + } + + if ($lock->owner) { + $writer->writeElement('{DAV:}owner', new XmlFragment($lock->owner)); + } + $writer->endElement(); // {DAV:}activelock + } + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/ResourceType.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/ResourceType.php new file mode 100644 index 0000000..75ddcba --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/ResourceType.php @@ -0,0 +1,120 @@ +value; + } + + /** + * Checks if the principal contains a certain value. + * + * @param string $type + * + * @return bool + */ + public function is($type) + { + return in_array($type, $this->value); + } + + /** + * Adds a resourcetype value to this property. + * + * @param string $type + */ + public function add($type) + { + $this->value[] = $type; + $this->value = array_unique($this->value); + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * Important note 2: You are responsible for advancing the reader to the + * next element. Not doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + return new self(parent::xmlDeserialize($reader)); + } + + /** + * Generate html representation for this value. + * + * The html output is 100% trusted, and no effort is being made to sanitize + * it. It's up to the implementor to sanitize user provided values. + * + * The output must be in UTF-8. + * + * The baseUri parameter is a url to the root of the application, and can + * be used to construct local links. + * + * @return string + */ + public function toHtml(HtmlOutputHelper $html) + { + return implode( + ', ', + array_map([$html, 'xmlName'], $this->getValue()) + ); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/ShareAccess.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/ShareAccess.php new file mode 100644 index 0000000..fdd5555 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/ShareAccess.php @@ -0,0 +1,135 @@ +value = $shareAccess; + } + + /** + * Returns the current value. + * + * @return int + */ + public function getValue() + { + return $this->value; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + switch ($this->value) { + case SharingPlugin::ACCESS_NOTSHARED: + $writer->writeElement('{DAV:}not-shared'); + break; + case SharingPlugin::ACCESS_SHAREDOWNER: + $writer->writeElement('{DAV:}shared-owner'); + break; + case SharingPlugin::ACCESS_READ: + $writer->writeElement('{DAV:}read'); + break; + case SharingPlugin::ACCESS_READWRITE: + $writer->writeElement('{DAV:}read-write'); + break; + case SharingPlugin::ACCESS_NOACCESS: + $writer->writeElement('{DAV:}no-access'); + break; + } + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $elems = $reader->parseInnerTree(); + foreach ($elems as $elem) { + switch ($elem['name']) { + case '{DAV:}not-shared': + return new self(SharingPlugin::ACCESS_NOTSHARED); + case '{DAV:}shared-owner': + return new self(SharingPlugin::ACCESS_SHAREDOWNER); + case '{DAV:}read': + return new self(SharingPlugin::ACCESS_READ); + case '{DAV:}read-write': + return new self(SharingPlugin::ACCESS_READWRITE); + case '{DAV:}no-access': + return new self(SharingPlugin::ACCESS_NOACCESS); + } + } + throw new BadRequest('Invalid value for {DAV:}share-access element'); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedLock.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedLock.php new file mode 100644 index 0000000..100829c --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedLock.php @@ -0,0 +1,52 @@ +writeElement('{DAV:}lockentry', [ + '{DAV:}lockscope' => ['{DAV:}exclusive' => null], + '{DAV:}locktype' => ['{DAV:}write' => null], + ]); + $writer->writeElement('{DAV:}lockentry', [ + '{DAV:}lockscope' => ['{DAV:}shared' => null], + '{DAV:}locktype' => ['{DAV:}write' => null], + ]); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedMethodSet.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedMethodSet.php new file mode 100644 index 0000000..6344010 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedMethodSet.php @@ -0,0 +1,114 @@ +methods = $methods; + } + + /** + * Returns the list of supported http methods. + * + * @return string[] + */ + public function getValue() + { + return $this->methods; + } + + /** + * Returns true or false if the property contains a specific method. + * + * @param string $methodName + * + * @return bool + */ + public function has($methodName) + { + return in_array( + $methodName, + $this->methods + ); + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + foreach ($this->getValue() as $val) { + $writer->startElement('{DAV:}supported-method'); + $writer->writeAttribute('name', $val); + $writer->endElement(); + } + } + + /** + * Generate html representation for this value. + * + * The html output is 100% trusted, and no effort is being made to sanitize + * it. It's up to the implementor to sanitize user provided values. + * + * The output must be in UTF-8. + * + * The baseUri parameter is a url to the root of the application, and can + * be used to construct local links. + * + * @return string + */ + public function toHtml(HtmlOutputHelper $html) + { + return implode( + ', ', + array_map([$html, 'h'], $this->getValue()) + ); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedReportSet.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedReportSet.php new file mode 100644 index 0000000..0b4990e --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Property/SupportedReportSet.php @@ -0,0 +1,144 @@ +addReport($reports); + } + } + + /** + * Adds a report to this property. + * + * The report must be a string in clark-notation. + * Multiple reports can be specified as an array. + * + * @param mixed $report + */ + public function addReport($report) + { + $report = (array) $report; + + foreach ($report as $r) { + if (!preg_match('/^{([^}]*)}(.*)$/', $r)) { + throw new DAV\Exception('Reportname must be in clark-notation'); + } + $this->reports[] = $r; + } + } + + /** + * Returns the list of supported reports. + * + * @return string[] + */ + public function getValue() + { + return $this->reports; + } + + /** + * Returns true or false if the property contains a specific report. + * + * @param string $reportName + * + * @return bool + */ + public function has($reportName) + { + return in_array( + $reportName, + $this->reports + ); + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + foreach ($this->getValue() as $val) { + $writer->startElement('{DAV:}supported-report'); + $writer->startElement('{DAV:}report'); + $writer->writeElement($val); + $writer->endElement(); + $writer->endElement(); + } + } + + /** + * Generate html representation for this value. + * + * The html output is 100% trusted, and no effort is being made to sanitize + * it. It's up to the implementor to sanitize user provided values. + * + * The output must be in UTF-8. + * + * The baseUri parameter is a url to the root of the application, and can + * be used to construct local links. + * + * @return string + */ + public function toHtml(HtmlOutputHelper $html) + { + return implode( + ', ', + array_map([$html, 'xmlName'], $this->getValue()) + ); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/Lock.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/Lock.php new file mode 100644 index 0000000..57d12ef --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/Lock.php @@ -0,0 +1,84 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $reader->pushContext(); + $reader->elementMap['{DAV:}owner'] = 'Sabre\\Xml\\Element\\XmlFragment'; + + $values = KeyValue::xmlDeserialize($reader); + + $reader->popContext(); + + $new = new self(); + $new->owner = !empty($values['{DAV:}owner']) ? $values['{DAV:}owner']->getXml() : null; + $new->scope = LockInfo::SHARED; + + if (isset($values['{DAV:}lockscope'])) { + foreach ($values['{DAV:}lockscope'] as $elem) { + if ('{DAV:}exclusive' === $elem['name']) { + $new->scope = LockInfo::EXCLUSIVE; + } + } + } + + return $new; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/MkCol.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/MkCol.php new file mode 100644 index 0000000..e0d7e90 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/MkCol.php @@ -0,0 +1,80 @@ +value array with properties that are supposed to get set + * during creation of the new collection. + * + * @return array + */ + public function getProperties() + { + return $this->properties; + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $self = new self(); + + $elementMap = $reader->elementMap; + $elementMap['{DAV:}prop'] = 'Sabre\DAV\Xml\Element\Prop'; + $elementMap['{DAV:}set'] = 'Sabre\Xml\Element\KeyValue'; + $elementMap['{DAV:}remove'] = 'Sabre\Xml\Element\KeyValue'; + + $elems = $reader->parseInnerTree($elementMap); + + foreach ($elems as $elem) { + if ('{DAV:}set' === $elem['name']) { + $self->properties = array_merge($self->properties, $elem['value']['{DAV:}prop']); + } + } + + return $self; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/PropFind.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/PropFind.php new file mode 100644 index 0000000..505e7c7 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/PropFind.php @@ -0,0 +1,79 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $self = new self(); + + $reader->pushContext(); + $reader->elementMap['{DAV:}prop'] = 'Sabre\Xml\Element\Elements'; + + foreach (KeyValue::xmlDeserialize($reader) as $k => $v) { + switch ($k) { + case '{DAV:}prop': + $self->properties = $v; + break; + case '{DAV:}allprop': + $self->allProp = true; + } + } + + $reader->popContext(); + + return $self; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/PropPatch.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/PropPatch.php new file mode 100644 index 0000000..4a27095 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/PropPatch.php @@ -0,0 +1,109 @@ +properties as $propertyName => $propertyValue) { + if (is_null($propertyValue)) { + $writer->startElement('{DAV:}remove'); + $writer->write(['{DAV:}prop' => [$propertyName => $propertyValue]]); + $writer->endElement(); + } else { + $writer->startElement('{DAV:}set'); + $writer->write(['{DAV:}prop' => [$propertyName => $propertyValue]]); + $writer->endElement(); + } + } + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $self = new self(); + + $elementMap = $reader->elementMap; + $elementMap['{DAV:}prop'] = 'Sabre\DAV\Xml\Element\Prop'; + $elementMap['{DAV:}set'] = 'Sabre\Xml\Element\KeyValue'; + $elementMap['{DAV:}remove'] = 'Sabre\Xml\Element\KeyValue'; + + $elems = $reader->parseInnerTree($elementMap); + + foreach ($elems as $elem) { + if ('{DAV:}set' === $elem['name']) { + $self->properties = array_merge($self->properties, $elem['value']['{DAV:}prop']); + } + if ('{DAV:}remove' === $elem['name']) { + // Ensuring there are no values. + foreach ($elem['value']['{DAV:}prop'] as $remove => $value) { + $self->properties[$remove] = null; + } + } + } + + return $self; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/ShareResource.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/ShareResource.php new file mode 100644 index 0000000..79d7dc8 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/ShareResource.php @@ -0,0 +1,80 @@ +sharees = $sharees; + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $elems = $reader->parseInnerTree([ + '{DAV:}sharee' => 'Sabre\DAV\Xml\Element\Sharee', + '{DAV:}share-access' => 'Sabre\DAV\Xml\Property\ShareAccess', + '{DAV:}prop' => 'Sabre\Xml\Deserializer\keyValue', + ]); + + $sharees = []; + + foreach ($elems as $elem) { + if ('{DAV:}sharee' !== $elem['name']) { + continue; + } + $sharees[] = $elem['value']; + } + + return new self($sharees); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/SyncCollectionReport.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/SyncCollectionReport.php new file mode 100644 index 0000000..8dd9576 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Request/SyncCollectionReport.php @@ -0,0 +1,118 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $self = new self(); + + $reader->pushContext(); + + $reader->elementMap['{DAV:}prop'] = 'Sabre\Xml\Element\Elements'; + $elems = KeyValue::xmlDeserialize($reader); + + $reader->popContext(); + + $required = [ + '{DAV:}sync-token', + '{DAV:}prop', + ]; + + foreach ($required as $elem) { + if (!array_key_exists($elem, $elems)) { + throw new BadRequest('The '.$elem.' element in the {DAV:}sync-collection report is required'); + } + } + + $self->properties = $elems['{DAV:}prop']; + $self->syncToken = $elems['{DAV:}sync-token']; + + if (isset($elems['{DAV:}limit'])) { + $nresults = null; + foreach ($elems['{DAV:}limit'] as $child) { + if ('{DAV:}nresults' === $child['name']) { + $nresults = (int) $child['value']; + } + } + $self->limit = $nresults; + } + + if (isset($elems['{DAV:}sync-level'])) { + $value = $elems['{DAV:}sync-level']; + if ('infinity' === $value) { + $value = \Sabre\DAV\Server::DEPTH_INFINITY; + } + $self->syncLevel = $value; + } + + return $self; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Response/MultiStatus.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Response/MultiStatus.php new file mode 100644 index 0000000..e824cda --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Response/MultiStatus.php @@ -0,0 +1,136 @@ +responses = $responses; + $this->syncToken = $syncToken; + } + + /** + * Returns the response list. + * + * @return \Sabre\DAV\Xml\Element\Response[] + */ + public function getResponses() + { + return $this->responses; + } + + /** + * Returns the sync-token, if available. + * + * @return string|null + */ + public function getSyncToken() + { + return $this->syncToken; + } + + /** + * The serialize method is called during xml writing. + * + * It should use the $writer argument to encode this object into XML. + * + * Important note: it is not needed to create the parent element. The + * parent element is already created, and we only have to worry about + * attributes, child elements and text (if any). + * + * Important note 2: If you are writing any new elements, you are also + * responsible for closing them. + */ + public function xmlSerialize(Writer $writer) + { + foreach ($this->getResponses() as $response) { + $writer->writeElement('{DAV:}response', $response); + } + if ($syncToken = $this->getSyncToken()) { + $writer->writeElement('{DAV:}sync-token', $syncToken); + } + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $elementMap = $reader->elementMap; + $elementMap['{DAV:}prop'] = 'Sabre\\DAV\\Xml\\Element\\Prop'; + $elements = $reader->parseInnerTree($elementMap); + + $responses = []; + $syncToken = null; + + if ($elements) { + foreach ($elements as $elem) { + if ('{DAV:}response' === $elem['name']) { + $responses[] = $elem['value']; + } + if ('{DAV:}sync-token' === $elem['name']) { + $syncToken = $elem['value']; + } + } + } + + return new self($responses, $syncToken); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Service.php b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Service.php new file mode 100644 index 0000000..4406b02 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAV/Xml/Service.php @@ -0,0 +1,47 @@ + 'Sabre\\DAV\\Xml\\Response\\MultiStatus', + '{DAV:}response' => 'Sabre\\DAV\\Xml\\Element\\Response', + + // Requests + '{DAV:}propfind' => 'Sabre\\DAV\\Xml\\Request\\PropFind', + '{DAV:}propertyupdate' => 'Sabre\\DAV\\Xml\\Request\\PropPatch', + '{DAV:}mkcol' => 'Sabre\\DAV\\Xml\\Request\\MkCol', + + // Properties + '{DAV:}resourcetype' => 'Sabre\\DAV\\Xml\\Property\\ResourceType', + ]; + + /** + * This is a default list of namespaces. + * + * If you are defining your own custom namespace, add it here to reduce + * bandwidth and improve legibility of xml bodies. + * + * @var array + */ + public $namespaceMap = [ + 'DAV:' => 'd', + 'http://sabredav.org/ns' => 's', + ]; +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/ACLTrait.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/ACLTrait.php new file mode 100644 index 0000000..98c1ce3 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/ACLTrait.php @@ -0,0 +1,94 @@ + '{DAV:}all', + 'principal' => '{DAV:}owner', + 'protected' => true, + ], + ]; + } + + /** + * Updates the ACL. + * + * This method will receive a list of new ACE's as an array argument. + */ + public function setACL(array $acl) + { + throw new \Sabre\DAV\Exception\Forbidden('Setting ACL is not supported on this node'); + } + + /** + * Returns the list of supported privileges for this node. + * + * The returned data structure is a list of nested privileges. + * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple + * standard structure. + * + * If null is returned from this method, the default privilege set is used, + * which is fine for most common usecases. + * + * @return array|null + */ + public function getSupportedPrivilegeSet() + { + return null; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/AbstractPrincipalCollection.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/AbstractPrincipalCollection.php new file mode 100644 index 0000000..d26f7d2 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/AbstractPrincipalCollection.php @@ -0,0 +1,178 @@ +principalPrefix = $principalPrefix; + $this->principalBackend = $principalBackend; + } + + /** + * This method returns a node for a principal. + * + * The passed array contains principal information, and is guaranteed to + * at least contain a uri item. Other properties may or may not be + * supplied by the authentication backend. + * + * @return DAV\INode + */ + abstract public function getChildForPrincipal(array $principalInfo); + + /** + * Returns the name of this collection. + * + * @return string + */ + public function getName() + { + list(, $name) = Uri\split($this->principalPrefix); + + return $name; + } + + /** + * Return the list of users. + * + * @return array + */ + public function getChildren() + { + if ($this->disableListing) { + throw new DAV\Exception\MethodNotAllowed('Listing members of this collection is disabled'); + } + $children = []; + foreach ($this->principalBackend->getPrincipalsByPrefix($this->principalPrefix) as $principalInfo) { + $children[] = $this->getChildForPrincipal($principalInfo); + } + + return $children; + } + + /** + * Returns a child object, by its name. + * + * @param string $name + * + * @throws DAV\Exception\NotFound + * + * @return DAV\INode + */ + public function getChild($name) + { + $principalInfo = $this->principalBackend->getPrincipalByPath($this->principalPrefix.'/'.$name); + if (!$principalInfo) { + throw new DAV\Exception\NotFound('Principal with name '.$name.' not found'); + } + + return $this->getChildForPrincipal($principalInfo); + } + + /** + * This method is used to search for principals matching a set of + * properties. + * + * This search is specifically used by RFC3744's principal-property-search + * REPORT. You should at least allow searching on + * http://sabredav.org/ns}email-address. + * + * The actual search should be a unicode-non-case-sensitive search. The + * keys in searchProperties are the WebDAV property names, while the values + * are the property values to search on. + * + * By default, if multiple properties are submitted to this method, the + * various properties should be combined with 'AND'. If $test is set to + * 'anyof', it should be combined using 'OR'. + * + * This method should simply return a list of 'child names', which may be + * used to call $this->getChild in the future. + * + * @param string $test + * + * @return array + */ + public function searchPrincipals(array $searchProperties, $test = 'allof') + { + $result = $this->principalBackend->searchPrincipals($this->principalPrefix, $searchProperties, $test); + $r = []; + + foreach ($result as $row) { + list(, $r[]) = Uri\split($row); + } + + return $r; + } + + /** + * Finds a principal by its URI. + * + * This method may receive any type of uri, but mailto: addresses will be + * the most common. + * + * Implementation of this API is optional. It is currently used by the + * CalDAV system to find principals based on their email addresses. If this + * API is not implemented, some features may not work correctly. + * + * This method must return a relative principal path, or null, if the + * principal was not found or you refuse to find it. + * + * @param string $uri + * + * @return string + */ + public function findByUri($uri) + { + return $this->principalBackend->findByUri($uri, $this->principalPrefix); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/AceConflict.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/AceConflict.php new file mode 100644 index 0000000..0fc3f77 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/AceConflict.php @@ -0,0 +1,31 @@ +ownerDocument; + + $np = $doc->createElementNS('DAV:', 'd:no-ace-conflict'); + $errorNode->appendChild($np); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/NeedPrivileges.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/NeedPrivileges.php new file mode 100644 index 0000000..af1f01c --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/NeedPrivileges.php @@ -0,0 +1,73 @@ +uri = $uri; + $this->privileges = $privileges; + + parent::__construct('User did not have the required privileges ('.implode(',', $privileges).') for path "'.$uri.'"'); + } + + /** + * Adds in extra information in the xml response. + * + * This method adds the {DAV:}need-privileges element as defined in rfc3744 + */ + public function serialize(DAV\Server $server, \DOMElement $errorNode) + { + $doc = $errorNode->ownerDocument; + + $np = $doc->createElementNS('DAV:', 'd:need-privileges'); + $errorNode->appendChild($np); + + foreach ($this->privileges as $privilege) { + $resource = $doc->createElementNS('DAV:', 'd:resource'); + $np->appendChild($resource); + + $resource->appendChild($doc->createElementNS('DAV:', 'd:href', $server->getBaseUri().$this->uri)); + + $priv = $doc->createElementNS('DAV:', 'd:privilege'); + $resource->appendChild($priv); + + preg_match('/^{([^}]*)}(.*)$/', $privilege, $privilegeParts); + $priv->appendChild($doc->createElementNS($privilegeParts[1], 'd:'.$privilegeParts[2])); + } + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/NoAbstract.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/NoAbstract.php new file mode 100644 index 0000000..b9c6616 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/NoAbstract.php @@ -0,0 +1,31 @@ +ownerDocument; + + $np = $doc->createElementNS('DAV:', 'd:no-abstract'); + $errorNode->appendChild($np); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/NotRecognizedPrincipal.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/NotRecognizedPrincipal.php new file mode 100644 index 0000000..d4e7284 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/NotRecognizedPrincipal.php @@ -0,0 +1,31 @@ +ownerDocument; + + $np = $doc->createElementNS('DAV:', 'd:recognized-principal'); + $errorNode->appendChild($np); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/NotSupportedPrivilege.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/NotSupportedPrivilege.php new file mode 100644 index 0000000..c04c5fa --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/Exception/NotSupportedPrivilege.php @@ -0,0 +1,31 @@ +ownerDocument; + + $np = $doc->createElementNS('DAV:', 'd:not-supported-privilege'); + $errorNode->appendChild($np); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/FS/Collection.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/FS/Collection.php new file mode 100644 index 0000000..85b04e2 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/FS/Collection.php @@ -0,0 +1,109 @@ +acl = $acl; + $this->owner = $owner; + } + + /** + * Returns a specific child node, referenced by its name. + * + * This method must throw Sabre\DAV\Exception\NotFound if the node does not + * exist. + * + * @param string $name + * + * @throws NotFound + * + * @return \Sabre\DAV\INode + */ + public function getChild($name) + { + $path = $this->path.'/'.$name; + + if (!file_exists($path)) { + throw new NotFound('File could not be located'); + } + if ('.' == $name || '..' == $name) { + throw new Forbidden('Permission denied to . and ..'); + } + if (is_dir($path)) { + return new self($path, $this->acl, $this->owner); + } else { + return new File($path, $this->acl, $this->owner); + } + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->owner; + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + return $this->acl; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/FS/File.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/FS/File.php new file mode 100644 index 0000000..5506aa2 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/FS/File.php @@ -0,0 +1,78 @@ +acl = $acl; + $this->owner = $owner; + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->owner; + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + return $this->acl; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/FS/HomeCollection.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/FS/HomeCollection.php new file mode 100644 index 0000000..fa476e0 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/FS/HomeCollection.php @@ -0,0 +1,123 @@ +storagePath = $storagePath; + } + + /** + * Returns the name of the node. + * + * This is used to generate the url. + * + * @return string + */ + public function getName() + { + return $this->collectionName; + } + + /** + * Returns a principals' collection of files. + * + * The passed array contains principal information, and is guaranteed to + * at least contain a uri item. Other properties may or may not be + * supplied by the authentication backend. + * + * @return \Sabre\DAV\INode + */ + public function getChildForPrincipal(array $principalInfo) + { + $owner = $principalInfo['uri']; + $acl = [ + [ + 'privilege' => '{DAV:}all', + 'principal' => '{DAV:}owner', + 'protected' => true, + ], + ]; + + list(, $principalBaseName) = Uri\split($owner); + + $path = $this->storagePath.'/'.$principalBaseName; + + if (!is_dir($path)) { + mkdir($path, 0777, true); + } + + return new Collection( + $path, + $acl, + $owner + ); + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + return [ + [ + 'principal' => '{DAV:}authenticated', + 'privilege' => '{DAV:}read', + 'protected' => true, + ], + ]; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/IACL.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/IACL.php new file mode 100644 index 0000000..291fb24 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/IACL.php @@ -0,0 +1,72 @@ +getChild in the future. + * + * @param string $test + * + * @return array + */ + public function searchPrincipals(array $searchProperties, $test = 'allof'); + + /** + * Finds a principal by its URI. + * + * This method may receive any type of uri, but mailto: addresses will be + * the most common. + * + * Implementation of this API is optional. It is currently used by the + * CalDAV system to find principals based on their email addresses. If this + * API is not implemented, some features may not work correctly. + * + * This method must return a relative principal path, or null, if the + * principal was not found or you refuse to find it. + * + * @param string $uri + * + * @return string + */ + public function findByUri($uri); +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/Plugin.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Plugin.php new file mode 100644 index 0000000..f049784 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/Plugin.php @@ -0,0 +1,1545 @@ + 'Display name', + '{http://sabredav.org/ns}email-address' => 'Email address', + ]; + + /** + * Any principal uri's added here, will automatically be added to the list + * of ACL's. They will effectively receive {DAV:}all privileges, as a + * protected privilege. + * + * @var array + */ + public $adminPrincipals = []; + + /** + * The ACL plugin allows privileges to be assigned to users that are not + * logged in. To facilitate that, it modifies the auth plugin's behavior + * to only require login when a privileged operation was denied. + * + * Unauthenticated access can be considered a security concern, so it's + * possible to turn this feature off to harden the server's security. + * + * @var bool + */ + public $allowUnauthenticatedAccess = true; + + /** + * Returns a list of features added by this plugin. + * + * This list is used in the response of a HTTP OPTIONS request. + * + * @return array + */ + public function getFeatures() + { + return ['access-control', 'calendarserver-principal-property-search']; + } + + /** + * Returns a list of available methods for a given url. + * + * @param string $uri + * + * @return array + */ + public function getMethods($uri) + { + return ['ACL']; + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using Sabre\DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'acl'; + } + + /** + * Returns a list of reports this plugin supports. + * + * This will be used in the {DAV:}supported-report-set property. + * Note that you still need to subscribe to the 'report' event to actually + * implement them + * + * @param string $uri + * + * @return array + */ + public function getSupportedReportSet($uri) + { + return [ + '{DAV:}expand-property', + '{DAV:}principal-match', + '{DAV:}principal-property-search', + '{DAV:}principal-search-property-set', + ]; + } + + /** + * Checks if the current user has the specified privilege(s). + * + * You can specify a single privilege, or a list of privileges. + * This method will throw an exception if the privilege is not available + * and return true otherwise. + * + * @param string $uri + * @param array|string $privileges + * @param int $recursion + * @param bool $throwExceptions if set to false, this method won't throw exceptions + * + * @throws NeedPrivileges + * @throws NotAuthenticated + * + * @return bool + */ + public function checkPrivileges($uri, $privileges, $recursion = self::R_PARENT, $throwExceptions = true) + { + if (!is_array($privileges)) { + $privileges = [$privileges]; + } + + $acl = $this->getCurrentUserPrivilegeSet($uri); + + $failed = []; + foreach ($privileges as $priv) { + if (!in_array($priv, $acl)) { + $failed[] = $priv; + } + } + + if ($failed) { + if ($this->allowUnauthenticatedAccess && is_null($this->getCurrentUserPrincipal())) { + // We are not authenticated. Kicking in the Auth plugin. + $authPlugin = $this->server->getPlugin('auth'); + $reasons = $authPlugin->getLoginFailedReasons(); + $authPlugin->challenge( + $this->server->httpRequest, + $this->server->httpResponse + ); + throw new NotAuthenticated(implode(', ', $reasons).'. Login was needed for privilege: '.implode(', ', $failed).' on '.$uri); + } + if ($throwExceptions) { + throw new NeedPrivileges($uri, $failed); + } else { + return false; + } + } + + return true; + } + + /** + * Returns the standard users' principal. + * + * This is one authoritative principal url for the current user. + * This method will return null if the user wasn't logged in. + * + * @return string|null + */ + public function getCurrentUserPrincipal() + { + /** @var $authPlugin \Sabre\DAV\Auth\Plugin */ + $authPlugin = $this->server->getPlugin('auth'); + if (!$authPlugin) { + return null; + } + + return $authPlugin->getCurrentPrincipal(); + } + + /** + * Returns a list of principals that's associated to the current + * user, either directly or through group membership. + * + * @return array + */ + public function getCurrentUserPrincipals() + { + $currentUser = $this->getCurrentUserPrincipal(); + + if (is_null($currentUser)) { + return []; + } + + return array_merge( + [$currentUser], + $this->getPrincipalMembership($currentUser) + ); + } + + /** + * Sets the default ACL rules. + * + * These rules are used for all nodes that don't implement the IACL interface. + */ + public function setDefaultAcl(array $acl) + { + $this->defaultAcl = $acl; + } + + /** + * Returns the default ACL rules. + * + * These rules are used for all nodes that don't implement the IACL interface. + * + * @return array + */ + public function getDefaultAcl() + { + return $this->defaultAcl; + } + + /** + * The default ACL rules. + * + * These rules are used for nodes that don't implement IACL. These default + * set of rules allow anyone to do anything, as long as they are + * authenticated. + * + * @var array + */ + protected $defaultAcl = [ + [ + 'principal' => '{DAV:}authenticated', + 'protected' => true, + 'privilege' => '{DAV:}all', + ], + ]; + + /** + * This array holds a cache for all the principals that are associated with + * a single principal. + * + * @var array + */ + protected $principalMembershipCache = []; + + /** + * Returns all the principal groups the specified principal is a member of. + * + * @param string $mainPrincipal + * + * @return array + */ + public function getPrincipalMembership($mainPrincipal) + { + // First check our cache + if (isset($this->principalMembershipCache[$mainPrincipal])) { + return $this->principalMembershipCache[$mainPrincipal]; + } + + $check = [$mainPrincipal]; + $principals = []; + + while (count($check)) { + $principal = array_shift($check); + + $node = $this->server->tree->getNodeForPath($principal); + if ($node instanceof IPrincipal) { + foreach ($node->getGroupMembership() as $groupMember) { + if (!in_array($groupMember, $principals)) { + $check[] = $groupMember; + $principals[] = $groupMember; + } + } + } + } + + // Store the result in the cache + $this->principalMembershipCache[$mainPrincipal] = $principals; + + return $principals; + } + + /** + * Find out of a principal equals another principal. + * + * This is a quick way to find out whether a principal URI is part of a + * group, or any subgroups. + * + * The first argument is the principal URI you want to check against. For + * example the principal group, and the second argument is the principal of + * which you want to find out of it is the same as the first principal, or + * in a member of the first principal's group or subgroups. + * + * So the arguments are not interchangeable. If principal A is in group B, + * passing 'B', 'A' will yield true, but 'A', 'B' is false. + * + * If the second argument is not passed, we will use the current user + * principal. + * + * @param string $checkPrincipal + * @param string $currentPrincipal + * + * @return bool + */ + public function principalMatchesPrincipal($checkPrincipal, $currentPrincipal = null) + { + if (is_null($currentPrincipal)) { + $currentPrincipal = $this->getCurrentUserPrincipal(); + } + if ($currentPrincipal === $checkPrincipal) { + return true; + } + if (is_null($currentPrincipal)) { + return false; + } + + return in_array( + $checkPrincipal, + $this->getPrincipalMembership($currentPrincipal) + ); + } + + /** + * Returns a tree of supported privileges for a resource. + * + * The returned array structure should be in this form: + * + * [ + * [ + * 'privilege' => '{DAV:}read', + * 'abstract' => false, + * 'aggregates' => [] + * ] + * ] + * + * Privileges can be nested using "aggregates". Doing so means that + * if you assign someone the aggregating privilege, all the + * sub-privileges will automatically be granted. + * + * Marking a privilege as abstract means that the privilege cannot be + * directly assigned, but must be assigned via the parent privilege. + * + * So a more complex version might look like this: + * + * [ + * [ + * 'privilege' => '{DAV:}read', + * 'abstract' => false, + * 'aggregates' => [ + * [ + * 'privilege' => '{DAV:}read-acl', + * 'abstract' => false, + * 'aggregates' => [], + * ] + * ] + * ] + * ] + * + * @param string|INode $node + * + * @return array + */ + public function getSupportedPrivilegeSet($node) + { + if (is_string($node)) { + $node = $this->server->tree->getNodeForPath($node); + } + + $supportedPrivileges = null; + if ($node instanceof IACL) { + $supportedPrivileges = $node->getSupportedPrivilegeSet(); + } + + if (is_null($supportedPrivileges)) { + // Default + $supportedPrivileges = [ + '{DAV:}read' => [ + 'abstract' => false, + 'aggregates' => [ + '{DAV:}read-acl' => [ + 'abstract' => false, + 'aggregates' => [], + ], + '{DAV:}read-current-user-privilege-set' => [ + 'abstract' => false, + 'aggregates' => [], + ], + ], + ], + '{DAV:}write' => [ + 'abstract' => false, + 'aggregates' => [ + '{DAV:}write-properties' => [ + 'abstract' => false, + 'aggregates' => [], + ], + '{DAV:}write-content' => [ + 'abstract' => false, + 'aggregates' => [], + ], + '{DAV:}unlock' => [ + 'abstract' => false, + 'aggregates' => [], + ], + ], + ], + ]; + if ($node instanceof DAV\ICollection) { + $supportedPrivileges['{DAV:}write']['aggregates']['{DAV:}bind'] = [ + 'abstract' => false, + 'aggregates' => [], + ]; + $supportedPrivileges['{DAV:}write']['aggregates']['{DAV:}unbind'] = [ + 'abstract' => false, + 'aggregates' => [], + ]; + } + if ($node instanceof IACL) { + $supportedPrivileges['{DAV:}write']['aggregates']['{DAV:}write-acl'] = [ + 'abstract' => false, + 'aggregates' => [], + ]; + } + } + + $this->server->emit( + 'getSupportedPrivilegeSet', + [$node, &$supportedPrivileges] + ); + + return $supportedPrivileges; + } + + /** + * Returns the supported privilege set as a flat list. + * + * This is much easier to parse. + * + * The returned list will be index by privilege name. + * The value is a struct containing the following properties: + * - aggregates + * - abstract + * - concrete + * + * @param string|INode $node + * + * @return array + */ + final public function getFlatPrivilegeSet($node) + { + $privs = [ + 'abstract' => false, + 'aggregates' => $this->getSupportedPrivilegeSet($node), + ]; + + $fpsTraverse = null; + $fpsTraverse = function ($privName, $privInfo, $concrete, &$flat) use (&$fpsTraverse) { + $myPriv = [ + 'privilege' => $privName, + 'abstract' => isset($privInfo['abstract']) && $privInfo['abstract'], + 'aggregates' => [], + 'concrete' => isset($privInfo['abstract']) && $privInfo['abstract'] ? $concrete : $privName, + ]; + + if (isset($privInfo['aggregates'])) { + foreach ($privInfo['aggregates'] as $subPrivName => $subPrivInfo) { + $myPriv['aggregates'][] = $subPrivName; + } + } + + $flat[$privName] = $myPriv; + + if (isset($privInfo['aggregates'])) { + foreach ($privInfo['aggregates'] as $subPrivName => $subPrivInfo) { + $fpsTraverse($subPrivName, $subPrivInfo, $myPriv['concrete'], $flat); + } + } + }; + + $flat = []; + $fpsTraverse('{DAV:}all', $privs, null, $flat); + + return $flat; + } + + /** + * Returns the full ACL list. + * + * Either a uri or a INode may be passed. + * + * null will be returned if the node doesn't support ACLs. + * + * @param string|DAV\INode $node + * + * @return array + */ + public function getAcl($node) + { + if (is_string($node)) { + $node = $this->server->tree->getNodeForPath($node); + } + if (!$node instanceof IACL) { + return $this->getDefaultAcl(); + } + $acl = $node->getACL(); + foreach ($this->adminPrincipals as $adminPrincipal) { + $acl[] = [ + 'principal' => $adminPrincipal, + 'privilege' => '{DAV:}all', + 'protected' => true, + ]; + } + + return $acl; + } + + /** + * Returns a list of privileges the current user has + * on a particular node. + * + * Either a uri or a DAV\INode may be passed. + * + * null will be returned if the node doesn't support ACLs. + * + * @param string|DAV\INode $node + * + * @return array + */ + public function getCurrentUserPrivilegeSet($node) + { + if (is_string($node)) { + $node = $this->server->tree->getNodeForPath($node); + } + + $acl = $this->getACL($node); + + $collected = []; + + $isAuthenticated = null !== $this->getCurrentUserPrincipal(); + + foreach ($acl as $ace) { + $principal = $ace['principal']; + + switch ($principal) { + case '{DAV:}owner': + $owner = $node->getOwner(); + if ($owner && $this->principalMatchesPrincipal($owner)) { + $collected[] = $ace; + } + break; + + // 'all' matches for every user + case '{DAV:}all': + $collected[] = $ace; + break; + + case '{DAV:}authenticated': + // Authenticated users only + if ($isAuthenticated) { + $collected[] = $ace; + } + break; + + case '{DAV:}unauthenticated': + // Unauthenticated users only + if (!$isAuthenticated) { + $collected[] = $ace; + } + break; + + default: + if ($this->principalMatchesPrincipal($ace['principal'])) { + $collected[] = $ace; + } + break; + } + } + + // Now we deduct all aggregated privileges. + $flat = $this->getFlatPrivilegeSet($node); + + $collected2 = []; + while (count($collected)) { + $current = array_pop($collected); + $collected2[] = $current['privilege']; + + if (!isset($flat[$current['privilege']])) { + // Ignoring privileges that are not in the supported-privileges list. + $this->server->getLogger()->debug('A node has the "'.$current['privilege'].'" in its ACL list, but this privilege was not reported in the supportedPrivilegeSet list. This will be ignored.'); + continue; + } + foreach ($flat[$current['privilege']]['aggregates'] as $subPriv) { + $collected2[] = $subPriv; + $collected[] = $flat[$subPriv]; + } + } + + return array_values(array_unique($collected2)); + } + + /** + * Returns a principal based on its uri. + * + * Returns null if the principal could not be found. + * + * @param string $uri + * + * @return string|null + */ + public function getPrincipalByUri($uri) + { + $result = null; + $collections = $this->principalCollectionSet; + foreach ($collections as $collection) { + try { + $principalCollection = $this->server->tree->getNodeForPath($collection); + } catch (NotFound $e) { + // Ignore and move on + continue; + } + + if (!$principalCollection instanceof IPrincipalCollection) { + // Not a principal collection, we're simply going to ignore + // this. + continue; + } + + $result = $principalCollection->findByUri($uri); + if ($result) { + return $result; + } + } + } + + /** + * Principal property search. + * + * This method can search for principals matching certain values in + * properties. + * + * This method will return a list of properties for the matched properties. + * + * @param array $searchProperties The properties to search on. This is a + * key-value list. The keys are property + * names, and the values the strings to + * match them on. + * @param array $requestedProperties this is the list of properties to + * return for every match + * @param string $collectionUri the principal collection to search on. + * If this is omitted, the standard + * principal collection-set will be used + * @param string $test "allof" to use AND to search the + * properties. 'anyof' for OR. + * + * @return array This method returns an array structure similar to + * Sabre\DAV\Server::getPropertiesForPath. Returned + * properties are index by a HTTP status code. + */ + public function principalSearch(array $searchProperties, array $requestedProperties, $collectionUri = null, $test = 'allof') + { + if (!is_null($collectionUri)) { + $uris = [$collectionUri]; + } else { + $uris = $this->principalCollectionSet; + } + + $lookupResults = []; + foreach ($uris as $uri) { + $principalCollection = $this->server->tree->getNodeForPath($uri); + if (!$principalCollection instanceof IPrincipalCollection) { + // Not a principal collection, we're simply going to ignore + // this. + continue; + } + + $results = $principalCollection->searchPrincipals($searchProperties, $test); + foreach ($results as $result) { + $lookupResults[] = rtrim($uri, '/').'/'.$result; + } + } + + $matches = []; + + foreach ($lookupResults as $lookupResult) { + list($matches[]) = $this->server->getPropertiesForPath($lookupResult, $requestedProperties, 0); + } + + return $matches; + } + + /** + * Sets up the plugin. + * + * This method is automatically called by the server class. + */ + public function initialize(DAV\Server $server) + { + if ($this->allowUnauthenticatedAccess) { + $authPlugin = $server->getPlugin('auth'); + if (!$authPlugin) { + throw new \Exception('The Auth plugin must be loaded before the ACL plugin if you want to allow unauthenticated access.'); + } + $authPlugin->autoRequireLogin = false; + } + + $this->server = $server; + $server->on('propFind', [$this, 'propFind'], 20); + $server->on('beforeMethod:*', [$this, 'beforeMethod'], 20); + $server->on('beforeBind', [$this, 'beforeBind'], 20); + $server->on('beforeUnbind', [$this, 'beforeUnbind'], 20); + $server->on('propPatch', [$this, 'propPatch']); + $server->on('beforeUnlock', [$this, 'beforeUnlock'], 20); + $server->on('report', [$this, 'report']); + $server->on('method:ACL', [$this, 'httpAcl']); + $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']); + $server->on('getPrincipalByUri', function ($principal, &$uri) { + $uri = $this->getPrincipalByUri($principal); + + // Break event chain + if ($uri) { + return false; + } + }); + + array_push($server->protectedProperties, + '{DAV:}alternate-URI-set', + '{DAV:}principal-URL', + '{DAV:}group-membership', + '{DAV:}principal-collection-set', + '{DAV:}current-user-principal', + '{DAV:}supported-privilege-set', + '{DAV:}current-user-privilege-set', + '{DAV:}acl', + '{DAV:}acl-restrictions', + '{DAV:}inherited-acl-set', + '{DAV:}owner', + '{DAV:}group' + ); + + // Automatically mapping nodes implementing IPrincipal to the + // {DAV:}principal resourcetype. + $server->resourceTypeMapping['Sabre\\DAVACL\\IPrincipal'] = '{DAV:}principal'; + + // Mapping the group-member-set property to the HrefList property + // class. + $server->xml->elementMap['{DAV:}group-member-set'] = 'Sabre\\DAV\\Xml\\Property\\Href'; + $server->xml->elementMap['{DAV:}acl'] = 'Sabre\\DAVACL\\Xml\\Property\\Acl'; + $server->xml->elementMap['{DAV:}acl-principal-prop-set'] = 'Sabre\\DAVACL\\Xml\\Request\\AclPrincipalPropSetReport'; + $server->xml->elementMap['{DAV:}expand-property'] = 'Sabre\\DAVACL\\Xml\\Request\\ExpandPropertyReport'; + $server->xml->elementMap['{DAV:}principal-property-search'] = 'Sabre\\DAVACL\\Xml\\Request\\PrincipalPropertySearchReport'; + $server->xml->elementMap['{DAV:}principal-search-property-set'] = 'Sabre\\DAVACL\\Xml\\Request\\PrincipalSearchPropertySetReport'; + $server->xml->elementMap['{DAV:}principal-match'] = 'Sabre\\DAVACL\\Xml\\Request\\PrincipalMatchReport'; + } + + /* {{{ Event handlers */ + + /** + * Triggered before any method is handled. + */ + public function beforeMethod(RequestInterface $request, ResponseInterface $response) + { + $method = $request->getMethod(); + $path = $request->getPath(); + + $exists = $this->server->tree->nodeExists($path); + + // If the node doesn't exists, none of these checks apply + if (!$exists) { + return; + } + + switch ($method) { + case 'GET': + case 'HEAD': + case 'OPTIONS': + // For these 3 we only need to know if the node is readable. + $this->checkPrivileges($path, '{DAV:}read'); + break; + + case 'PUT': + case 'LOCK': + // This method requires the write-content priv if the node + // already exists, and bind on the parent if the node is being + // created. + // The bind privilege is handled in the beforeBind event. + $this->checkPrivileges($path, '{DAV:}write-content'); + break; + + case 'UNLOCK': + // Unlock is always allowed at the moment. + break; + + case 'PROPPATCH': + $this->checkPrivileges($path, '{DAV:}write-properties'); + break; + + case 'ACL': + $this->checkPrivileges($path, '{DAV:}write-acl'); + break; + + case 'COPY': + case 'MOVE': + // Copy requires read privileges on the entire source tree. + // If the target exists write-content normally needs to be + // checked, however, we're deleting the node beforehand and + // creating a new one after, so this is handled by the + // beforeUnbind event. + // + // The creation of the new node is handled by the beforeBind + // event. + // + // If MOVE is used beforeUnbind will also be used to check if + // the sourcenode can be deleted. + $this->checkPrivileges($path, '{DAV:}read', self::R_RECURSIVE); + break; + } + } + + /** + * Triggered before a new node is created. + * + * This allows us to check permissions for any operation that creates a + * new node, such as PUT, MKCOL, MKCALENDAR, LOCK, COPY and MOVE. + * + * @param string $uri + */ + public function beforeBind($uri) + { + list($parentUri) = Uri\split($uri); + $this->checkPrivileges($parentUri, '{DAV:}bind'); + } + + /** + * Triggered before a node is deleted. + * + * This allows us to check permissions for any operation that will delete + * an existing node. + * + * @param string $uri + */ + public function beforeUnbind($uri) + { + list($parentUri) = Uri\split($uri); + $this->checkPrivileges($parentUri, '{DAV:}unbind', self::R_RECURSIVEPARENTS); + } + + /** + * Triggered before a node is unlocked. + * + * @param string $uri + * @TODO: not yet implemented + */ + public function beforeUnlock($uri, DAV\Locks\LockInfo $lock) + { + } + + /** + * Triggered before properties are looked up in specific nodes. + * + * @TODO really should be broken into multiple methods, or even a class. + */ + public function propFind(DAV\PropFind $propFind, DAV\INode $node) + { + $path = $propFind->getPath(); + + // Checking the read permission + if (!$this->checkPrivileges($path, '{DAV:}read', self::R_PARENT, false)) { + // User is not allowed to read properties + + // Returning false causes the property-fetching system to pretend + // that the node does not exist, and will cause it to be hidden + // from listings such as PROPFIND or the browser plugin. + if ($this->hideNodesFromListings) { + return false; + } + + // Otherwise we simply mark every property as 403. + foreach ($propFind->getRequestedProperties() as $requestedProperty) { + $propFind->set($requestedProperty, null, 403); + } + + return; + } + + /* Adding principal properties */ + if ($node instanceof IPrincipal) { + $propFind->handle('{DAV:}alternate-URI-set', function () use ($node) { + return new Href($node->getAlternateUriSet()); + }); + $propFind->handle('{DAV:}principal-URL', function () use ($node) { + return new Href($node->getPrincipalUrl().'/'); + }); + $propFind->handle('{DAV:}group-member-set', function () use ($node) { + $members = $node->getGroupMemberSet(); + foreach ($members as $k => $member) { + $members[$k] = rtrim($member, '/').'/'; + } + + return new Href($members); + }); + $propFind->handle('{DAV:}group-membership', function () use ($node) { + $members = $node->getGroupMembership(); + foreach ($members as $k => $member) { + $members[$k] = rtrim($member, '/').'/'; + } + + return new Href($members); + }); + $propFind->handle('{DAV:}displayname', [$node, 'getDisplayName']); + } + + $propFind->handle('{DAV:}principal-collection-set', function () { + $val = $this->principalCollectionSet; + // Ensuring all collections end with a slash + foreach ($val as $k => $v) { + $val[$k] = $v.'/'; + } + + return new Href($val); + }); + $propFind->handle('{DAV:}current-user-principal', function () { + if ($url = $this->getCurrentUserPrincipal()) { + return new Xml\Property\Principal(Xml\Property\Principal::HREF, $url.'/'); + } else { + return new Xml\Property\Principal(Xml\Property\Principal::UNAUTHENTICATED); + } + }); + $propFind->handle('{DAV:}supported-privilege-set', function () use ($node) { + return new Xml\Property\SupportedPrivilegeSet($this->getSupportedPrivilegeSet($node)); + }); + $propFind->handle('{DAV:}current-user-privilege-set', function () use ($node, $propFind, $path) { + if (!$this->checkPrivileges($path, '{DAV:}read-current-user-privilege-set', self::R_PARENT, false)) { + $propFind->set('{DAV:}current-user-privilege-set', null, 403); + } else { + $val = $this->getCurrentUserPrivilegeSet($node); + + return new Xml\Property\CurrentUserPrivilegeSet($val); + } + }); + $propFind->handle('{DAV:}acl', function () use ($node, $propFind, $path) { + /* The ACL property contains all the permissions */ + if (!$this->checkPrivileges($path, '{DAV:}read-acl', self::R_PARENT, false)) { + $propFind->set('{DAV:}acl', null, 403); + } else { + $acl = $this->getACL($node); + + return new Xml\Property\Acl($this->getACL($node)); + } + }); + $propFind->handle('{DAV:}acl-restrictions', function () { + return new Xml\Property\AclRestrictions(); + }); + + /* Adding ACL properties */ + if ($node instanceof IACL) { + $propFind->handle('{DAV:}owner', function () use ($node) { + return new Href($node->getOwner().'/'); + }); + } + } + + /** + * This method intercepts PROPPATCH methods and make sure the + * group-member-set is updated correctly. + * + * @param string $path + */ + public function propPatch($path, DAV\PropPatch $propPatch) + { + $propPatch->handle('{DAV:}group-member-set', function ($value) use ($path) { + if (is_null($value)) { + $memberSet = []; + } elseif ($value instanceof Href) { + $memberSet = array_map( + [$this->server, 'calculateUri'], + $value->getHrefs() + ); + } else { + throw new DAV\Exception('The group-member-set property MUST be an instance of Sabre\DAV\Property\HrefList or null'); + } + $node = $this->server->tree->getNodeForPath($path); + if (!($node instanceof IPrincipal)) { + // Fail + return false; + } + + $node->setGroupMemberSet($memberSet); + // We must also clear our cache, just in case + + $this->principalMembershipCache = []; + + return true; + }); + } + + /** + * This method handles HTTP REPORT requests. + * + * @param string $reportName + * @param mixed $report + * @param mixed $path + */ + public function report($reportName, $report, $path) + { + switch ($reportName) { + case '{DAV:}principal-property-search': + $this->server->transactionType = 'report-principal-property-search'; + $this->principalPropertySearchReport($path, $report); + + return false; + case '{DAV:}principal-search-property-set': + $this->server->transactionType = 'report-principal-search-property-set'; + $this->principalSearchPropertySetReport($path, $report); + + return false; + case '{DAV:}expand-property': + $this->server->transactionType = 'report-expand-property'; + $this->expandPropertyReport($path, $report); + + return false; + case '{DAV:}principal-match': + $this->server->transactionType = 'report-principal-match'; + $this->principalMatchReport($path, $report); + + return false; + case '{DAV:}acl-principal-prop-set': + $this->server->transactionType = 'acl-principal-prop-set'; + $this->aclPrincipalPropSetReport($path, $report); + + return false; + } + } + + /** + * This method is responsible for handling the 'ACL' event. + * + * @return bool + */ + public function httpAcl(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + $body = $request->getBodyAsString(); + + if (!$body) { + throw new DAV\Exception\BadRequest('XML body expected in ACL request'); + } + + $acl = $this->server->xml->expect('{DAV:}acl', $body); + $newAcl = $acl->getPrivileges(); + + // Normalizing urls + foreach ($newAcl as $k => $newAce) { + $newAcl[$k]['principal'] = $this->server->calculateUri($newAce['principal']); + } + $node = $this->server->tree->getNodeForPath($path); + + if (!$node instanceof IACL) { + throw new DAV\Exception\MethodNotAllowed('This node does not support the ACL method'); + } + + $oldAcl = $this->getACL($node); + + $supportedPrivileges = $this->getFlatPrivilegeSet($node); + + /* Checking if protected principals from the existing principal set are + not overwritten. */ + foreach ($oldAcl as $oldAce) { + if (!isset($oldAce['protected']) || !$oldAce['protected']) { + continue; + } + + $found = false; + foreach ($newAcl as $newAce) { + if ( + $newAce['privilege'] === $oldAce['privilege'] && + $newAce['principal'] === $oldAce['principal'] && + $newAce['protected'] + ) { + $found = true; + } + } + + if (!$found) { + throw new Exception\AceConflict('This resource contained a protected {DAV:}ace, but this privilege did not occur in the ACL request'); + } + } + + foreach ($newAcl as $newAce) { + // Do we recognize the privilege + if (!isset($supportedPrivileges[$newAce['privilege']])) { + throw new Exception\NotSupportedPrivilege('The privilege you specified ('.$newAce['privilege'].') is not recognized by this server'); + } + + if ($supportedPrivileges[$newAce['privilege']]['abstract']) { + throw new Exception\NoAbstract('The privilege you specified ('.$newAce['privilege'].') is an abstract privilege'); + } + + // Looking up the principal + try { + $principal = $this->server->tree->getNodeForPath($newAce['principal']); + } catch (NotFound $e) { + throw new Exception\NotRecognizedPrincipal('The specified principal ('.$newAce['principal'].') does not exist'); + } + if (!($principal instanceof IPrincipal)) { + throw new Exception\NotRecognizedPrincipal('The specified uri ('.$newAce['principal'].') is not a principal'); + } + } + $node->setACL($newAcl); + + $response->setStatus(200); + + // Breaking the event chain, because we handled this method. + return false; + } + + /* }}} */ + + /* Reports {{{ */ + + /** + * The principal-match report is defined in RFC3744, section 9.3. + * + * This report allows a client to figure out based on the current user, + * or a principal URL, the principal URL and principal URLs of groups that + * principal belongs to. + * + * @param string $path + */ + protected function principalMatchReport($path, Xml\Request\PrincipalMatchReport $report) + { + $depth = $this->server->getHTTPDepth(0); + if (0 !== $depth) { + throw new BadRequest('The principal-match report is only defined on Depth: 0'); + } + + $currentPrincipals = $this->getCurrentUserPrincipals(); + + $result = []; + + if (Xml\Request\PrincipalMatchReport::SELF === $report->type) { + // Finding all principals under the request uri that match the + // current principal. + foreach ($currentPrincipals as $currentPrincipal) { + if ($currentPrincipal === $path || 0 === strpos($currentPrincipal, $path.'/')) { + $result[] = $currentPrincipal; + } + } + } else { + // We need to find all resources that have a property that matches + // one of the current principals. + $candidates = $this->server->getPropertiesForPath( + $path, + [$report->principalProperty], + 1 + ); + + foreach ($candidates as $candidate) { + if (!isset($candidate[200][$report->principalProperty])) { + continue; + } + + $hrefs = $candidate[200][$report->principalProperty]; + + if (!$hrefs instanceof Href) { + continue; + } + + foreach ($hrefs->getHrefs() as $href) { + if (in_array(trim($href, '/'), $currentPrincipals)) { + $result[] = $candidate['href']; + continue 2; + } + } + } + } + + $responses = []; + + foreach ($result as $item) { + $properties = []; + + if ($report->properties) { + $foo = $this->server->getPropertiesForPath($item, $report->properties); + $foo = $foo[0]; + $item = $foo['href']; + unset($foo['href']); + $properties = $foo; + } + + $responses[] = new DAV\Xml\Element\Response( + $item, + $properties, + '200' + ); + } + + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setBody( + $this->server->xml->write( + '{DAV:}multistatus', + $responses, + $this->server->getBaseUri() + ) + ); + } + + /** + * The expand-property report is defined in RFC3253 section 3.8. + * + * This report is very similar to a standard PROPFIND. The difference is + * that it has the additional ability to look at properties containing a + * {DAV:}href element, follow that property and grab additional elements + * there. + * + * Other rfc's, such as ACL rely on this report, so it made sense to put + * it in this plugin. + * + * @param string $path + * @param Xml\Request\ExpandPropertyReport $report + */ + protected function expandPropertyReport($path, $report) + { + $depth = $this->server->getHTTPDepth(0); + + $result = $this->expandProperties($path, $report->properties, $depth); + + $xml = $this->server->xml->write( + '{DAV:}multistatus', + new DAV\Xml\Response\MultiStatus($result), + $this->server->getBaseUri() + ); + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setBody($xml); + } + + /** + * This method expands all the properties and returns + * a list with property values. + * + * @param array $path + * @param array $requestedProperties the list of required properties + * @param int $depth + * + * @return array + */ + protected function expandProperties($path, array $requestedProperties, $depth) + { + $foundProperties = $this->server->getPropertiesForPath($path, array_keys($requestedProperties), $depth); + + $result = []; + + foreach ($foundProperties as $node) { + foreach ($requestedProperties as $propertyName => $childRequestedProperties) { + // We're only traversing if sub-properties were requested + if (!is_array($childRequestedProperties) || 0 === count($childRequestedProperties)) { + continue; + } + + // We only have to do the expansion if the property was found + // and it contains an href element. + if (!array_key_exists($propertyName, $node[200])) { + continue; + } + + if (!$node[200][$propertyName] instanceof DAV\Xml\Property\Href) { + continue; + } + + $childHrefs = $node[200][$propertyName]->getHrefs(); + $childProps = []; + + foreach ($childHrefs as $href) { + // Gathering the result of the children + $childProps[] = [ + 'name' => '{DAV:}response', + 'value' => $this->expandProperties($href, $childRequestedProperties, 0)[0], + ]; + } + + // Replacing the property with its expanded form. + $node[200][$propertyName] = $childProps; + } + $result[] = new DAV\Xml\Element\Response($node['href'], $node); + } + + return $result; + } + + /** + * principalSearchPropertySetReport. + * + * This method responsible for handing the + * {DAV:}principal-search-property-set report. This report returns a list + * of properties the client may search on, using the + * {DAV:}principal-property-search report. + * + * @param string $path + * @param Xml\Request\PrincipalSearchPropertySetReport $report + */ + protected function principalSearchPropertySetReport($path, $report) + { + $httpDepth = $this->server->getHTTPDepth(0); + if (0 !== $httpDepth) { + throw new DAV\Exception\BadRequest('This report is only defined when Depth: 0'); + } + + $writer = $this->server->xml->getWriter(); + $writer->openMemory(); + $writer->startDocument(); + + $writer->startElement('{DAV:}principal-search-property-set'); + + foreach ($this->principalSearchPropertySet as $propertyName => $description) { + $writer->startElement('{DAV:}principal-search-property'); + $writer->startElement('{DAV:}prop'); + + $writer->writeElement($propertyName); + + $writer->endElement(); // prop + + if ($description) { + $writer->write([[ + 'name' => '{DAV:}description', + 'value' => $description, + 'attributes' => ['xml:lang' => 'en'], + ]]); + } + + $writer->endElement(); // principal-search-property + } + + $writer->endElement(); // principal-search-property-set + + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setStatus(200); + $this->server->httpResponse->setBody($writer->outputMemory()); + } + + /** + * principalPropertySearchReport. + * + * This method is responsible for handing the + * {DAV:}principal-property-search report. This report can be used for + * clients to search for groups of principals, based on the value of one + * or more properties. + * + * @param string $path + */ + protected function principalPropertySearchReport($path, Xml\Request\PrincipalPropertySearchReport $report) + { + if ($report->applyToPrincipalCollectionSet) { + $path = null; + } + if (0 !== $this->server->getHttpDepth('0')) { + throw new BadRequest('Depth must be 0'); + } + $result = $this->principalSearch( + $report->searchProperties, + $report->properties, + $path, + $report->test + ); + + $prefer = $this->server->getHTTPPrefer(); + + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer'); + $this->server->httpResponse->setBody($this->server->generateMultiStatus($result, 'minimal' === $prefer['return'])); + } + + /** + * aclPrincipalPropSet REPORT. + * + * This method is responsible for handling the {DAV:}acl-principal-prop-set + * REPORT, as defined in: + * + * https://tools.ietf.org/html/rfc3744#section-9.2 + * + * This REPORT allows a user to quickly fetch information about all + * principals specified in the access control list. Most commonly this + * is used to for example generate a UI with ACL rules, allowing you + * to show names for principals for every entry. + * + * @param string $path + */ + protected function aclPrincipalPropSetReport($path, Xml\Request\AclPrincipalPropSetReport $report) + { + if (0 !== $this->server->getHTTPDepth(0)) { + throw new BadRequest('The {DAV:}acl-principal-prop-set REPORT only supports Depth 0'); + } + + // Fetching ACL rules for the given path. We're using the property + // API and not the local getACL, because it will ensure that all + // business rules and restrictions are applied. + $acl = $this->server->getProperties($path, '{DAV:}acl'); + + if (!$acl || !isset($acl['{DAV:}acl'])) { + throw new Forbidden('Could not fetch ACL rules for this path'); + } + + $principals = []; + foreach ($acl['{DAV:}acl']->getPrivileges() as $ace) { + if ('{' === $ace['principal'][0]) { + // It's not a principal, it's one of the special rules such as {DAV:}authenticated + continue; + } + + $principals[] = $ace['principal']; + } + + $properties = $this->server->getPropertiesForMultiplePaths( + $principals, + $report->properties + ); + + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setBody( + $this->server->generateMultiStatus($properties) + ); + } + + /* }}} */ + + /** + * This method is used to generate HTML output for the + * DAV\Browser\Plugin. This allows us to generate an interface users + * can use to create new calendars. + * + * @param string $output + * + * @return bool + */ + public function htmlActionsPanel(DAV\INode $node, &$output) + { + if (!$node instanceof PrincipalCollection) { + return; + } + + $output .= '
+

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/composer/vendor/sabre/dav/lib/DAVACL/Principal.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Principal.php new file mode 100644 index 0000000..ada38ab --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/Principal.php @@ -0,0 +1,199 @@ +principalBackend = $principalBackend; + $this->principalProperties = $principalProperties; + } + + /** + * Returns the full principal url. + * + * @return string + */ + public function getPrincipalUrl() + { + return $this->principalProperties['uri']; + } + + /** + * Returns a list of alternative urls for a principal. + * + * This can for example be an email address, or ldap url. + * + * @return array + */ + public function getAlternateUriSet() + { + $uris = []; + if (isset($this->principalProperties['{DAV:}alternate-URI-set'])) { + $uris = $this->principalProperties['{DAV:}alternate-URI-set']; + } + + if (isset($this->principalProperties['{http://sabredav.org/ns}email-address'])) { + $uris[] = 'mailto:'.$this->principalProperties['{http://sabredav.org/ns}email-address']; + } + + return array_unique($uris); + } + + /** + * Returns the list of group members. + * + * If this principal is a group, this function should return + * all member principal uri's for the group. + * + * @return array + */ + public function getGroupMemberSet() + { + return $this->principalBackend->getGroupMemberSet($this->principalProperties['uri']); + } + + /** + * Returns the list of groups this principal is member of. + * + * If this principal is a member of a (list of) groups, this function + * should return a list of principal uri's for it's members. + * + * @return array + */ + public function getGroupMembership() + { + return $this->principalBackend->getGroupMemberShip($this->principalProperties['uri']); + } + + /** + * Sets a list of group members. + * + * If this principal is a group, this method sets all the group members. + * The list of members is always overwritten, never appended to. + * + * This method should throw an exception if the members could not be set. + */ + public function setGroupMemberSet(array $groupMembers) + { + $this->principalBackend->setGroupMemberSet($this->principalProperties['uri'], $groupMembers); + } + + /** + * Returns this principals name. + * + * @return string + */ + public function getName() + { + $uri = $this->principalProperties['uri']; + list(, $name) = Uri\split($uri); + + return $name; + } + + /** + * Returns the name of the user. + * + * @return string + */ + public function getDisplayName() + { + if (isset($this->principalProperties['{DAV:}displayname'])) { + return $this->principalProperties['{DAV:}displayname']; + } else { + return $this->getName(); + } + } + + /** + * Returns a list of properties. + * + * @param array $requestedProperties + * + * @return array + */ + public function getProperties($requestedProperties) + { + $newProperties = []; + foreach ($requestedProperties as $propName) { + if (isset($this->principalProperties[$propName])) { + $newProperties[$propName] = $this->principalProperties[$propName]; + } + } + + return $newProperties; + } + + /** + * Updates properties on this node. + * + * This method received a PropPatch object, which contains all the + * information about the update. + * + * To update specific properties, call the 'handle' method on this object. + * Read the PropPatch documentation for more information. + */ + public function propPatch(DAV\PropPatch $propPatch) + { + return $this->principalBackend->updatePrincipal( + $this->principalProperties['uri'], + $propPatch + ); + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->principalProperties['uri']; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/AbstractBackend.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/AbstractBackend.php new file mode 100644 index 0000000..f61a0c9 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/AbstractBackend.php @@ -0,0 +1,54 @@ +searchPrincipals( + $principalPrefix, + ['{http://sabredav.org/ns}email-address' => substr($uri, 7)] + ); + + if ($result) { + return $result[0]; + } + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/BackendInterface.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/BackendInterface.php new file mode 100644 index 0000000..2fd3191 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/BackendInterface.php @@ -0,0 +1,143 @@ + [ + 'dbField' => 'displayname', + ], + + /* + * This is the users' primary email-address. + */ + '{http://sabredav.org/ns}email-address' => [ + 'dbField' => 'email', + ], + ]; + + /** + * Sets up the backend. + */ + public function __construct(\PDO $pdo) + { + $this->pdo = $pdo; + } + + /** + * Returns a list of principals based on a prefix. + * + * This prefix will often contain something like 'principals'. You are only + * expected to return principals that are in this base path. + * + * You are expected to return at least a 'uri' for every user, you can + * return any additional properties if you wish so. Common properties are: + * {DAV:}displayname + * {http://sabredav.org/ns}email-address - This is a custom SabreDAV + * field that's actually injected in a number of other properties. If + * you have an email address, use this property. + * + * @param string $prefixPath + * + * @return array + */ + public function getPrincipalsByPrefix($prefixPath) + { + $fields = [ + 'uri', + ]; + + foreach ($this->fieldMap as $key => $value) { + $fields[] = $value['dbField']; + } + $result = $this->pdo->query('SELECT '.implode(',', $fields).' FROM '.$this->tableName); + + $principals = []; + + while ($row = $result->fetch(\PDO::FETCH_ASSOC)) { + // Checking if the principal is in the prefix + list($rowPrefix) = Uri\split($row['uri']); + if ($rowPrefix !== $prefixPath) { + continue; + } + + $principal = [ + 'uri' => $row['uri'], + ]; + foreach ($this->fieldMap as $key => $value) { + if ($row[$value['dbField']]) { + $principal[$key] = $row[$value['dbField']]; + } + } + $principals[] = $principal; + } + + return $principals; + } + + /** + * Returns a specific principal, specified by it's path. + * The returned structure should be the exact same as from + * getPrincipalsByPrefix. + * + * @param string $path + * + * @return array + */ + public function getPrincipalByPath($path) + { + $fields = [ + 'id', + 'uri', + ]; + + foreach ($this->fieldMap as $key => $value) { + $fields[] = $value['dbField']; + } + $stmt = $this->pdo->prepare('SELECT '.implode(',', $fields).' FROM '.$this->tableName.' WHERE uri = ?'); + $stmt->execute([$path]); + + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + if (!$row) { + return; + } + + $principal = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + ]; + foreach ($this->fieldMap as $key => $value) { + if ($row[$value['dbField']]) { + $principal[$key] = $row[$value['dbField']]; + } + } + + return $principal; + } + + /** + * Updates one ore more webdav properties on a principal. + * + * The list of mutations is stored in a Sabre\DAV\PropPatch object. + * To do the actual updates, you must tell this object which properties + * you're going to process with the handle() method. + * + * Calling the handle method is like telling the PropPatch object "I + * promise I can handle updating this property". + * + * Read the PropPatch documentation for more info and examples. + * + * @param string $path + */ + public function updatePrincipal($path, DAV\PropPatch $propPatch) + { + $propPatch->handle(array_keys($this->fieldMap), function ($properties) use ($path) { + $query = 'UPDATE '.$this->tableName.' SET '; + $first = true; + + $values = []; + + foreach ($properties as $key => $value) { + $dbField = $this->fieldMap[$key]['dbField']; + + if (!$first) { + $query .= ', '; + } + $first = false; + $query .= $dbField.' = :'.$dbField; + $values[$dbField] = $value; + } + + $query .= ' WHERE uri = :uri'; + $values['uri'] = $path; + + $stmt = $this->pdo->prepare($query); + $stmt->execute($values); + + return true; + }); + } + + /** + * This method is used to search for principals matching a set of + * properties. + * + * This search is specifically used by RFC3744's principal-property-search + * REPORT. + * + * The actual search should be a unicode-non-case-sensitive search. The + * keys in searchProperties are the WebDAV property names, while the values + * are the property values to search on. + * + * By default, if multiple properties are submitted to this method, the + * various properties should be combined with 'AND'. If $test is set to + * 'anyof', it should be combined using 'OR'. + * + * This method should simply return an array with full principal uri's. + * + * If somebody attempted to search on a property the backend does not + * support, you should simply return 0 results. + * + * You can also just return 0 results if you choose to not support + * searching at all, but keep in mind that this may stop certain features + * from working. + * + * @param string $prefixPath + * @param string $test + * + * @return array + */ + public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') + { + if (0 == count($searchProperties)) { + return []; + } //No criteria + + $query = 'SELECT uri FROM '.$this->tableName.' WHERE '; + $values = []; + foreach ($searchProperties as $property => $value) { + switch ($property) { + case '{DAV:}displayname': + $column = 'displayname'; + break; + case '{http://sabredav.org/ns}email-address': + $column = 'email'; + break; + default: + // Unsupported property + return []; + } + if (count($values) > 0) { + $query .= (0 == strcmp($test, 'anyof') ? ' OR ' : ' AND '); + } + $query .= 'lower('.$column.') LIKE lower(?)'; + $values[] = '%'.$value.'%'; + } + $stmt = $this->pdo->prepare($query); + $stmt->execute($values); + + $principals = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + // Checking if the principal is in the prefix + list($rowPrefix) = Uri\split($row['uri']); + if ($rowPrefix !== $prefixPath) { + continue; + } + + $principals[] = $row['uri']; + } + + return $principals; + } + + /** + * Finds a principal by its URI. + * + * This method may receive any type of uri, but mailto: addresses will be + * the most common. + * + * Implementation of this API is optional. It is currently used by the + * CalDAV system to find principals based on their email addresses. If this + * API is not implemented, some features may not work correctly. + * + * This method must return a relative principal path, or null, if the + * principal was not found or you refuse to find it. + * + * @param string $uri + * @param string $principalPrefix + * + * @return string + */ + public function findByUri($uri, $principalPrefix) + { + $uriParts = Uri\parse($uri); + + // Only two types of uri are supported : + // - the "mailto:" scheme with some non-empty address + // - a principals uri, in the form "principals/NAME" + // In both cases, `path` must not be empty. + if (empty($uriParts['path'])) { + return null; + } + + $uri = null; + if ('mailto' === $uriParts['scheme']) { + $query = 'SELECT uri FROM '.$this->tableName.' WHERE lower(email)=lower(?)'; + $stmt = $this->pdo->prepare($query); + $stmt->execute([$uriParts['path']]); + + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + // Checking if the principal is in the prefix + list($rowPrefix) = Uri\split($row['uri']); + if ($rowPrefix !== $principalPrefix) { + continue; + } + + $uri = $row['uri']; + break; //Stop on first match + } + } else { + $pathParts = Uri\split($uriParts['path']); // We can do this since $uriParts['path'] is not null + + if (2 === count($pathParts) && $pathParts[0] === $principalPrefix) { + // Checking that this uri exists + $query = 'SELECT * FROM '.$this->tableName.' WHERE uri = ?'; + $stmt = $this->pdo->prepare($query); + $stmt->execute([$uriParts['path']]); + $rows = $stmt->fetchAll(); + + if (count($rows) > 0) { + $uri = $uriParts['path']; + } + } + } + + return $uri; + } + + /** + * Returns the list of members for a group-principal. + * + * @param string $principal + * + * @return array + */ + public function getGroupMemberSet($principal) + { + $principal = $this->getPrincipalByPath($principal); + if (!$principal) { + throw new DAV\Exception('Principal not found'); + } + $stmt = $this->pdo->prepare('SELECT principals.uri as uri FROM '.$this->groupMembersTableName.' AS groupmembers LEFT JOIN '.$this->tableName.' AS principals ON groupmembers.member_id = principals.id WHERE groupmembers.principal_id = ?'); + $stmt->execute([$principal['id']]); + + $result = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $result[] = $row['uri']; + } + + return $result; + } + + /** + * Returns the list of groups a principal is a member of. + * + * @param string $principal + * + * @return array + */ + public function getGroupMembership($principal) + { + $principal = $this->getPrincipalByPath($principal); + if (!$principal) { + throw new DAV\Exception('Principal not found'); + } + $stmt = $this->pdo->prepare('SELECT principals.uri as uri FROM '.$this->groupMembersTableName.' AS groupmembers LEFT JOIN '.$this->tableName.' AS principals ON groupmembers.principal_id = principals.id WHERE groupmembers.member_id = ?'); + $stmt->execute([$principal['id']]); + + $result = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $result[] = $row['uri']; + } + + return $result; + } + + /** + * Updates the list of group members for a group principal. + * + * The principals should be passed as a list of uri's. + * + * @param string $principal + */ + public function setGroupMemberSet($principal, array $members) + { + // Grabbing the list of principal id's. + $stmt = $this->pdo->prepare('SELECT id, uri FROM '.$this->tableName.' WHERE uri IN (? '.str_repeat(', ? ', count($members)).');'); + $stmt->execute(array_merge([$principal], $members)); + + $memberIds = []; + $principalId = null; + + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + if ($row['uri'] == $principal) { + $principalId = $row['id']; + } else { + $memberIds[] = $row['id']; + } + } + if (!$principalId) { + throw new DAV\Exception('Principal not found'); + } + // Wiping out old members + $stmt = $this->pdo->prepare('DELETE FROM '.$this->groupMembersTableName.' WHERE principal_id = ?;'); + $stmt->execute([$principalId]); + + foreach ($memberIds as $memberId) { + $stmt = $this->pdo->prepare('INSERT INTO '.$this->groupMembersTableName.' (principal_id, member_id) VALUES (?, ?);'); + $stmt->execute([$principalId, $memberId]); + } + } + + /** + * Creates a new principal. + * + * This method receives a full path for the new principal. The mkCol object + * contains any additional webdav properties specified during the creation + * of the principal. + * + * @param string $path + */ + public function createPrincipal($path, MkCol $mkCol) + { + $stmt = $this->pdo->prepare('INSERT INTO '.$this->tableName.' (uri) VALUES (?)'); + $stmt->execute([$path]); + $this->updatePrincipal($path, $mkCol); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/PrincipalCollection.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/PrincipalCollection.php new file mode 100644 index 0000000..b823b6c --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/PrincipalCollection.php @@ -0,0 +1,96 @@ +principalBackend, $principal); + } + + /** + * Creates a new collection. + * + * This method will receive a MkCol object with all the information about + * the new collection that's being created. + * + * The MkCol object contains information about the resourceType of the new + * collection. If you don't support the specified resourceType, you should + * throw Exception\InvalidResourceType. + * + * The object also contains a list of WebDAV properties for the new + * collection. + * + * You should call the handle() method on this object to specify exactly + * which properties you are storing. This allows the system to figure out + * exactly which properties you didn't store, which in turn allows other + * plugins (such as the propertystorage plugin) to handle storing the + * property for you. + * + * @param string $name + * + * @throws InvalidResourceType + */ + public function createExtendedCollection($name, MkCol $mkCol) + { + if (!$mkCol->hasResourceType('{DAV:}principal')) { + throw new InvalidResourceType('Only resources of type {DAV:}principal may be created here'); + } + + $this->principalBackend->createPrincipal( + $this->principalPrefix.'/'.$name, + $mkCol + ); + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + return [ + [ + 'principal' => '{DAV:}authenticated', + 'privilege' => '{DAV:}read', + 'protected' => true, + ], + ]; + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/Acl.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/Acl.php new file mode 100644 index 0000000..c6e236d --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/Acl.php @@ -0,0 +1,257 @@ +privileges = $privileges; + $this->prefixBaseUrl = $prefixBaseUrl; + } + + /** + * Returns the list of privileges for this property. + * + * @return array + */ + public function getPrivileges() + { + return $this->privileges; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + foreach ($this->privileges as $ace) { + $this->serializeAce($writer, $ace); + } + } + + /** + * Generate html representation for this value. + * + * The html output is 100% trusted, and no effort is being made to sanitize + * it. It's up to the implementor to sanitize user provided values. + * + * The output must be in UTF-8. + * + * The baseUri parameter is a url to the root of the application, and can + * be used to construct local links. + * + * @return string + */ + public function toHtml(HtmlOutputHelper $html) + { + ob_start(); + echo ''; + echo ''; + 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/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/AclRestrictions.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/AclRestrictions.php new file mode 100644 index 0000000..b5629c8 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/AclRestrictions.php @@ -0,0 +1,42 @@ +writeElement('{DAV:}grant-only'); + $writer->writeElement('{DAV:}no-invert'); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/CurrentUserPrivilegeSet.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/CurrentUserPrivilegeSet.php new file mode 100644 index 0000000..e38a45c --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/CurrentUserPrivilegeSet.php @@ -0,0 +1,145 @@ +privileges = $privileges; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + foreach ($this->privileges as $privName) { + $writer->startElement('{DAV:}privilege'); + $writer->writeElement($privName); + $writer->endElement(); + } + } + + /** + * Returns true or false, whether the specified principal appears in the + * list. + * + * @param string $privilegeName + * + * @return bool + */ + public function has($privilegeName) + { + return in_array($privilegeName, $this->privileges); + } + + /** + * Returns the list of privileges. + * + * @return array + */ + public function getValue() + { + return $this->privileges; + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $result = []; + + $tree = $reader->parseInnerTree(['{DAV:}privilege' => 'Sabre\\Xml\\Element\\Elements']); + foreach ($tree as $element) { + if ('{DAV:}privilege' !== $element['name']) { + continue; + } + $result[] = $element['value'][0]; + } + + return new self($result); + } + + /** + * Generate html representation for this value. + * + * The html output is 100% trusted, and no effort is being made to sanitize + * it. It's up to the implementor to sanitize user provided values. + * + * The output must be in UTF-8. + * + * The baseUri parameter is a url to the root of the application, and can + * be used to construct local links. + * + * @return string + */ + public function toHtml(HtmlOutputHelper $html) + { + return implode( + ', ', + array_map([$html, 'xmlName'], $this->getValue()) + ); + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/Principal.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/Principal.php new file mode 100644 index 0000000..5b9ee45 --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/Principal.php @@ -0,0 +1,186 @@ +type = $type; + if (self::HREF === $type && is_null($href)) { + throw new DAV\Exception('The href argument must be specified for the HREF principal type.'); + } + if ($href) { + $href = rtrim($href, '/').'/'; + parent::__construct($href); + } + } + + /** + * Returns the principal type. + * + * @return int + */ + public function getType() + { + return $this->type; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + switch ($this->type) { + case self::UNAUTHENTICATED: + $writer->writeElement('{DAV:}unauthenticated'); + break; + case self::AUTHENTICATED: + $writer->writeElement('{DAV:}authenticated'); + break; + case self::HREF: + parent::xmlSerialize($writer); + break; + case self::ALL: + $writer->writeElement('{DAV:}all'); + break; + } + } + + /** + * Generate html representation for this value. + * + * The html output is 100% trusted, and no effort is being made to sanitize + * it. It's up to the implementor to sanitize user provided values. + * + * The output must be in UTF-8. + * + * The baseUri parameter is a url to the root of the application, and can + * be used to construct local links. + * + * @return string + */ + public function toHtml(HtmlOutputHelper $html) + { + switch ($this->type) { + case self::UNAUTHENTICATED: + return 'unauthenticated'; + case self::AUTHENTICATED: + return 'authenticated'; + case self::HREF: + return parent::toHtml($html); + case self::ALL: + return 'all'; + } + + return 'unknown'; + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * Important note 2: You are responsible for advancing the reader to the + * next element. Not doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $tree = $reader->parseInnerTree()[0]; + + switch ($tree['name']) { + case '{DAV:}unauthenticated': + return new self(self::UNAUTHENTICATED); + case '{DAV:}authenticated': + return new self(self::AUTHENTICATED); + case '{DAV:}href': + return new self(self::HREF, $tree['value']); + case '{DAV:}all': + return new self(self::ALL); + default: + throw new BadRequest('Unknown or unsupported principal type: '.$tree['name']); + } + } +} diff --git a/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php new file mode 100644 index 0000000..6e7514b --- /dev/null +++ b/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php @@ -0,0 +1,146 @@ +privileges = $privileges; + } + + /** + * Returns the privilege value. + * + * @return array + */ + public function getValue() + { + return $this->privileges; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + $this->serializePriv($writer, '{DAV:}all', ['aggregates' => $this->privileges]); + } + + /** + * Generate html representation for this value. + * + * The html output is 100% trusted, and no effort is being made to sanitize + * it. It's up to the implementor to sanitize user provided values. + * + * The output must be in UTF-8. + * + * The baseUri parameter is a url to the root of the application, and can + * be used to construct local links. + * + * @return string + */ + public function toHtml(HtmlOutputHelper $html) + { + $traverse = function ($privName, $priv) use (&$traverse, $html) { + echo '
  • '; + 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/composer/vendor/sabre/dav/lib/DAVACL/Xml/Request/AclPrincipalPropSetReport.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Request/AclPrincipalPropSetReport.php new file mode 100644 index 0000000..4fc6127 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/dav/lib/DAVACL/Xml/Request/ExpandPropertyReport.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Request/ExpandPropertyReport.php new file mode 100644 index 0000000..70a7e22 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalMatchReport.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalMatchReport.php new file mode 100644 index 0000000..b495824 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalPropertySearchReport.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalPropertySearchReport.php new file mode 100644 index 0000000..bddceca --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalSearchPropertySetReport.php b/lib/composer/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalSearchPropertySetReport.php new file mode 100644 index 0000000..7f15d8a --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/event/.php-cs-fixer.dist.php b/lib/composer/vendor/sabre/event/.php-cs-fixer.dist.php new file mode 100644 index 0000000..319886c --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/event/LICENSE b/lib/composer/vendor/sabre/event/LICENSE new file mode 100644 index 0000000..962a492 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/event/composer.json b/lib/composer/vendor/sabre/event/composer.json new file mode 100644 index 0000000..0d3ec06 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/event/lib/Emitter.php b/lib/composer/vendor/sabre/event/lib/Emitter.php new file mode 100644 index 0000000..e1f23fc --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/event/lib/EventEmitter.php b/lib/composer/vendor/sabre/event/lib/EventEmitter.php new file mode 100644 index 0000000..865c99b --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/event/lib/Loop/functions.php b/lib/composer/vendor/sabre/event/lib/Loop/functions.php new file mode 100644 index 0000000..9412a77 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/event/lib/Promise.php b/lib/composer/vendor/sabre/event/lib/Promise.php new file mode 100644 index 0000000..66903fb --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/event/lib/Promise/functions.php b/lib/composer/vendor/sabre/event/lib/Promise/functions.php new file mode 100644 index 0000000..67e80cb --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/event/lib/PromiseAlreadyResolvedException.php b/lib/composer/vendor/sabre/event/lib/PromiseAlreadyResolvedException.php new file mode 100644 index 0000000..abb6c10 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/event/lib/coroutine.php b/lib/composer/vendor/sabre/event/lib/coroutine.php new file mode 100644 index 0000000..f664efa --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/.github/workflows/ci.yml b/lib/composer/vendor/sabre/http/.github/workflows/ci.yml new file mode 100644 index 0000000..56bc1a3 --- /dev/null +++ b/lib/composer/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@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: 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@v4 + if: matrix.coverage != 'none' diff --git a/lib/composer/vendor/sabre/http/.gitignore b/lib/composer/vendor/sabre/http/.gitignore new file mode 100644 index 0000000..a5356dd --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/.php-cs-fixer.dist.php b/lib/composer/vendor/sabre/http/.php-cs-fixer.dist.php new file mode 100644 index 0000000..f9d4b7a --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/.php_cs.dist b/lib/composer/vendor/sabre/http/.php_cs.dist new file mode 100644 index 0000000..c5c78a9 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/CHANGELOG.md b/lib/composer/vendor/sabre/http/CHANGELOG.md new file mode 100644 index 0000000..4158150 --- /dev/null +++ b/lib/composer/vendor/sabre/http/CHANGELOG.md @@ -0,0 +1,398 @@ +ChangeLog +========= + +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/composer/vendor/sabre/http/LICENSE b/lib/composer/vendor/sabre/http/LICENSE new file mode 100644 index 0000000..864041b --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/README.md b/lib/composer/vendor/sabre/http/README.md new file mode 100644 index 0000000..2f01c14 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/bin/.empty b/lib/composer/vendor/sabre/http/bin/.empty new file mode 100644 index 0000000..e69de29 diff --git a/lib/composer/vendor/sabre/http/composer.json b/lib/composer/vendor/sabre/http/composer.json new file mode 100644 index 0000000..48caa44 --- /dev/null +++ b/lib/composer/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", + "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/composer/vendor/sabre/http/examples/asyncclient.php b/lib/composer/vendor/sabre/http/examples/asyncclient.php new file mode 100644 index 0000000..5bcc844 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/examples/basicauth.php b/lib/composer/vendor/sabre/http/examples/basicauth.php new file mode 100644 index 0000000..9c13da8 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/examples/client.php b/lib/composer/vendor/sabre/http/examples/client.php new file mode 100644 index 0000000..fd169a4 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/examples/digestauth.php b/lib/composer/vendor/sabre/http/examples/digestauth.php new file mode 100644 index 0000000..5594306 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/examples/reverseproxy.php b/lib/composer/vendor/sabre/http/examples/reverseproxy.php new file mode 100644 index 0000000..ce51ad4 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/examples/stringify.php b/lib/composer/vendor/sabre/http/examples/stringify.php new file mode 100644 index 0000000..8ceefe2 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/lib/Auth/AWS.php b/lib/composer/vendor/sabre/http/lib/Auth/AWS.php new file mode 100644 index 0000000..2690c63 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/lib/Auth/AbstractAuth.php b/lib/composer/vendor/sabre/http/lib/Auth/AbstractAuth.php new file mode 100644 index 0000000..07f451b --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/lib/Auth/Basic.php b/lib/composer/vendor/sabre/http/lib/Auth/Basic.php new file mode 100644 index 0000000..c1bad1a --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/lib/Auth/Bearer.php b/lib/composer/vendor/sabre/http/lib/Auth/Bearer.php new file mode 100644 index 0000000..580e239 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/lib/Auth/Digest.php b/lib/composer/vendor/sabre/http/lib/Auth/Digest.php new file mode 100644 index 0000000..08fa34f --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/lib/Client.php b/lib/composer/vendor/sabre/http/lib/Client.php new file mode 100644 index 0000000..c00f9e1 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/lib/ClientException.php b/lib/composer/vendor/sabre/http/lib/ClientException.php new file mode 100644 index 0000000..2ca4a28 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/lib/HttpException.php b/lib/composer/vendor/sabre/http/lib/HttpException.php new file mode 100644 index 0000000..80b3ae6 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/lib/MessageDecoratorTrait.php b/lib/composer/vendor/sabre/http/lib/MessageDecoratorTrait.php new file mode 100644 index 0000000..191ba0f --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/lib/MessageInterface.php b/lib/composer/vendor/sabre/http/lib/MessageInterface.php new file mode 100644 index 0000000..4531654 --- /dev/null +++ b/lib/composer/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) { + // Guessing we're a http endpoint. + $this->absoluteUrl = 'http://'. + ($this->getHeader('Host') ?? 'localhost'). + $this->getUrl(); + } + + 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/composer/vendor/sabre/http/lib/RequestDecorator.php b/lib/composer/vendor/sabre/http/lib/RequestDecorator.php new file mode 100644 index 0000000..23e790e --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/lib/RequestInterface.php b/lib/composer/vendor/sabre/http/lib/RequestInterface.php new file mode 100644 index 0000000..5ec7777 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/lib/ResponseDecorator.php b/lib/composer/vendor/sabre/http/lib/ResponseDecorator.php new file mode 100644 index 0000000..30b9ec0 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/lib/ResponseInterface.php b/lib/composer/vendor/sabre/http/lib/ResponseInterface.php new file mode 100644 index 0000000..fd73d25 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/lib/Version.php b/lib/composer/vendor/sabre/http/lib/Version.php new file mode 100644 index 0000000..4ac82f6 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/phpstan.neon b/lib/composer/vendor/sabre/http/phpstan.neon new file mode 100644 index 0000000..213da6d --- /dev/null +++ b/lib/composer/vendor/sabre/http/phpstan.neon @@ -0,0 +1,2 @@ +parameters: + level: 1 diff --git a/lib/composer/vendor/sabre/http/tests/HTTP/Auth/AWSTest.php b/lib/composer/vendor/sabre/http/tests/HTTP/Auth/AWSTest.php new file mode 100644 index 0000000..a019adc --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/tests/HTTP/Auth/BasicTest.php b/lib/composer/vendor/sabre/http/tests/HTTP/Auth/BasicTest.php new file mode 100644 index 0000000..ef333eb --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/tests/HTTP/Auth/BearerTest.php b/lib/composer/vendor/sabre/http/tests/HTTP/Auth/BearerTest.php new file mode 100644 index 0000000..82e18d2 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/tests/HTTP/Auth/DigestTest.php b/lib/composer/vendor/sabre/http/tests/HTTP/Auth/DigestTest.php new file mode 100644 index 0000000..f3be054 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/tests/HTTP/ClientTest.php b/lib/composer/vendor/sabre/http/tests/HTTP/ClientTest.php new file mode 100644 index 0000000..8e7efef --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/tests/HTTP/FunctionsTest.php b/lib/composer/vendor/sabre/http/tests/HTTP/FunctionsTest.php new file mode 100644 index 0000000..3bdbb60 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/tests/HTTP/MessageDecoratorTest.php b/lib/composer/vendor/sabre/http/tests/HTTP/MessageDecoratorTest.php new file mode 100644 index 0000000..78293eb --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/tests/HTTP/MessageTest.php b/lib/composer/vendor/sabre/http/tests/HTTP/MessageTest.php new file mode 100644 index 0000000..597067e --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/tests/HTTP/NegotiateTest.php b/lib/composer/vendor/sabre/http/tests/HTTP/NegotiateTest.php new file mode 100644 index 0000000..82f14ea --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/tests/HTTP/RequestDecoratorTest.php b/lib/composer/vendor/sabre/http/tests/HTTP/RequestDecoratorTest.php new file mode 100644 index 0000000..f6f3a72 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/tests/HTTP/RequestTest.php b/lib/composer/vendor/sabre/http/tests/HTTP/RequestTest.php new file mode 100644 index 0000000..a810900 --- /dev/null +++ b/lib/composer/vendor/sabre/http/tests/HTTP/RequestTest.php @@ -0,0 +1,137 @@ + '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); + } +} diff --git a/lib/composer/vendor/sabre/http/tests/HTTP/ResponseDecoratorTest.php b/lib/composer/vendor/sabre/http/tests/HTTP/ResponseDecoratorTest.php new file mode 100644 index 0000000..a4ef31c --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/tests/HTTP/ResponseTest.php b/lib/composer/vendor/sabre/http/tests/HTTP/ResponseTest.php new file mode 100644 index 0000000..ef21294 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/tests/HTTP/SapiTest.php b/lib/composer/vendor/sabre/http/tests/HTTP/SapiTest.php new file mode 100644 index 0000000..a0b4c6f --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/tests/HTTP/URLUtilTest.php b/lib/composer/vendor/sabre/http/tests/HTTP/URLUtilTest.php new file mode 100644 index 0000000..bf0503e --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/tests/bootstrap.php b/lib/composer/vendor/sabre/http/tests/bootstrap.php new file mode 100644 index 0000000..b47f704 --- /dev/null +++ b/lib/composer/vendor/sabre/http/tests/bootstrap.php @@ -0,0 +1,17 @@ + + + + HTTP/ + + + + + + ../lib/ + + + + + + + diff --git a/lib/composer/vendor/sabre/http/tests/www/bar.php b/lib/composer/vendor/sabre/http/tests/www/bar.php new file mode 100644 index 0000000..05de39e --- /dev/null +++ b/lib/composer/vendor/sabre/http/tests/www/bar.php @@ -0,0 +1,5 @@ + +bar diff --git a/lib/composer/vendor/sabre/http/tests/www/connection_aborted.php b/lib/composer/vendor/sabre/http/tests/www/connection_aborted.php new file mode 100644 index 0000000..724ad2d --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/http/tests/www/foo b/lib/composer/vendor/sabre/http/tests/www/foo new file mode 100644 index 0000000..257cc56 --- /dev/null +++ b/lib/composer/vendor/sabre/http/tests/www/foo @@ -0,0 +1 @@ +foo diff --git a/lib/composer/vendor/sabre/http/tests/www/large.php b/lib/composer/vendor/sabre/http/tests/www/large.php new file mode 100644 index 0000000..65fefb5 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/uri/LICENSE b/lib/composer/vendor/sabre/uri/LICENSE new file mode 100644 index 0000000..ae2c992 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/uri/composer.json b/lib/composer/vendor/sabre/uri/composer.json new file mode 100644 index 0000000..ba4a8e0 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/uri/lib/InvalidUriException.php b/lib/composer/vendor/sabre/uri/lib/InvalidUriException.php new file mode 100644 index 0000000..7f37ca5 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/LICENSE b/lib/composer/vendor/sabre/vobject/LICENSE new file mode 100644 index 0000000..a99c8da --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/README.md b/lib/composer/vendor/sabre/vobject/README.md new file mode 100644 index 0000000..659e3fa --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/bin/bench.php b/lib/composer/vendor/sabre/vobject/bin/bench.php new file mode 100755 index 0000000..0a2736f --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/bin/bench_manipulatevcard.php b/lib/composer/vendor/sabre/vobject/bin/bench_manipulatevcard.php new file mode 100644 index 0000000..df6d9f2 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/bin/fetch_windows_zones.php b/lib/composer/vendor/sabre/vobject/bin/fetch_windows_zones.php new file mode 100755 index 0000000..2361dc3 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/bin/generateicalendardata.php b/lib/composer/vendor/sabre/vobject/bin/generateicalendardata.php new file mode 100755 index 0000000..019ed97 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/bin/mergeduplicates.php b/lib/composer/vendor/sabre/vobject/bin/mergeduplicates.php new file mode 100755 index 0000000..31b2c14 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/bin/rrulebench.php b/lib/composer/vendor/sabre/vobject/bin/rrulebench.php new file mode 100644 index 0000000..6900800 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/bin/vobject b/lib/composer/vendor/sabre/vobject/bin/vobject new file mode 100755 index 0000000..2aca7e7 --- /dev/null +++ b/lib/composer/vendor/sabre/vobject/bin/vobject @@ -0,0 +1,27 @@ +#!/usr/bin/env php +main($argv)); + diff --git a/lib/composer/vendor/sabre/vobject/composer.json b/lib/composer/vendor/sabre/vobject/composer.json new file mode 100644 index 0000000..9d1b426 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/BirthdayCalendarGenerator.php b/lib/composer/vendor/sabre/vobject/lib/BirthdayCalendarGenerator.php new file mode 100644 index 0000000..fade50e --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Cli.php b/lib/composer/vendor/sabre/vobject/lib/Cli.php new file mode 100644 index 0000000..3bde16f --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Component.php b/lib/composer/vendor/sabre/vobject/lib/Component.php new file mode 100644 index 0000000..ca82ad4 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Component/Available.php b/lib/composer/vendor/sabre/vobject/lib/Component/Available.php new file mode 100644 index 0000000..5510b9e --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Component/VAlarm.php b/lib/composer/vendor/sabre/vobject/lib/Component/VAlarm.php new file mode 100644 index 0000000..bd00eb6 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Component/VAvailability.php b/lib/composer/vendor/sabre/vobject/lib/Component/VAvailability.php new file mode 100644 index 0000000..04ec38d --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Component/VCalendar.php b/lib/composer/vendor/sabre/vobject/lib/Component/VCalendar.php new file mode 100644 index 0000000..017aed7 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Component/VCard.php b/lib/composer/vendor/sabre/vobject/lib/Component/VCard.php new file mode 100644 index 0000000..82fab82 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Component/VEvent.php b/lib/composer/vendor/sabre/vobject/lib/Component/VEvent.php new file mode 100644 index 0000000..6ea93ed --- /dev/null +++ b/lib/composer/vendor/sabre/vobject/lib/Component/VEvent.php @@ -0,0 +1,140 @@ +RRULE) { + 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/composer/vendor/sabre/vobject/lib/Component/VFreeBusy.php b/lib/composer/vendor/sabre/vobject/lib/Component/VFreeBusy.php new file mode 100644 index 0000000..fef418b --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Component/VJournal.php b/lib/composer/vendor/sabre/vobject/lib/Component/VJournal.php new file mode 100644 index 0000000..9b7f1b8 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Component/VTimeZone.php b/lib/composer/vendor/sabre/vobject/lib/Component/VTimeZone.php new file mode 100644 index 0000000..21c0623 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Component/VTodo.php b/lib/composer/vendor/sabre/vobject/lib/Component/VTodo.php new file mode 100644 index 0000000..6f022ba --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/DateTimeParser.php b/lib/composer/vendor/sabre/vobject/lib/DateTimeParser.php new file mode 100644 index 0000000..69072ef --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Document.php b/lib/composer/vendor/sabre/vobject/lib/Document.php new file mode 100644 index 0000000..36f20dd --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/ElementList.php b/lib/composer/vendor/sabre/vobject/lib/ElementList.php new file mode 100644 index 0000000..e419d48 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/EofException.php b/lib/composer/vendor/sabre/vobject/lib/EofException.php new file mode 100644 index 0000000..837af7e --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/FreeBusyGenerator.php b/lib/composer/vendor/sabre/vobject/lib/FreeBusyGenerator.php new file mode 100644 index 0000000..56ae166 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/ITip/Broker.php b/lib/composer/vendor/sabre/vobject/lib/ITip/Broker.php new file mode 100644 index 0000000..9d68fc4 --- /dev/null +++ b/lib/composer/vendor/sabre/vobject/lib/ITip/Broker.php @@ -0,0 +1,986 @@ +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; + } + + if (in_array($eventInfo['organizer'], $userHref)) { + return $this->parseEventForOrganizer($baseCalendar, $eventInfo, $oldEventInfo); + } elseif ($oldCalendar) { + // We need to figure out if the user is an attendee, but we're only + // doing so if there's an oldCalendar, because we only want to + // process updates, not creation of new events. + foreach ($eventInfo['attendees'] as $attendee) { + if (in_array($attendee['href'], $userHref)) { + return $this->parseEventForAttendee($baseCalendar, $eventInfo, $oldEventInfo, $attendee['href']); + } + } + } + + 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; + } + } + + // 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 $attendee) { + unset($attendee['SCHEDULE-FORCE-SEND']); + unset($attendee['SCHEDULE-STATUS']); + + // We're adding PARTSTAT=NEEDS-ACTION to ensure that + // iOS shows an "Inbox Item" + if (!isset($attendee['PARTSTAT'])) { + $attendee['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(); + } + } + 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, + ]; + } + } + $instances[$recurId] = $vevent; + } + + 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/composer/vendor/sabre/vobject/lib/ITip/ITipException.php b/lib/composer/vendor/sabre/vobject/lib/ITip/ITipException.php new file mode 100644 index 0000000..9495636 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/ITip/SameOrganizerForAllComponentsException.php b/lib/composer/vendor/sabre/vobject/lib/ITip/SameOrganizerForAllComponentsException.php new file mode 100644 index 0000000..4c48625 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/PHPUnitAssertions.php b/lib/composer/vendor/sabre/vobject/lib/PHPUnitAssertions.php new file mode 100644 index 0000000..45c0a21 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Parameter.php b/lib/composer/vendor/sabre/vobject/lib/Parameter.php new file mode 100644 index 0000000..0f0b586 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/ParseException.php b/lib/composer/vendor/sabre/vobject/lib/ParseException.php new file mode 100644 index 0000000..a8f497b --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Parser/MimeDir.php b/lib/composer/vendor/sabre/vobject/lib/Parser/MimeDir.php new file mode 100644 index 0000000..d484d6a --- /dev/null +++ b/lib/composer/vendor/sabre/vobject/lib/Parser/MimeDir.php @@ -0,0 +1,689 @@ +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, + ]; + + $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 + continue; + } + throw new ParseException('Invalid Mimedir file. Line starting at '.$this->startLine.' did not follow iCalendar/vCard conventions'); + } + 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, + ]; + } + continue; + } + if (isset($match['paramName'])) { + $lastParam = strtoupper($match['paramName']); + if (!isset($property['parameters'][$lastParam])) { + $property['parameters'][$lastParam] = null; + } + continue; + } + if (isset($match['propValue'])) { + $property['value'] = $match['propValue']; + continue; + } + if (isset($match['name']) && $match['name']) { + $property['name'] = strtoupper($match['name']); + continue; + } + + // @codeCoverageIgnoreStart + throw new \LogicException('This code should not be reachable'); + // @codeCoverageIgnoreEnd + } + + if (is_null($property['value'])) { + $property['value'] = ''; + } + if (!$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/composer/vendor/sabre/vobject/lib/Parser/Parser.php b/lib/composer/vendor/sabre/vobject/lib/Parser/Parser.php new file mode 100644 index 0000000..b7b6114 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Parser/XML.php b/lib/composer/vendor/sabre/vobject/lib/Parser/XML.php new file mode 100644 index 0000000..7877317 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Parser/XML/Element/KeyValue.php b/lib/composer/vendor/sabre/vobject/lib/Parser/XML/Element/KeyValue.php new file mode 100644 index 0000000..e017725 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Property.php b/lib/composer/vendor/sabre/vobject/lib/Property.php new file mode 100644 index 0000000..7cf5914 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Property/Binary.php b/lib/composer/vendor/sabre/vobject/lib/Property/Binary.php new file mode 100644 index 0000000..1262dd0 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Property/Boolean.php b/lib/composer/vendor/sabre/vobject/lib/Property/Boolean.php new file mode 100644 index 0000000..4bd6ffd --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Property/FlatText.php b/lib/composer/vendor/sabre/vobject/lib/Property/FlatText.php new file mode 100644 index 0000000..d15cfe0 --- /dev/null +++ b/lib/composer/vendor/sabre/vobject/lib/Property/FlatText.php @@ -0,0 +1,46 @@ +setValue($val); + } +} diff --git a/lib/composer/vendor/sabre/vobject/lib/Property/FloatValue.php b/lib/composer/vendor/sabre/vobject/lib/Property/FloatValue.php new file mode 100644 index 0000000..e780ae6 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Property/ICalendar/CalAddress.php b/lib/composer/vendor/sabre/vobject/lib/Property/ICalendar/CalAddress.php new file mode 100644 index 0000000..c90967d --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Property/ICalendar/Date.php b/lib/composer/vendor/sabre/vobject/lib/Property/ICalendar/Date.php new file mode 100644 index 0000000..d8e86d1 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Property/ICalendar/Duration.php b/lib/composer/vendor/sabre/vobject/lib/Property/ICalendar/Duration.php new file mode 100644 index 0000000..e18fe19 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Property/ICalendar/Period.php b/lib/composer/vendor/sabre/vobject/lib/Property/ICalendar/Period.php new file mode 100644 index 0000000..ae8a789 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Property/ICalendar/Recur.php b/lib/composer/vendor/sabre/vobject/lib/Property/ICalendar/Recur.php new file mode 100644 index 0000000..cd3d7a5 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Property/IntegerValue.php b/lib/composer/vendor/sabre/vobject/lib/Property/IntegerValue.php new file mode 100644 index 0000000..3ae7752 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Property/Text.php b/lib/composer/vendor/sabre/vobject/lib/Property/Text.php new file mode 100644 index 0000000..16d2c07 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Property/Time.php b/lib/composer/vendor/sabre/vobject/lib/Property/Time.php new file mode 100644 index 0000000..1b81609 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Property/Unknown.php b/lib/composer/vendor/sabre/vobject/lib/Property/Unknown.php new file mode 100644 index 0000000..6f404c2 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Property/Uri.php b/lib/composer/vendor/sabre/vobject/lib/Property/Uri.php new file mode 100644 index 0000000..1ad1fb1 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Property/UtcOffset.php b/lib/composer/vendor/sabre/vobject/lib/Property/UtcOffset.php new file mode 100644 index 0000000..04b8844 --- /dev/null +++ b/lib/composer/vendor/sabre/vobject/lib/Property/UtcOffset.php @@ -0,0 +1,70 @@ +value = $dt->format('Ymd'); + } +} diff --git a/lib/composer/vendor/sabre/vobject/lib/Property/VCard/DateAndOrTime.php b/lib/composer/vendor/sabre/vobject/lib/Property/VCard/DateAndOrTime.php new file mode 100644 index 0000000..7bf79c4 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Property/VCard/DateTime.php b/lib/composer/vendor/sabre/vobject/lib/Property/VCard/DateTime.php new file mode 100644 index 0000000..49c1f35 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Property/VCard/PhoneNumber.php b/lib/composer/vendor/sabre/vobject/lib/Property/VCard/PhoneNumber.php new file mode 100644 index 0000000..b714ffd --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Property/VCard/TimeStamp.php b/lib/composer/vendor/sabre/vobject/lib/Property/VCard/TimeStamp.php new file mode 100644 index 0000000..da6ea3d --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Reader.php b/lib/composer/vendor/sabre/vobject/lib/Reader.php new file mode 100644 index 0000000..055d546 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Recur/EventIterator.php b/lib/composer/vendor/sabre/vobject/lib/Recur/EventIterator.php new file mode 100644 index 0000000..55d6e47 --- /dev/null +++ b/lib/composer/vendor/sabre/vobject/lib/Recur/EventIterator.php @@ -0,0 +1,497 @@ +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)) { + $this->recurIterator = new RDateIterator( + $this->masterEvent->RDATE->getParts(), + $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/composer/vendor/sabre/vobject/lib/Recur/MaxInstancesExceededException.php b/lib/composer/vendor/sabre/vobject/lib/Recur/MaxInstancesExceededException.php new file mode 100644 index 0000000..cb08358 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Recur/RRuleIterator.php b/lib/composer/vendor/sabre/vobject/lib/Recur/RRuleIterator.php new file mode 100644 index 0000000..ca53b63 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Settings.php b/lib/composer/vendor/sabre/vobject/lib/Settings.php new file mode 100644 index 0000000..b0bb80a --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Splitter/SplitterInterface.php b/lib/composer/vendor/sabre/vobject/lib/Splitter/SplitterInterface.php new file mode 100644 index 0000000..c845ac5 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/StringUtil.php b/lib/composer/vendor/sabre/vobject/lib/StringUtil.php new file mode 100644 index 0000000..b04539e --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/TimezoneGuesser/FindFromOffset.php b/lib/composer/vendor/sabre/vobject/lib/TimezoneGuesser/FindFromOffset.php new file mode 100644 index 0000000..990ac96 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneMap.php b/lib/composer/vendor/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneMap.php new file mode 100644 index 0000000..b52ba6a --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/TimezoneGuesser/GuessFromLicEntry.php b/lib/composer/vendor/sabre/vobject/lib/TimezoneGuesser/GuessFromLicEntry.php new file mode 100644 index 0000000..f340a39 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/TimezoneGuesser/GuessFromMsTzId.php b/lib/composer/vendor/sabre/vobject/lib/TimezoneGuesser/GuessFromMsTzId.php new file mode 100644 index 0000000..b11ce18 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/TimezoneGuesser/TimezoneFinder.php b/lib/composer/vendor/sabre/vobject/lib/TimezoneGuesser/TimezoneFinder.php new file mode 100644 index 0000000..5aa880a --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/Version.php b/lib/composer/vendor/sabre/vobject/lib/Version.php new file mode 100644 index 0000000..ccaf1eb --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/timezonedata/exchangezones.php b/lib/composer/vendor/sabre/vobject/lib/timezonedata/exchangezones.php new file mode 100644 index 0000000..3e7eace --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/timezonedata/lotuszones.php b/lib/composer/vendor/sabre/vobject/lib/timezonedata/lotuszones.php new file mode 100644 index 0000000..4b50808 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/timezonedata/php-bc.php b/lib/composer/vendor/sabre/vobject/lib/timezonedata/php-bc.php new file mode 100644 index 0000000..3116c68 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/lib/timezonedata/windowszones.php b/lib/composer/vendor/sabre/vobject/lib/timezonedata/windowszones.php new file mode 100644 index 0000000..2049a95 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/resources/schema/xcal.rng b/lib/composer/vendor/sabre/vobject/resources/schema/xcal.rng new file mode 100644 index 0000000..4a51460 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/vobject/resources/schema/xcard.rng b/lib/composer/vendor/sabre/vobject/resources/schema/xcard.rng new file mode 100644 index 0000000..c0b7cfb --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/xml/.github/workflows/ci.yml b/lib/composer/vendor/sabre/xml/.github/workflows/ci.yml new file mode 100644 index 0000000..5775aba --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/xml/.php-cs-fixer.dist.php b/lib/composer/vendor/sabre/xml/.php-cs-fixer.dist.php new file mode 100644 index 0000000..f9d4b7a --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/xml/LICENSE b/lib/composer/vendor/sabre/xml/LICENSE new file mode 100644 index 0000000..c9faf40 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/xml/README.md b/lib/composer/vendor/sabre/xml/README.md new file mode 100644 index 0000000..55af24f --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/xml/bin/.empty b/lib/composer/vendor/sabre/xml/bin/.empty new file mode 100644 index 0000000..e69de29 diff --git a/lib/composer/vendor/sabre/xml/composer.json b/lib/composer/vendor/sabre/xml/composer.json new file mode 100644 index 0000000..d7577c2 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/xml/lib/ContextStackTrait.php b/lib/composer/vendor/sabre/xml/lib/ContextStackTrait.php new file mode 100644 index 0000000..4e15bd4 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/xml/lib/Deserializer/functions.php b/lib/composer/vendor/sabre/xml/lib/Deserializer/functions.php new file mode 100644 index 0000000..5081809 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/xml/lib/Element.php b/lib/composer/vendor/sabre/xml/lib/Element.php new file mode 100644 index 0000000..559eb54 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/xml/lib/Element/Cdata.php b/lib/composer/vendor/sabre/xml/lib/Element/Cdata.php new file mode 100644 index 0000000..1367343 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/xml/lib/Element/Elements.php b/lib/composer/vendor/sabre/xml/lib/Element/Elements.php new file mode 100644 index 0000000..6915fd4 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/xml/lib/Element/KeyValue.php b/lib/composer/vendor/sabre/xml/lib/Element/KeyValue.php new file mode 100644 index 0000000..7d75a3a --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/xml/lib/Element/Uri.php b/lib/composer/vendor/sabre/xml/lib/Element/Uri.php new file mode 100644 index 0000000..6527638 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/xml/lib/Element/XmlFragment.php b/lib/composer/vendor/sabre/xml/lib/Element/XmlFragment.php new file mode 100644 index 0000000..99d1f87 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/xml/lib/LibXMLException.php b/lib/composer/vendor/sabre/xml/lib/LibXMLException.php new file mode 100644 index 0000000..993f95f --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/xml/lib/ParseException.php b/lib/composer/vendor/sabre/xml/lib/ParseException.php new file mode 100644 index 0000000..158cf01 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/xml/lib/Serializer/functions.php b/lib/composer/vendor/sabre/xml/lib/Serializer/functions.php new file mode 100644 index 0000000..23f22d4 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/xml/lib/Service.php b/lib/composer/vendor/sabre/xml/lib/Service.php new file mode 100644 index 0000000..6e52263 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/xml/lib/Version.php b/lib/composer/vendor/sabre/xml/lib/Version.php new file mode 100644 index 0000000..c2da842 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/xml/lib/XmlDeserializable.php b/lib/composer/vendor/sabre/xml/lib/XmlDeserializable.php new file mode 100644 index 0000000..0a57203 --- /dev/null +++ b/lib/composer/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/composer/vendor/sabre/xml/lib/XmlSerializable.php b/lib/composer/vendor/sabre/xml/lib/XmlSerializable.php new file mode 100644 index 0000000..2affc33 --- /dev/null +++ b/lib/composer/vendor/sabre/xml/lib/XmlSerializable.php @@ -0,0 +1,34 @@ + diff --git a/source/helpers/cache.php b/source/helpers/cache.php new file mode 100644 index 0000000..f1698b8 --- /dev/null +++ b/source/helpers/cache.php @@ -0,0 +1,370 @@ +*/ +{ + + /** + */ + public function has( + string $key + ) : bool + ; + + + /** + */ + public function get( + string $key + )/* : type_value*/ + ; + + + /** + */ + public function set( + string $key, + /*type_value */$value + ) : void + ; + +} + + +/** + */ +class class_cache_encoded + implements interface_cache/**/ +{ + + /** + */ + private interface_cache $core; + + + /** + */ + private \Closure $encode; + + + /** + */ + private \Closure $decode; + + + /** + */ + public function __construct( + interface_cache $core, + \Closure $encode, + \Closure $decode + ) + { + $this->core = $core; + $this->encode = $encode; + $this->decode = $decode; + } + + + /** + * [implementation] + */ + public function has( + string $key + ) : bool + { + return $this->core->has($key); + } + + + /** + * [implementation] + */ + public function get( + string $key + )/* : type_value*/ + { + return ($this->decode)($this->core->get($key)); + } + + + /** + * [implementation] + */ + public function set( + string $key, + /*type_value */$value + ) : void + { + $this->core->set($key, ($this->encode)($value)); + } + +} + + +/** + */ +class class_cache_memory + implements interface_cache/**/ +{ + + /** + * @property array $data {map} + */ + private array $data; + + + /** + */ + public function __construct( + ) + { + $this->data = []; + } + + + /** + * [implementation] + */ + public function has( + string $key + ) : bool + { + return \array_key_exists($key, $this->data); + } + + + /** + * [implementation] + */ + public function get( + string $key + )/* : any*/ + { + if (! \array_key_exists($key, $this->data)) + { + throw (new \Exception('missing')); + } + else + { + return $this->data[$key]; + } + } + + + /** + * [implementation] + */ + public function set( + string $key, + /*any */$value + ) : void + { + $this->data[$key] = $value; + } + +} + + + +/** + */ +class class_cache_file + implements interface_cache/**/ +{ + + /** + */ + private $directory; + + + /** + */ + public function __construct( + string $directory + ) + { + $this->directory = $directory; + } + + + public function compose_path( + string $key + ) : string + { + return \sprintf( + '%s/%s', $this->directory, + \hash('sha256', $key) + ); + } + + + /** + * [implementation] + */ + public function has( + string $key + ) : bool + { + $path = $this->compose_path($key); + return \file_exists($path); + } + + + /** + * [implementation] + */ + public function get( + string $key + )/* : string*/ + { + $path = $this->compose_path($key); + if (! \file_exists($path)) + { + throw (new \Exception('missing')); + } + else + { + return \file_get_contents($path); + } + } + + + /** + * [implementation] + */ + public function set( + string $key, + /*string */$value + ) : void + { + $path = $this->compose_path($key); + \file_put_contents($path, $value); + } + +} + + +/** + */ +class class_cache_apcu + implements interface_cache/**/ +{ + + /** + */ + private string $prefix; + + + /** + */ + public function __construct( + ?array $options = null + ) + { + $options = \array_merge( + [ + 'prefix' => 'davigil_', + ], + ($options ?? []) + ); + $this->prefix = $options['prefix']; + } + + + /** + */ + private function compose_id( + string $key + ) : string + { + return \sprintf('%s%s', $this->prefix, $key); + } + + + /** + * [implementation] + */ + public function has( + string $key + ) : bool + { + $id = $this->compose_id($key); + return \apcu_exists($id); + } + + + /** + * [implementation] + */ + public function get( + string $key + )/* : any*/ + { + $id = $this->compose_id($key); + return \apcu_fetch($id); + } + + + /** + * [implementation] + */ + public function set( + string $key, + /*any */$value + ) : void + { + $id = $this->compose_id($key); + \apcu_store($id, $value); + } + +} + + +/** + */ +function get/**/( + interface_cache $cache/**/, + string $key, + \Closure $retrieve, + ?array $options = null +)/* : type_value*/ +{ + $options = \array_merge( + [ + 'ttl' => null, + 'now' => \time(), + ], + ($options ?? []) + ); + if (! $cache->has($key)) + { + $entry = null; + $shall_retrieve = true; + } + else + { + $entry = $cache->get($key); + $shall_retrieve = (($entry['expiry'] !== null) && ($options['now'] >= $entry['expiry'])); + } + if ($shall_retrieve) + { + $value = ($retrieve)(); + $entry = [ + 'expiry' => (($options['ttl'] === null) ? null : ($options['now'] + $options['ttl'])), + 'value' => $value, + ]; + $cache->set($key, $entry); + } + else + { + $value = $entry['value']; + } + return $value; +} + + ?> diff --git a/source/helpers/call.php b/source/helpers/call.php new file mode 100644 index 0000000..8d953af --- /dev/null +++ b/source/helpers/call.php @@ -0,0 +1,21 @@ + diff --git a/source/helpers/ics.php b/source/helpers/ics.php new file mode 100644 index 0000000..26927f8 --- /dev/null +++ b/source/helpers/ics.php @@ -0,0 +1,793 @@ +year = $year; + $this->month = $month; + $this->day = $day; + } +} + + +/** + */ +class struct_time +{ + public int $hour; + public int $minute; + public int $second; + public bool $utc; + + public function __construct( + int $hour, + int $minute, + int $second, + bool $utc + ) + { + $this->hour = $hour; + $this->minute = $minute; + $this->second = $second; + $this->utc = $utc; + } +} + + +/** + */ +class struct_datetime +{ + public struct_date $date; + public ?struct_time $time; + + public function __construct( + struct_date $date, + ?struct_time $time + ) + { + $this->date = $date; + $this->time = $time; + } +} + + +/** + */ +class struct_dt +{ + public /*type_tzid*/string $tzid; + public struct_datetime $value; + + public function __construct( + string $tzid, + struct_datetime $value + ) + { + $this->tzid = $tzid; + $this->value = $value; + } +} + + +/** + */ +class struct_duration +{ + public bool $negative; + public ?int $weeks; + public ?int $days; + public ?int $hours; + public ?int $minutes; + public ?int $seconds; +} + + +/** + */ +class struct_vtimezone_entry +{ + public struct_datetime $dtstart; + public struct_rrule $rrule; + public ?/*type_offset*/string $tzoffsetfrom; + public ?/*type_offset*/string $tzoffsetto; +} + + +/** + */ +class struct_vtimezone +{ + public ?string/*type_tzid*/ $tzid; + public ?struct_vtimezone_entry $standard; + public ?struct_vtimezone_entry $daylight; +} + + +/** + */ +class struct_geo +{ + public float $latitude; + public float $longitude; +} + + +/** + */ +class struct_organizer +{ + public ?string $value; + public ?string $cn; + public ?string $dir; + public ?string $sent_by; +} + + +/** + * @see https://www.rfc-editor.org/rfc/rfc5545#section-3.6.1 + */ +class struct_vevent +{ + // required + public string $uid; + public struct_datetime $dtstamp; + + // required if "method" is not specified in the parent + public ?struct_dt $dtstart; + + // optional + public ?enum_class $class; + public ?struct_datetime $created; + public ?string $description; + public ?struct_geo $geo; + public ?struct_datetime $last_modified; + public ?string $location; + /** + * @see https://www.rfc-editor.org/rfc/rfc5545#section-3.8.4.3 + */ + public ?struct_organizer $organizer; + public ?int $priority; + public ?int $sequence; + public ?enum_event_status $status; + public ?string $summary; + public ?enum_transp $transp; + public ?string $url; + public $recurid; + public ?type_rrule $rrule; + + // either or + public ?struct_dt $dtend; + public ?type_duration $duration; + + // optional + public $attach; + public ?string $attendee; + public /*list*/?array $categories; + public $comment; + public $contact; + public $exdate; + public $rstatus; + public $related; + public $resources; + public $rdate; + + // extra + public /*map*/?array $x_props; + public /*map*/?array $iana_props; +} + + +/** + * @see https://www.rfc-editor.org/rfc/rfc5545#section-3.4 + */ +class struct_vcalendar +{ + // required + public string $version; + public string $prodid; + public /*list*/array $vevents; + + // optional + public ?string $calscale; + public ?string $method; + public ?struct_vtimezone $vtimezone; + + // extra + public /*map*/?array $x_props; + public /*map*/?array $iana_props; +}; + + + +/** + */ +function date_decode( + string $date_encoded +) : struct_date +{ + return (new struct_date( + \intval(\substr($date_encoded, 0, 4)), + \intval(\substr($date_encoded, 4, 2)), + \intval(\substr($date_encoded, 6, 2)) + )); +} + + +/** + */ +function time_decode( + string $time_encoded +) : struct_time +{ + return (new struct_time( + \intval(\substr($time_encoded, 0, 2)), + \intval(\substr($time_encoded, 2, 2)), + \intval(\substr($time_encoded, 4, 2)), + ((\strlen($time_encoded >= 7) && ($time_encoded[6] === 'Z'))) + )); +} + + +/** + */ +function datetime_decode( + string $datetime_encoded +) : struct_datetime +{ + $parts = \explode('T', $datetime_encoded, 2); + return (new struct_datetime( + date_decode($parts[0]), + ((\count($parts) >= 2) ? time_decode($parts[1]) : null) + )); +} + + +/** + */ +enum enum_decode_state_label { + case expect_vcalendar_begin/* = "expect_vcalendar_begin"*/; + case expect_vcalendar_property/* = "expect_vcalendar_property"*/; + case expect_vevent_property/* = "expect_vevent_property"*/; + case done/* = "done"*/; +} + + +/** + */ +function class_encode( + enum_class $class +) : string +{ + switch ($class) + { + case enum_class::private_: {return 'PRIVATE'; break;} + case enum_class::public_: {return 'PUBLIC'; break;} + case enum_class::confidential: {return 'CONFIDENTIAL'; break;} + } +} + + +/** + */ +function class_decode( + string $class_encoded +) : enum_class +{ + return [ + 'PRIVATE' => enum_class::private_, + 'PUBLIC' => enum_class::public_, + 'CONFIDENTIAL' => enum_class::confidential, + ][$class_encoded]; +} + + +/** + */ +function event_status_encode( + enum_event_status $event_status +) : string +{ + switch ($event_status) + { + case enum_event_status::tentative: {return 'TENTATIVE'; break;} + case enum_event_status::confirmed: {return 'CONFIRMED'; break;} + case enum_event_status::cancelled: {return 'CANCELLED'; break;} + } +} + + +/** + */ +function event_status_decode( + string $event_status_encoded +) : enum_event_status +{ + return [ + 'TENTATIVE' => enum_event_status::tentative, + 'CONFIRMED' => enum_event_status::confirmed, + 'CANCELLED' => enum_event_status::cancelled, + ][$event_status_encoded]; +} + + +/** + */ +function transp_encode( + enum_transp $transp +) : string +{ + switch (transp) + { + case enum_transp::opaque: {return 'OPAQUE'; break;} + case enum_transp::transparent: {return 'TRANSPARENT'; break;} + } +} + + +/** + */ +function transp_decode( + string $transp_encoded +) : enum_transp +{ + return [ + 'OPAQUE' => enum_transp::opaque, + 'TRANSPARENT' => enum_transp::transparent, + ][$transp_encoded]; +} + + +/** + */ +/* +function datetime_to_unixtimestamp( + struct_datetime $datetime +) : int +{ + if (($datetime->time !== null) && (! $datetime->time->utc)) + { + throw (new \Exception('can not convert not utc time values')); + } + else + { + return lib_plankton.pit.from_datetime( + { + "timezone_shift": 0, + "date": { + "year": datetime.date.year, + "month": datetime.date.month, + "day": datetime.date.day, + }, + "time": { + "hour": ((datetime.time === null) ? 0 : datetime.time.hour), + "minute": ((datetime.time === null) ? 0 : datetime.time.minute), + "second": ((datetime.time === null) ? 0 : datetime.time.second), + } + } + ); + } +} + */ + + + +/** + */ +function vcalendar_decode( + string $ics +) : struct_vcalendar +{ + $path = \sprintf('/tmp/foo.ics'); + \file_put_contents($path, $ics); + $ical = new \ICal\ICal( + $path, + [ + 'defaultSpan' => 2, // Default value + 'defaultTimeZone' => 'UTC', + 'defaultWeekStart' => 'MO', // Default value + 'disableCharacterReplacement' => false, // Default value + 'filterDaysAfter' => null, // Default value + 'filterDaysBefore' => null, // Default value + 'httpUserAgent' => null, // Default value + 'skipRecurrence' => false, // Default value + ] + ); + // $ical->initFile($path); + /** + * @todo transform correctly + */ + $result = new struct_vcalendar(); + $result->events = \array_map( + function ($event_raw) { + $vevent = new struct_vevent(); + $vevent->uid = $event_raw->uid; + $vevent->summary = $event_raw->summary; + $vevent->dtstart = (new struct_dt( + '', + datetime_decode($event_raw->dtstart) + )); + $vevent->dtend = ( + ($event_raw->dtend === null) + ? + null + : + (new struct_dt( + '', + datetime_decode($event_raw->dtend) + )) + ); + $vevent->location = $event_raw->location; + $vevent->description = $event_raw->description; + $vevent->categories = \array_reduce( + \array_map( + fn($x) => \explode(',', $x), + \array_values( + \array_filter( + $event_raw->additionalProperties['categories_array'], + fn($x) => \is_string($x) + ) + ) + ), + fn($x, $y) => \array_merge($x, $y), + [] + ); + return $vevent; + }, + $ical->events() + ); + return $result; +} + + +/** + * @see https://www.rfc-editor.org/rfc/rfc5545 + * @see https://icalendar.org/iCalendar-RFC-5545/ + */ +function date_encode( + struct_date $date +) : string +{ + return \davigil\helpers\string_\coin( + '{{year}}{{month}}{{day}}', + [ + 'year' => \str_pad(\sprintf('%u', $date->year), 4, '0', \STR_PAD_LEFT), + 'month' => \str_pad(\sprintf('%u', $date->month), 2, '0', \STR_PAD_LEFT), + 'day' => \str_pad(\sprintf('%u', $date->day), 2, '0', \STR_PAD_LEFT), + ] + ); +} + + +/** + */ +function time_encode( + struct_time $time +) : string +{ + return \davigil\helpers\string_\coin( + '{{hour}}{{minute}}{{second}}{{utc}}', + [ + 'hour' => \str_pad(\sprintf('%u', $time->hour), 2, '0', \STR_PAD_LEFT), + 'minute' => \str_pad(\sprintf('%u', $time->minute), 2, '0', \STR_PAD_LEFT), + 'second' => \str_pad(\sprintf('%u', $time->second), 2, '0', \STR_PAD_LEFT), + 'utc' => ($time->utc ? 'Z' : ''), + ] + ); +} + + +/** + */ +function datetime_encode( + struct_datetime $datetime +) : string +{ + return \davigil\helpers\string_\coin( + '{{date}}T{{time}}', + [ + 'date' => date_encode($datetime->date), + 'time' => time_encode($datetime->time), + ] + ); +} + + +/** + * @todo complete + */ +function vcalendar_encode( + struct_vcalendar $vcalendar +) : string +{ + $content_lines = []; + \array_push($content_lines, 'BEGIN:VCALENDAR'); + \array_push($content_lines, \davigil\helpers\string_\coin('VERSION:{{version}}', ['version' => $vcalendar->version])); + \array_push($content_lines, \davigil\helpers\string_\coin('PRODID:{{prodid}}', ['prodid' => $vcalendar->prodid])); + \array_push($content_lines, \davigil\helpers\string_\coin('METHOD:{{method}}', ['method' => $vcalendar->method])); + foreach ($vcalendar->events as $vevent) + { + \array_push($content_lines, 'BEGIN:VEVENT'); + { + // uid + \array_push( + $content_lines, + \davigil\helpers\string_\coin( + 'UID:{{uid}}', + [ + 'uid' => $vevent->uid, + ] + ) + ); + + // dtstart + \array_push( + $content_lines, + \davigil\helpers\string_\coin( + 'DTSTART:{{dtstart}}', + [ + 'dtstart' => datetime_encode($vevent->dtstart->value), + ] + ) + ); + + // dtend + if ($vevent->dtend !== null) + { + \array_push( + $content_lines, + \davigil\helpers\string_\coin( + 'DTEND:{{dtend}}', + [ + 'dtend' => datetime_encode($vevent->dtend->value), + ] + ) + ); + } + + // dtstamp + \array_push( + $content_lines, + \davigil\helpers\string_\coin( + 'DTSTAMP:{{dtstamp}}', + [ + 'dtstamp' => datetime_encode($vevent->dtstamp), + ] + ) + ); + + // class + if ($vevent->class !== null) + { + \array_push( + $content_lines, + \davigil\helpers\string_\coin( + 'CLASS:{{class}}', + [ + 'class' => class_encode($vevent->class), + ] + ) + ); + } + + // summary + \array_push( + $content_lines, + \davigil\helpers\string_\coin( + 'SUMMARY:{{summary}}', + [ + 'summary' => $vevent->summary, + ] + ) + ); + + // description + if ($vevent->description !== null) + { + \array_push( + $content_lines, + \davigil\helpers\string_\coin( + 'DESCRIPTION:{{description}}', + [ + 'description' => $vevent->description, + ] + ) + ); + } + + // location + if ($vevent->location !== null) + { + \array_push( + $content_lines, + \davigil\helpers\string_\coin( + 'LOCATION:{{location}}', + [ + 'location' => $vevent->location, + ] + ) + ); + } + + /* + // geo + if (vevent.geo !== undefined) { + content_lines.push( + lib_plankton.string.coin( + "GEO:{{geo_latitude}};{{geo_longitude}}", + { + "geo_latitude": vevent.geo.latitude.toFixed(4), + "geo_longitude": vevent.geo.longitude.toFixed(4), + } + ) + ); + } + */ + + /* + // url + if (vevent.url !== undefined) { + content_lines.push( + lib_plankton.string.coin( + "URL:{{url}}", + { + "url": vevent.url, + } + ) + ); + } + */ + } + \array_push($content_lines, 'END:VEVENT'); + } + \array_push($content_lines, 'END:VCALENDAR'); + + $lines = []; + foreach ($content_lines as $content_line) + { + $slices = \davigil\helpers\string_\slice($content_line, 75 - 1); + \array_push($lines, $slices[0]); + foreach (\array_slice($slices, 1) as $slice) + { + \array_push($lines, ' ' . $slice); + } + } + + return \implode("\r\n", $lines); +} + + +/** + */ +function datetime_to_unix_timestamp( + struct_datetime $datetime +) : int +{ + return \davigil\helpers\pit\pit_to_unix_timestamp( + \davigil\helpers\pit\pit_from_datetime( + new \davigil\helpers\pit\struct_datetime( + 0, + new \davigil\helpers\pit\struct_date( + $datetime->date->year, + $datetime->date->month, + $datetime->date->day + ), + ( + ($datetime->time === null) + ? + null + : + new \davigil\helpers\pit\struct_time( + $datetime->time->hour, + $datetime->time->minute, + $datetime->time->second + ) + ) + ) + ) + ); +} + + +/** + */ +function datetime_from_unix_timestamp( + int $unix_timestamp +) : struct_datetime +{ + $pit = \davigil\helpers\pit\pit_from_unix_timestamp($unix_timestamp); + $datetime = \davigil\helpers\pit\pit_to_datetime($pit); + return (new struct_datetime( + new struct_date( + $datetime->date->year, + $datetime->date->month, + $datetime->date->day + ), + ( + ($datetime->time === null) + ? + null + : + new struct_time( + $datetime->time->hour, + $datetime->time->minute, + $datetime->time->second, + true + ) + ) + )); +} + + ?> diff --git a/source/helpers/pit.php b/source/helpers/pit.php new file mode 100644 index 0000000..3c6f668 --- /dev/null +++ b/source/helpers/pit.php @@ -0,0 +1,241 @@ +stamp = $stamp; + } +} + + +/** + */ +class struct_date +{ + public int $year; + public int $month; + public int $day; + + public function __construct( + int $year, + int $month, + int $day, + ) + { + $this->year = $year; + $this->month = $month; + $this->day = $day; + } +} + + +/** + */ +class struct_time +{ + public int $hour; + public int $minute; + public int $second; + + public function __construct( + int $hour, + int $minute, + int $second + ) + { + $this->hour = $hour; + $this->minute = $minute; + $this->second = $second; + } +} + + +/** + */ +class struct_datetime +{ + public int $timezone_shift; + public struct_date $date; + public ?struct_time $time; + + public function __construct( + int $timezone_shift, + struct_date $date, + ?struct_time $time + ) + { + $this->timezone_shift = $timezone_shift; + $this->date = $date; + $this->time = $time; + } +} + + +/** + */ +function date_to_string( + struct_date $date +) : string +{ + return \davigil\helpers\string_\coin( + '{{year}}{{month}}{{day}}', + [ + 'year' => \davigil\helpers\call\convey( + $date->year, + [ + fn($x) => \sprintf('%u', $x), + fn($x) => \str_pad($x, 4, '0', \STR_PAD_LEFT), + ] + ), + 'month' => \davigil\helpers\call\convey( + $date->month, + [ + fn($x) => \sprintf('%u', $x), + fn($x) => \str_pad($x, 2, '0', \STR_PAD_LEFT), + ] + ), + 'day' => \davigil\helpers\call\convey( + $date->day, + [ + fn($x) => \sprintf('%u', $x), + fn($x) => \str_pad($x, 2, '0', \STR_PAD_LEFT), + ] + ), + ] + ); +} + + +/** + */ +function time_to_string( + struct_time $time +) : string +{ + return \davigil\helpers\string_\coin( + '{{hour}}{{minute}}{{second}}', + [ + 'hour' => \davigil\helpers\call\convey( + $time->hour, + [ + fn($x) => \sprintf('%u', $x), + fn($x) => \str_pad($x, 2, '0', \STR_PAD_LEFT), + ] + ), + 'minute' => \davigil\helpers\call\convey( + $time->minute, + [ + fn($x) => \sprintf('%u', $x), + fn($x) => \str_pad($x, 2, '0', \STR_PAD_LEFT), + ] + ), + 'second' => \davigil\helpers\call\convey( + $time->second, + [ + fn($x) => \sprintf('%u', $x), + fn($x) => \str_pad($x, 2, '0', \STR_PAD_LEFT), + ] + ), + ] + ); +} + + +/** + */ +function datetime_to_string( + struct_datetime $datetime +) : string +{ + return \davigil\helpers\string_\coin( + '{{date}}{{macro_time}}', + [ + 'date' => date_to_string($datetime->date), + 'macro_time' => ( + ($datetime->time === null) + ? + '' + : + \davigil\helpers\string_\coin( + 'T{{time}}{{utc}}', + [ + 'time' => time_to_string($datetime->time), + 'utc' => (($datetime->timezone_shift === 0) ? 'Z' : ''), + ] + ) + ), + ] + ); +} + + +/** + */ +function pit_to_unix_timestamp( + struct_pit $pit +) : int +{ + return $pit->stamp; +} + + +/** + */ +function pit_from_unix_timestamp( + int $unix_timestamp +) : struct_pit +{ + return (new struct_pit( + $unix_timestamp + )); +} + + +/** + */ +function pit_from_datetime( + struct_datetime $datetime +) : struct_pit +{ + return (new struct_pit( + \strtotime(datetime_to_string($datetime)) + )); +} + + +/** + */ +function pit_to_datetime( + struct_pit $pit +) : struct_datetime +{ + $str = \date('c', $pit->stamp); + return (new struct_datetime( + 0, + new struct_date( + \intval(\substr($str, 0, 4)), + \intval(\substr($str, 5, 2)), + \intval(\substr($str, 8, 2)), + ), + new struct_time( + \intval(\substr($str, 11, 2)), + \intval(\substr($str, 14, 2)), + \intval(\substr($str, 17, 2)), + ) + )); +} + + ?> diff --git a/source/helpers/string.php b/source/helpers/string.php new file mode 100644 index 0000000..32cfb4a --- /dev/null +++ b/source/helpers/string.php @@ -0,0 +1,39 @@ + $value) + { + $result = \str_replace(\sprintf('{{%s}}', $key), $value, $result); + } + return $result; +} + + +/** + */ +function slice( + string $str, + int $size +) : array +{ + $slices = []; + $rest = $str; + while (\strlen($rest) > 0) + { + \array_push($slices, \substr($rest, 0, $size)); + $rest = \substr($rest, $size); + } + return $slices; +} + + ?> diff --git a/source/main.php b/source/main.php new file mode 100644 index 0000000..400542d --- /dev/null +++ b/source/main.php @@ -0,0 +1,61 @@ +setBaseUri('/'); + + $server->addPlugin( + new \Sabre\DAV\Auth\Plugin( + \davigil\overwrites\make_auth_backend( + $source, + \davigil\conf\get()['auth'] + ) + ) + ); + /** + * this breaks authentication + */ + // $server->addPlugin(new \Sabre\DAVACL\Plugin()); + $server->addPlugin(new \Sabre\CalDAV\Plugin()); + $server->addPlugin(new \Sabre\CalDAV\Subscriptions\Plugin()); + $server->addPlugin(new \Sabre\CalDAV\Schedule\Plugin()); + $server->addPlugin(new \Sabre\DAV\Sync\Plugin()); + $server->addPlugin(new \Sabre\DAV\Sharing\Plugin()); + $server->addPlugin(new \Sabre\CalDAV\SharingPlugin()); + /** + * not required + */ + $server->addPlugin(new \Sabre\DAV\Browser\Plugin()); + + $server->start(); +} + +main(); + + ?> diff --git a/source/overwrites/auths/_factory.php b/source/overwrites/auths/_factory.php new file mode 100644 index 0000000..4f659ff --- /dev/null +++ b/source/overwrites/auths/_factory.php @@ -0,0 +1,37 @@ + diff --git a/source/overwrites/auths/basic.php b/source/overwrites/auths/basic.php new file mode 100644 index 0000000..d4c5b5c --- /dev/null +++ b/source/overwrites/auths/basic.php @@ -0,0 +1,51 @@ +source = $source; + $this->setRealm('davigil'); + } + + + /** + */ + protected function validateUserPass( + /*string */$username, + /*string */$password + )/* : bool*/ + { + $data = $this->source->get( + [ + 'username' => $username, + 'password' => $password, + ] + ); + return ($data !== null); + } + +} + + ?> diff --git a/source/overwrites/auths/none.php b/source/overwrites/auths/none.php new file mode 100644 index 0000000..2c83178 --- /dev/null +++ b/source/overwrites/auths/none.php @@ -0,0 +1,39 @@ + diff --git a/source/overwrites/caldav_backend.php b/source/overwrites/caldav_backend.php new file mode 100644 index 0000000..45f20a4 --- /dev/null +++ b/source/overwrites/caldav_backend.php @@ -0,0 +1,295 @@ +source = $source; + } + + + /** + */ + private function hash_tag( + string $tag + ) : string + { + return \hash('sha256', $tag); + } + + + /** + */ + private function encode_tag( + string $tag + ) : string + { + return \davigil\helpers\call\convey( + $tag, + [ + fn($x) => \strtolower($x), + fn($x) => \preg_replace('/ä/', 'ae', $x), + fn($x) => \preg_replace('/ö/', 'oe', $x), + fn($x) => \preg_replace('/ü/', 'ue', $x), + fn($x) => \preg_replace('/ß/', 'sz', $x), + fn($x) => \preg_replace('/\-/', '', $x), + fn($x) => \preg_replace('/ /', '-', $x), + fn($x) => \preg_replace('/[^a-zA-Z0-9_\-]/s', '_', $x), + ] + ); + } + + + /** + */ + public function getCalendarsForUser( + $principalUri + ) + { + $data = $this->source->get([]); + $tags = []; + foreach ($data as $entry) + { + foreach ($entry['tags'] as $tag) + { + $tags[$tag] = null; + } + } + $result = \array_map( + fn($tag) => [ + 'id' => $this->hash_tag($tag), + 'uri' => $this->encode_tag($tag), + 'principaluri' => $principalUri, + '{DAV:}displayname' => $tag, + \sprintf('{%s}supported-calendar-component-set', \Sabre\CalDAV\Plugin::NS_CALDAV) => new \Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet(['VEVENT']), + ], + \array_keys($tags) + ); + // \error_log(\json_encode($result, \JSON_PRETTY_PRINT)); + return $result; + } + + + /** + */ + public function createCalendar( + $principalUri, + $calendarUri, + array $properties + ) + { + throw (new \Exception('not implemented: createCalendar')); + } + + + /** + */ + public function deleteCalendar( + $calendarId + ) + { + throw (new \Exception('not implemented: deleteCalendar')); + } + + + /** + */ + public function getCalendarObjects( + $calendarId + ) + { + $tag = $calendarId; + $data = $this->source->get([]); + $result = \array_map( + fn($entry) => [ + 'calendarid' => $calendarId, + 'id' => $entry['id'], + // 'uri' => \sprintf('%s.ics', $entry['id']), + 'uri' => $entry['id'], + 'lastmodified' => \time(), + // 'etag' => null, + // 'size' => null, + 'component' => 'vevent', + '{DAV:}displayname' => $entry['title'], + ], + \array_values( + \array_filter( + $data, + fn($entry) => \in_array( + $tag, + \array_map( + fn($x) => $this->hash_tag($x), + $entry['tags'] + ) + ) + ) + ) + ); + return $result; + } + + + /** + * @todo outsource + */ + private function event_to_vevent( + array $event + ) : \davigil\helpers\ics\struct_vevent + { + $vevent = new \davigil\helpers\ics\struct_vevent(); + { + $vevent->uid = $event['id']; + $vevent->dtstamp = \davigil\helpers\ics\datetime_from_unix_timestamp($event['begin']); + $vevent->dtstart = new \davigil\helpers\ics\struct_dt( + '', + \davigil\helpers\ics\datetime_from_unix_timestamp($event['begin']) + ); + $vevent->dtend = ( + ($event['end'] === null) + ? + null + : + new \davigil\helpers\ics\struct_dt( + '', + \davigil\helpers\ics\datetime_from_unix_timestamp($event['end']) + ) + ); + $vevent->summary = $event['title']; + $vevent->location = $event['location']; + $vevent->description = $event['description']; + $vevent->class = \davigil\helpers\ics\enum_class::public_; + } + return $vevent; + } + + + /** + * @todo outsource + */ + private function events_to_vcalendar( + array $events + ) : \davigil\helpers\ics\struct_vcalendar + { + $vcalendar = new \davigil\helpers\ics\struct_vcalendar(); + { + $vcalendar->version = '2.0'; + /** + * @todo conf + */ + $vcalendar->prodid = 'davigil'; + $vcalendar->method = 'PUBLISH'; + $vcalendar->events = \array_map( + fn($event) => $this->event_to_vevent($event), + $events + ); + } + return $vcalendar; + } + + + /** + */ + public function getCalendarObject( + $calendarId, + $objectUri + ) + { + $id = $objectUri; + $data = $this->source->get([]); + $entries = \array_values( + \array_filter( + $data, + fn($entry) => ($entry['id'] === $id) + ) + ); + if (\count($entries) !== 1) + { + throw (new \Exception(\sprintf('not found or ambiguous'))); + } + else + { + $vcalendar = $this->events_to_vcalendar($entries); + $ics = \davigil\helpers\ics\vcalendar_encode($vcalendar); + return [ + 'calendardata' => $ics, + 'uri' => $objectUri, + /** + * @todo + */ + 'lastmodified' => \time(), + /** + * @todo + */ + // 'etag' => '""', + /** + * @todo + */ + // 'size' => 1, + 'component' => 'vcalendar', + ]; + } + } + + + /** + */ + public function createCalendarObject( + $calendarId, + $objectUri, + $calendarData + ) + { + throw (new \Exception('not implemented: createCalendarObject')); + } + + + /** + */ + public function updateCalendarObject( + $calendarId, + $objectUri, + $calendarData + ) + { + throw (new \Exception('not implemented: updateCalendarObject')); + } + + + /** + */ + public function deleteCalendarObject( + $calendarId, + $objectUri + ) + { + throw (new \Exception('not implemented: deleteCalendarObject')); + } + +} + + ?> diff --git a/source/overwrites/principal_backend.php b/source/overwrites/principal_backend.php new file mode 100644 index 0000000..72d3112 --- /dev/null +++ b/source/overwrites/principal_backend.php @@ -0,0 +1,94 @@ + 'principals/dummy', + ] + ]; + */ + } + + + /** + */ + public function getPrincipalByPath( + $path + ) + { + // throw (new \Exception('not implemented: getPrincipalByPath')); + $parts = \explode('/', $path); + $username = $parts[1]; + return [ + 'uri' => $path, + 'displayname' => $username, + ]; + } + + + /** + */ + public function updatePrincipal($path, \Sabre\DAV\PropPatch $propPatch) + { + throw (new \Exception('not implemented: updatePrincipal')); + } + + + /** + */ + public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') + { + throw (new \Exception('not implemented: searchPrincipals')); + } + + + /** + */ + public function findByUri($uri, $principalPrefix) + { + throw (new \Exception('not implemented: findByUri')); + } + + + /** + */ + public function getGroupMemberSet($principal) + { + throw (new \Exception('not implemented: getGroupMemberSet')); + } + + + /** + */ + public function getGroupMembership($principal) + { + return []; + } + + + /** + */ + public function setGroupMemberSet($principal, array $members) + { + throw (new \Exception('not implemented: setGroupMemberSet')); + } + +} + + ?> diff --git a/source/sources/_factory.php b/source/sources/_factory.php new file mode 100644 index 0000000..42b0aeb --- /dev/null +++ b/source/sources/_factory.php @@ -0,0 +1,36 @@ + + * } + */ +function make( + array $descriptor +) : interface_source +{ + switch ($descriptor['kind']) + { + case 'ics_feed': + { + return (new class_source_ics_feed( + $descriptor['data']['url'] + )); + } + default: + { + throw (new \Exception(\sprintf('unhandled source kind: %s', $descriptor['kind']))); + break; + } + } +} + + diff --git a/source/sources/_interface.php b/source/sources/_interface.php new file mode 100644 index 0000000..ff89e9e --- /dev/null +++ b/source/sources/_interface.php @@ -0,0 +1,39 @@ + + * } + * @return array { + * list< + * record< + * id:string, + * title:string, + * begin:type_unix_timestamp, + * end:(null|type_unix_timestamp), + * location:(null|string), + * description:(null|string), + * tags:list, + * > + * > + * } + */ + public function get( + array $parameters + ) : array + ; + +} + + ?> diff --git a/source/sources/ics_feed.php b/source/sources/ics_feed.php new file mode 100644 index 0000000..0175389 --- /dev/null +++ b/source/sources/ics_feed.php @@ -0,0 +1,115 @@ +url = $url; + $this->cache_file = new \davigil\helpers\cache\class_cache_encoded( + new \davigil\helpers\cache\class_cache_file('data'), + fn($value) => \json_encode($value), + fn($value_encoded) => \json_decode($value_encoded, true) + ); + $this->cache_memory = new \davigil\helpers\cache\class_cache_memory(); + } + + + /** + */ + private function retrieve( + ) : array + { + $client = new \Sabre\HTTP\Client(); + $request = new \Sabre\HTTP\Request( + 'GET', + $this->url, + [], + null + ); + $response = $client->send($request); + $status_code = $response->getStatus(); + switch ($status_code) + { + case 200: + { + $ics = $response->getBody(); + $vcalendar = \davigil\helpers\ics\vcalendar_decode($ics); + $data = \array_map( + fn($vevent) => [ + 'id' => $vevent->uid, + 'title' => $vevent->summary, + 'begin' => \davigil\helpers\ics\datetime_to_unix_timestamp($vevent->dtstart->value), + 'end' => ( + ($vevent->dtend === null) + ? + null + : + \davigil\helpers\ics\datetime_to_unix_timestamp($vevent->dtend->value) + ), + 'location' => $vevent->location, + 'description' => $vevent->description, + 'tags' => $vevent->categories, + ], + $vcalendar->events + ); + return $data; + break; + } + default: + { + throw (new \Exception(\sprintf('unhandled response status code: %u', $status_code))); + break; + } + } + } + + + /** + * [implementation] + */ + public function get( + array $parameters + ) : array + { + $key = $this->url; + return \davigil\helpers\cache\get( + $this->cache_memory, + $key, + fn() => \davigil\helpers\cache\get( + $this->cache_file, + $key, + fn() => $this->retrieve(), + [ + 'ttl' => (60 * 5), + ] + ), + [ + 'ttl' => null, + ] + ); + } + +} + + ?> diff --git a/tools/build b/tools/build new file mode 100755 index 0000000..330f7e1 --- /dev/null +++ b/tools/build @@ -0,0 +1,23 @@ +#!/usr/bin/env sh + +## consts + +dir_lib=lib +dir_source=source +dir_build=build +name=index.php + +## exec + +mkdir -p ${dir_build} +cp -r ${dir_lib}/composer/* ${dir_build}/ + +mkdir -p ${dir_build}/data +mkdir -p ${dir_build}/public + +rm -f ${dir_build}/${name} +cp -r -u -v ${dir_source}/* ${dir_build}/ +ln -s main.php ${dir_build}/index.php + +cp conf/example.json ${dir_build}/conf.json + diff --git a/tools/run b/tools/run new file mode 100755 index 0000000..34c99ec --- /dev/null +++ b/tools/run @@ -0,0 +1,4 @@ +#!/usr/bin/env sh + +php -S localhost:8000 -t build + diff --git a/tools/update-libs b/tools/update-libs new file mode 100755 index 0000000..95cb20f --- /dev/null +++ b/tools/update-libs @@ -0,0 +1,8 @@ +#!/usr/bin/env sh + +mkdir -p lib/composer +cd lib/composer +composer require \ + sabre/dav \ + johngrogg/ics-parser +cd -