From 3eee68c40491bc453e681a7ec27fd5236430f8aa Mon Sep 17 00:00:00 2001 From: Laurent Destailleur Date: Tue, 27 Mar 2018 16:15:19 +0200 Subject: [PATCH] NEW Prototype of a module DAV --- build/rpm/dolibarr_fedora.spec | 1 + build/rpm/dolibarr_generic.spec | 1 + build/rpm/dolibarr_mandriva.spec | 1 + build/rpm/dolibarr_opensuse.spec | 1 + htdocs/core/modules/modDav.class.php | 340 ++ htdocs/dav/fileserver.php | 103 + htdocs/includes/sabre/autoload.php | 7 + .../includes/sabre/composer/ClassLoader.php | 441 +++ htdocs/includes/sabre/composer/LICENSE | 21 + .../sabre/composer/autoload_classmap.php | 9 + .../sabre/composer/autoload_files.php | 16 + .../sabre/composer/autoload_namespaces.php | 9 + .../includes/sabre/composer/autoload_psr4.php | 19 + .../includes/sabre/composer/autoload_real.php | 70 + .../sabre/composer/autoload_static.php | 89 + htdocs/includes/sabre/composer/installed.json | 470 +++ htdocs/includes/sabre/psr/log/.gitignore | 1 + htdocs/includes/sabre/psr/log/LICENSE | 19 + .../sabre/psr/log/Psr/Log/AbstractLogger.php | 128 + .../log/Psr/Log/InvalidArgumentException.php | 7 + .../sabre/psr/log/Psr/Log/LogLevel.php | 18 + .../psr/log/Psr/Log/LoggerAwareInterface.php | 18 + .../psr/log/Psr/Log/LoggerAwareTrait.php | 26 + .../sabre/psr/log/Psr/Log/LoggerInterface.php | 123 + .../sabre/psr/log/Psr/Log/LoggerTrait.php | 140 + .../sabre/psr/log/Psr/Log/NullLogger.php | 28 + .../log/Psr/Log/Test/LoggerInterfaceTest.php | 140 + htdocs/includes/sabre/psr/log/README.md | 45 + htdocs/includes/sabre/psr/log/composer.json | 26 + htdocs/includes/sabre/sabre/dav/.gitignore | 43 + htdocs/includes/sabre/sabre/dav/.travis.yml | 36 + .../includes/sabre/sabre/dav/CONTRIBUTING.md | 87 + htdocs/includes/sabre/sabre/dav/bin/build.php | 177 + .../sabre/sabre/dav/bin/googlecode_upload.py | 248 ++ .../sabre/sabre/dav/bin/migrateto20.php | 453 +++ .../sabre/sabre/dav/bin/migrateto21.php | 176 + .../sabre/sabre/dav/bin/migrateto30.php | 171 + .../sabre/sabre/dav/bin/migrateto32.php | 268 ++ .../sabre/sabre/dav/bin/naturalselection | 140 + htdocs/includes/sabre/sabre/dav/bin/sabredav | 2 + .../includes/sabre/sabre/dav/bin/sabredav.php | 53 + htdocs/includes/sabre/sabre/dav/composer.json | 68 + .../lib/CalDAV/Backend/AbstractBackend.php | 226 ++ .../lib/CalDAV/Backend/BackendInterface.php | 270 ++ .../CalDAV/Backend/NotificationSupport.php | 61 + .../sabre/dav/lib/CalDAV/Backend/PDO.php | 1511 +++++++++ .../lib/CalDAV/Backend/SchedulingSupport.php | 65 + .../dav/lib/CalDAV/Backend/SharingSupport.php | 60 + .../dav/lib/CalDAV/Backend/SimplePDO.php | 296 ++ .../CalDAV/Backend/SubscriptionSupport.php | 89 + .../dav/lib/CalDAV/Backend/SyncSupport.php | 81 + .../sabre/sabre/dav/lib/CalDAV/Calendar.php | 472 +++ .../sabre/dav/lib/CalDAV/CalendarHome.php | 378 +++ .../sabre/dav/lib/CalDAV/CalendarObject.php | 237 ++ .../dav/lib/CalDAV/CalendarQueryValidator.php | 375 +++ .../sabre/dav/lib/CalDAV/CalendarRoot.php | 80 + .../CalDAV/Exception/InvalidComponentType.php | 35 + .../sabre/dav/lib/CalDAV/ICSExportPlugin.php | 378 +++ .../sabre/sabre/dav/lib/CalDAV/ICalendar.php | 18 + .../sabre/dav/lib/CalDAV/ICalendarObject.php | 21 + .../lib/CalDAV/ICalendarObjectContainer.php | 39 + .../sabre/dav/lib/CalDAV/ISharedCalendar.php | 26 + .../lib/CalDAV/Notifications/Collection.php | 101 + .../lib/CalDAV/Notifications/ICollection.php | 23 + .../dav/lib/CalDAV/Notifications/INode.php | 40 + .../dav/lib/CalDAV/Notifications/Node.php | 121 + .../dav/lib/CalDAV/Notifications/Plugin.php | 180 + .../sabre/sabre/dav/lib/CalDAV/Plugin.php | 1068 ++++++ .../dav/lib/CalDAV/Principal/Collection.php | 33 + .../dav/lib/CalDAV/Principal/IProxyRead.php | 19 + .../dav/lib/CalDAV/Principal/IProxyWrite.php | 19 + .../dav/lib/CalDAV/Principal/ProxyRead.php | 181 ++ .../dav/lib/CalDAV/Principal/ProxyWrite.php | 181 ++ .../sabre/dav/lib/CalDAV/Principal/User.php | 135 + .../sabre/dav/lib/CalDAV/Schedule/IInbox.php | 15 + .../dav/lib/CalDAV/Schedule/IMipPlugin.php | 190 ++ .../sabre/dav/lib/CalDAV/Schedule/IOutbox.php | 15 + .../lib/CalDAV/Schedule/ISchedulingObject.php | 13 + .../sabre/dav/lib/CalDAV/Schedule/Inbox.php | 203 ++ .../sabre/dav/lib/CalDAV/Schedule/Outbox.php | 123 + .../sabre/dav/lib/CalDAV/Schedule/Plugin.php | 1066 ++++++ .../lib/CalDAV/Schedule/SchedulingObject.php | 155 + .../sabre/dav/lib/CalDAV/SharedCalendar.php | 229 ++ .../sabre/dav/lib/CalDAV/SharingPlugin.php | 401 +++ .../CalDAV/Subscriptions/ISubscription.php | 40 + .../dav/lib/CalDAV/Subscriptions/Plugin.php | 120 + .../lib/CalDAV/Subscriptions/Subscription.php | 221 ++ .../lib/CalDAV/Xml/Filter/CalendarData.php | 84 + .../dav/lib/CalDAV/Xml/Filter/CompFilter.php | 97 + .../dav/lib/CalDAV/Xml/Filter/ParamFilter.php | 82 + .../dav/lib/CalDAV/Xml/Filter/PropFilter.php | 98 + .../lib/CalDAV/Xml/Notification/Invite.php | 302 ++ .../CalDAV/Xml/Notification/InviteReply.php | 213 ++ .../Notification/NotificationInterface.php | 45 + .../CalDAV/Xml/Notification/SystemStatus.php | 182 ++ .../Xml/Property/AllowedSharingModes.php | 87 + .../CalDAV/Xml/Property/EmailAddressSet.php | 80 + .../dav/lib/CalDAV/Xml/Property/Invite.php | 128 + .../Xml/Property/ScheduleCalendarTransp.php | 130 + .../SupportedCalendarComponentSet.php | 129 + .../Xml/Property/SupportedCalendarData.php | 60 + .../Xml/Property/SupportedCollationSet.php | 57 + .../Xml/Request/CalendarMultiGetReport.php | 124 + .../Xml/Request/CalendarQueryReport.php | 139 + .../Xml/Request/FreeBusyQueryReport.php | 91 + .../lib/CalDAV/Xml/Request/InviteReply.php | 150 + .../dav/lib/CalDAV/Xml/Request/MkCalendar.php | 79 + .../dav/lib/CalDAV/Xml/Request/Share.php | 111 + .../sabre/dav/lib/CardDAV/AddressBook.php | 357 ++ .../sabre/dav/lib/CardDAV/AddressBookHome.php | 191 ++ .../sabre/dav/lib/CardDAV/AddressBookRoot.php | 80 + .../lib/CardDAV/Backend/AbstractBackend.php | 38 + .../lib/CardDAV/Backend/BackendInterface.php | 190 ++ .../sabre/dav/lib/CardDAV/Backend/PDO.php | 550 ++++ .../dav/lib/CardDAV/Backend/SyncSupport.php | 81 + .../sabre/sabre/dav/lib/CardDAV/Card.php | 216 ++ .../sabre/dav/lib/CardDAV/IAddressBook.php | 18 + .../sabre/sabre/dav/lib/CardDAV/ICard.php | 19 + .../sabre/dav/lib/CardDAV/IDirectory.php | 20 + .../sabre/sabre/dav/lib/CardDAV/Plugin.php | 940 ++++++ .../sabre/dav/lib/CardDAV/VCFExportPlugin.php | 172 + .../lib/CardDAV/Xml/Filter/AddressData.php | 63 + .../lib/CardDAV/Xml/Filter/ParamFilter.php | 89 + .../dav/lib/CardDAV/Xml/Filter/PropFilter.php | 98 + .../Xml/Property/SupportedAddressData.php | 83 + .../Xml/Property/SupportedCollationSet.php | 47 + .../Xml/Request/AddressBookMultiGetReport.php | 113 + .../Xml/Request/AddressBookQueryReport.php | 199 ++ .../lib/DAV/Auth/Backend/AbstractBasic.php | 144 + .../lib/DAV/Auth/Backend/AbstractBearer.php | 138 + .../lib/DAV/Auth/Backend/AbstractDigest.php | 168 + .../sabre/dav/lib/DAV/Auth/Backend/Apache.php | 96 + .../lib/DAV/Auth/Backend/BackendInterface.php | 70 + .../lib/DAV/Auth/Backend/BasicCallBack.php | 58 + .../sabre/dav/lib/DAV/Auth/Backend/File.php | 77 + .../sabre/dav/lib/DAV/Auth/Backend/PDO.php | 57 + .../sabre/sabre/dav/lib/DAV/Auth/Plugin.php | 285 ++ .../dav/lib/DAV/Browser/GuessContentType.php | 101 + .../sabre/dav/lib/DAV/Browser/HtmlOutput.php | 34 + .../dav/lib/DAV/Browser/HtmlOutputHelper.php | 117 + .../dav/lib/DAV/Browser/MapGetToPropFind.php | 60 + .../sabre/dav/lib/DAV/Browser/Plugin.php | 802 +++++ .../sabre/dav/lib/DAV/Browser/PropFindAll.php | 132 + .../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 .../sabre/sabre/dav/lib/DAV/Client.php | 439 +++ .../sabre/sabre/dav/lib/DAV/Collection.php | 109 + .../sabre/sabre/dav/lib/DAV/CorePlugin.php | 959 ++++++ .../sabre/sabre/dav/lib/DAV/Exception.php | 57 + .../dav/lib/DAV/Exception/BadRequest.php | 30 + .../sabre/dav/lib/DAV/Exception/Conflict.php | 30 + .../dav/lib/DAV/Exception/ConflictingLock.php | 36 + .../sabre/dav/lib/DAV/Exception/Forbidden.php | 29 + .../lib/DAV/Exception/InsufficientStorage.php | 29 + .../lib/DAV/Exception/InvalidResourceType.php | 33 + .../lib/DAV/Exception/InvalidSyncToken.php | 38 + .../dav/lib/DAV/Exception/LengthRequired.php | 30 + .../Exception/LockTokenMatchesRequestUri.php | 41 + .../sabre/dav/lib/DAV/Exception/Locked.php | 72 + .../lib/DAV/Exception/MethodNotAllowed.php | 47 + .../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 | 71 + .../lib/DAV/Exception/ReportNotSupported.php | 32 + .../RequestedRangeNotSatisfiable.php | 30 + .../lib/DAV/Exception/ServiceUnavailable.php | 30 + .../dav/lib/DAV/Exception/TooManyMatches.php | 38 + .../DAV/Exception/UnsupportedMediaType.php | 30 + .../sabre/sabre/dav/lib/DAV/FS/Directory.php | 151 + .../sabre/sabre/dav/lib/DAV/FS/File.php | 95 + .../sabre/sabre/dav/lib/DAV/FS/Node.php | 80 + .../sabre/dav/lib/DAV/FSExt/Directory.php | 211 ++ .../sabre/sabre/dav/lib/DAV/FSExt/File.php | 152 + .../includes/sabre/sabre/dav/lib/DAV/File.php | 96 + .../sabre/sabre/dav/lib/DAV/ICollection.php | 76 + .../sabre/dav/lib/DAV/IExtendedCollection.php | 43 + .../sabre/sabre/dav/lib/DAV/IFile.php | 81 + .../sabre/sabre/dav/lib/DAV/IMoveTarget.php | 44 + .../sabre/sabre/dav/lib/DAV/IMultiGet.php | 36 + .../sabre/sabre/dav/lib/DAV/INode.php | 46 + .../sabre/sabre/dav/lib/DAV/IProperties.php | 47 + .../sabre/sabre/dav/lib/DAV/IQuota.php | 26 + .../lib/DAV/Locks/Backend/AbstractBackend.php | 18 + .../DAV/Locks/Backend/BackendInterface.php | 50 + .../sabre/dav/lib/DAV/Locks/Backend/File.php | 185 ++ .../sabre/dav/lib/DAV/Locks/Backend/PDO.php | 180 + .../sabre/dav/lib/DAV/Locks/LockInfo.php | 80 + .../sabre/sabre/dav/lib/DAV/Locks/Plugin.php | 589 ++++ .../sabre/sabre/dav/lib/DAV/MkCol.php | 72 + .../sabre/sabre/dav/lib/DAV/Mount/Plugin.php | 86 + .../includes/sabre/sabre/dav/lib/DAV/Node.php | 54 + .../lib/DAV/PartialUpdate/IPatchSupport.php | 47 + .../dav/lib/DAV/PartialUpdate/Plugin.php | 215 ++ .../sabre/sabre/dav/lib/DAV/PropFind.php | 347 ++ .../sabre/sabre/dav/lib/DAV/PropPatch.php | 373 +++ .../Backend/BackendInterface.php | 80 + .../lib/DAV/PropertyStorage/Backend/PDO.php | 246 ++ .../dav/lib/DAV/PropertyStorage/Plugin.php | 185 ++ .../sabre/sabre/dav/lib/DAV/Server.php | 1687 ++++++++++ .../sabre/sabre/dav/lib/DAV/ServerPlugin.php | 110 + .../sabre/dav/lib/DAV/Sharing/ISharedNode.php | 69 + .../sabre/dav/lib/DAV/Sharing/Plugin.php | 342 ++ .../sabre/dav/lib/DAV/SimpleCollection.php | 107 + .../sabre/sabre/dav/lib/DAV/SimpleFile.php | 121 + .../sabre/sabre/dav/lib/DAV/StringUtil.php | 91 + .../dav/lib/DAV/Sync/ISyncCollection.php | 88 + .../sabre/sabre/dav/lib/DAV/Sync/Plugin.php | 277 ++ .../dav/lib/DAV/TemporaryFileFilterPlugin.php | 297 ++ .../includes/sabre/sabre/dav/lib/DAV/Tree.php | 340 ++ .../sabre/sabre/dav/lib/DAV/UUIDUtil.php | 64 + .../sabre/sabre/dav/lib/DAV/Version.php | 19 + .../sabre/dav/lib/DAV/Xml/Element/Prop.php | 116 + .../dav/lib/DAV/Xml/Element/Response.php | 253 ++ .../sabre/dav/lib/DAV/Xml/Element/Sharee.php | 199 ++ .../dav/lib/DAV/Xml/Property/Complex.php | 89 + .../lib/DAV/Xml/Property/GetLastModified.php | 110 + .../sabre/dav/lib/DAV/Xml/Property/Href.php | 165 + .../sabre/dav/lib/DAV/Xml/Property/Invite.php | 70 + .../dav/lib/DAV/Xml/Property/LocalHref.php | 48 + .../lib/DAV/Xml/Property/LockDiscovery.php | 106 + .../dav/lib/DAV/Xml/Property/ResourceType.php | 128 + .../dav/lib/DAV/Xml/Property/ShareAccess.php | 143 + .../lib/DAV/Xml/Property/SupportedLock.php | 54 + .../DAV/Xml/Property/SupportedMethodSet.php | 121 + .../DAV/Xml/Property/SupportedReportSet.php | 154 + .../sabre/dav/lib/DAV/Xml/Request/Lock.php | 81 + .../sabre/dav/lib/DAV/Xml/Request/MkCol.php | 82 + .../dav/lib/DAV/Xml/Request/PropFind.php | 83 + .../dav/lib/DAV/Xml/Request/PropPatch.php | 118 + .../dav/lib/DAV/Xml/Request/ShareResource.php | 81 + .../DAV/Xml/Request/SyncCollectionReport.php | 122 + .../dav/lib/DAV/Xml/Response/MultiStatus.php | 142 + .../sabre/sabre/dav/lib/DAV/Xml/Service.php | 47 + .../sabre/sabre/dav/lib/DAVACL/ACLTrait.php | 100 + .../DAVACL/AbstractPrincipalCollection.php | 181 ++ .../dav/lib/DAVACL/Exception/AceConflict.php | 35 + .../lib/DAVACL/Exception/NeedPrivileges.php | 82 + .../dav/lib/DAVACL/Exception/NoAbstract.php | 35 + .../Exception/NotRecognizedPrincipal.php | 35 + .../Exception/NotSupportedPrivilege.php | 35 + .../sabre/dav/lib/DAVACL/FS/Collection.php | 111 + .../sabre/sabre/dav/lib/DAVACL/FS/File.php | 80 + .../dav/lib/DAVACL/FS/HomeCollection.php | 128 + .../sabre/sabre/dav/lib/DAVACL/IACL.php | 74 + .../sabre/sabre/dav/lib/DAVACL/IPrincipal.php | 77 + .../dav/lib/DAVACL/IPrincipalCollection.php | 62 + .../sabre/sabre/dav/lib/DAVACL/Plugin.php | 1636 ++++++++++ .../sabre/sabre/dav/lib/DAVACL/Principal.php | 221 ++ .../PrincipalBackend/AbstractBackend.php | 53 + .../PrincipalBackend/BackendInterface.php | 141 + .../CreatePrincipalSupport.php | 30 + .../dav/lib/DAVACL/PrincipalBackend/PDO.php | 431 +++ .../dav/lib/DAVACL/PrincipalCollection.php | 98 + .../sabre/dav/lib/DAVACL/Xml/Property/Acl.php | 277 ++ .../DAVACL/Xml/Property/AclRestrictions.php | 45 + .../Xml/Property/CurrentUserPrivilegeSet.php | 159 + .../dav/lib/DAVACL/Xml/Property/Principal.php | 196 ++ .../Xml/Property/SupportedPrivilegeSet.php | 160 + .../Xml/Request/AclPrincipalPropSetReport.php | 67 + .../Xml/Request/ExpandPropertyReport.php | 103 + .../Xml/Request/PrincipalMatchReport.php | 107 + .../Request/PrincipalPropertySearchReport.php | 127 + .../PrincipalSearchPropertySetReport.php | 58 + .../Sabre/CalDAV/Backend/AbstractPDOTest.php | 1431 ++++++++ .../Sabre/CalDAV/Backend/AbstractTest.php | 178 + .../dav/tests/Sabre/CalDAV/Backend/Mock.php | 258 ++ .../Sabre/CalDAV/Backend/MockScheduling.php | 91 + .../Sabre/CalDAV/Backend/MockSharing.php | 204 ++ .../Backend/MockSubscriptionSupport.php | 156 + .../Sabre/CalDAV/Backend/PDOMySQLTest.php | 9 + .../Sabre/CalDAV/Backend/PDOPgSqlTest.php | 9 + .../Sabre/CalDAV/Backend/PDOSqliteTest.php | 9 + .../Sabre/CalDAV/Backend/SimplePDOTest.php | 445 +++ .../CalDAV/CalendarHomeNotificationsTest.php | 49 + .../CalendarHomeSharedCalendarsTest.php | 80 + .../CalDAV/CalendarHomeSubscriptionsTest.php | 85 + .../tests/Sabre/CalDAV/CalendarHomeTest.php | 215 ++ .../tests/Sabre/CalDAV/CalendarObjectTest.php | 383 +++ .../Sabre/CalDAV/CalendarQueryVAlarmTest.php | 122 + .../CalDAV/CalendarQueryValidatorTest.php | 829 +++++ .../dav/tests/Sabre/CalDAV/CalendarTest.php | 256 ++ .../ExpandEventsDTSTARTandDTENDTest.php | 113 + .../ExpandEventsDTSTARTandDTENDbyDayTest.php | 102 + .../CalDAV/ExpandEventsDoubleEventsTest.php | 103 + .../CalDAV/ExpandEventsFloatingTimeTest.php | 207 ++ .../tests/Sabre/CalDAV/FreeBusyReportTest.php | 174 + .../Sabre/CalDAV/GetEventsByTimerangeTest.php | 82 + .../Sabre/CalDAV/ICSExportPluginTest.php | 386 +++ .../dav/tests/Sabre/CalDAV/Issue166Test.php | 63 + .../dav/tests/Sabre/CalDAV/Issue172Test.php | 135 + .../dav/tests/Sabre/CalDAV/Issue203Test.php | 137 + .../dav/tests/Sabre/CalDAV/Issue205Test.php | 98 + .../dav/tests/Sabre/CalDAV/Issue211Test.php | 89 + .../dav/tests/Sabre/CalDAV/Issue220Test.php | 99 + .../dav/tests/Sabre/CalDAV/Issue228Test.php | 79 + .../tests/Sabre/CalDAV/JCalTransformTest.php | 262 ++ .../CalDAV/Notifications/CollectionTest.php | 85 + .../Sabre/CalDAV/Notifications/NodeTest.php | 96 + .../Sabre/CalDAV/Notifications/PluginTest.php | 168 + .../dav/tests/Sabre/CalDAV/PluginTest.php | 1086 +++++++ .../Sabre/CalDAV/Principal/CollectionTest.php | 20 + .../Sabre/CalDAV/Principal/ProxyReadTest.php | 102 + .../Sabre/CalDAV/Principal/ProxyWriteTest.php | 40 + .../tests/Sabre/CalDAV/Principal/UserTest.php | 127 + .../CalDAV/Schedule/DeliverNewEventTest.php | 92 + .../CalDAV/Schedule/FreeBusyRequestTest.php | 611 ++++ .../Sabre/CalDAV/Schedule/IMip/MockPlugin.php | 50 + .../Sabre/CalDAV/Schedule/IMipPluginTest.php | 221 ++ .../tests/Sabre/CalDAV/Schedule/InboxTest.php | 136 + .../Sabre/CalDAV/Schedule/OutboxPostTest.php | 134 + .../Sabre/CalDAV/Schedule/OutboxTest.php | 48 + .../Sabre/CalDAV/Schedule/PluginBasicTest.php | 39 + .../CalDAV/Schedule/PluginPropertiesTest.php | 146 + ...PluginPropertiesWithSharedCalendarTest.php | 71 + .../CalDAV/Schedule/ScheduleDeliverTest.php | 666 ++++ .../CalDAV/Schedule/SchedulingObjectTest.php | 378 +++ .../tests/Sabre/CalDAV/SharedCalendarTest.php | 176 + .../tests/Sabre/CalDAV/SharingPluginTest.php | 396 +++ .../Subscriptions/CreateSubscriptionTest.php | 123 + .../Sabre/CalDAV/Subscriptions/PluginTest.php | 50 + .../CalDAV/Subscriptions/SubscriptionTest.php | 131 + .../sabre/dav/tests/Sabre/CalDAV/TestUtil.php | 189 ++ .../tests/Sabre/CalDAV/ValidateICalTest.php | 406 +++ .../Xml/Notification/InviteReplyTest.php | 146 + .../CalDAV/Xml/Notification/InviteTest.php | 165 + .../Xml/Notification/SystemStatusTest.php | 69 + .../Xml/Property/AllowedSharingModesTest.php | 38 + .../Xml/Property/EmailAddressSetTest.php | 40 + .../Sabre/CalDAV/Xml/Property/InviteTest.php | 112 + .../Property/ScheduleCalendarTranspTest.php | 118 + .../SupportedCalendarComponentSetTest.php | 102 + .../Property/SupportedCalendarDataTest.php | 36 + .../Property/SupportedCollationSetTest.php | 37 + .../Xml/Request/CalendarQueryReportTest.php | 369 +++ .../CalDAV/Xml/Request/InviteReplyTest.php | 78 + .../Sabre/CalDAV/Xml/Request/ShareTest.php | 83 + .../Sabre/CardDAV/AbstractPluginTest.php | 43 + .../Sabre/CardDAV/AddressBookHomeTest.php | 159 + .../Sabre/CardDAV/AddressBookQueryTest.php | 355 ++ .../Sabre/CardDAV/AddressBookRootTest.php | 31 + .../tests/Sabre/CardDAV/AddressBookTest.php | 194 ++ .../Sabre/CardDAV/Backend/AbstractPDOTest.php | 373 +++ .../dav/tests/Sabre/CardDAV/Backend/Mock.php | 258 ++ .../Sabre/CardDAV/Backend/PDOMySQLTest.php | 9 + .../Sabre/CardDAV/Backend/PDOPgSqlTest.php | 9 + .../Sabre/CardDAV/Backend/PDOSqliteTest.php | 9 + .../dav/tests/Sabre/CardDAV/CardTest.php | 210 ++ .../tests/Sabre/CardDAV/IDirectoryTest.php | 30 + .../dav/tests/Sabre/CardDAV/MultiGetTest.php | 99 + .../dav/tests/Sabre/CardDAV/PluginTest.php | 102 + .../CardDAV/SogoStripContentTypeTest.php | 56 + .../dav/tests/Sabre/CardDAV/TestUtil.php | 62 + .../dav/tests/Sabre/CardDAV/VCFExportTest.php | 135 + .../Sabre/CardDAV/ValidateFilterTest.php | 209 ++ .../tests/Sabre/CardDAV/ValidateVCardTest.php | 305 ++ .../Xml/Property/SupportedAddressDataTest.php | 38 + .../Property/SupportedCollationSetTest.php | 38 + .../Xml/Request/AddressBookMultiGetTest.php | 47 + .../Request/AddressBookQueryReportTest.php | 350 ++ .../dav/tests/Sabre/DAV/AbstractServer.php | 64 + .../DAV/Auth/Backend/AbstractBasicTest.php | 91 + .../DAV/Auth/Backend/AbstractBearerTest.php | 90 + .../DAV/Auth/Backend/AbstractDigestTest.php | 138 + .../DAV/Auth/Backend/AbstractPDOTest.php | 45 + .../Sabre/DAV/Auth/Backend/ApacheTest.php | 71 + .../DAV/Auth/Backend/BasicCallBackTest.php | 36 + .../tests/Sabre/DAV/Auth/Backend/FileTest.php | 41 + .../dav/tests/Sabre/DAV/Auth/Backend/Mock.php | 87 + .../Sabre/DAV/Auth/Backend/PDOMySQLTest.php | 9 + .../Sabre/DAV/Auth/Backend/PDOPgSqlTest.php | 9 + .../Sabre/DAV/Auth/Backend/PDOSqliteTest.php | 9 + .../dav/tests/Sabre/DAV/Auth/PluginTest.php | 133 + .../dav/tests/Sabre/DAV/BasicNodeTest.php | 235 ++ .../DAV/Browser/GuessContentTypeTest.php | 70 + .../DAV/Browser/MapGetToPropFindTest.php | 44 + .../tests/Sabre/DAV/Browser/PluginTest.php | 186 ++ .../Sabre/DAV/Browser/PropFindAllTest.php | 70 + .../sabre/dav/tests/Sabre/DAV/ClientMock.php | 34 + .../sabre/dav/tests/Sabre/DAV/ClientTest.php | 306 ++ .../dav/tests/Sabre/DAV/CorePluginTest.php | 14 + .../dav/tests/Sabre/DAV/DbTestHelperTrait.php | 143 + .../tests/Sabre/DAV/Exception/LockedTest.php | 67 + .../DAV/Exception/PaymentRequiredTest.php | 14 + .../DAV/Exception/ServiceUnavailableTest.php | 14 + .../DAV/Exception/TooManyMatchesTest.php | 35 + .../dav/tests/Sabre/DAV/ExceptionTest.php | 30 + .../tests/Sabre/DAV/FSExt/DirectoryTest.php | 30 + .../dav/tests/Sabre/DAV/FSExt/FileTest.php | 110 + .../dav/tests/Sabre/DAV/FSExt/ServerTest.php | 246 ++ .../tests/Sabre/DAV/GetIfConditionsTest.php | 337 ++ .../tests/Sabre/DAV/HTTPPreferParsingTest.php | 188 ++ .../dav/tests/Sabre/DAV/HttpCopyTest.php | 199 ++ .../dav/tests/Sabre/DAV/HttpDeleteTest.php | 137 + .../sabre/dav/tests/Sabre/DAV/HttpGetTest.php | 158 + .../dav/tests/Sabre/DAV/HttpHeadTest.php | 97 + .../dav/tests/Sabre/DAV/HttpMoveTest.php | 119 + .../sabre/dav/tests/Sabre/DAV/HttpPutTest.php | 349 ++ .../sabre/dav/tests/Sabre/DAV/Issue33Test.php | 106 + .../Sabre/DAV/Locks/Backend/AbstractTest.php | 196 ++ .../Sabre/DAV/Locks/Backend/FileTest.php | 24 + .../tests/Sabre/DAV/Locks/Backend/Mock.php | 139 + .../Sabre/DAV/Locks/Backend/PDOMySQLTest.php | 9 + .../Sabre/DAV/Locks/Backend/PDOPgSqlTest.php | 9 + .../Sabre/DAV/Locks/Backend/PDOSqliteTest.php | 9 + .../tests/Sabre/DAV/Locks/Backend/PDOTest.php | 20 + .../dav/tests/Sabre/DAV/Locks/MSWordTest.php | 124 + .../dav/tests/Sabre/DAV/Locks/Plugin2Test.php | 69 + .../dav/tests/Sabre/DAV/Locks/PluginTest.php | 1003 ++++++ .../dav/tests/Sabre/DAV/Mock/Collection.php | 168 + .../sabre/dav/tests/Sabre/DAV/Mock/File.php | 163 + .../Sabre/DAV/Mock/PropertiesCollection.php | 94 + .../dav/tests/Sabre/DAV/Mock/SharedNode.php | 125 + .../tests/Sabre/DAV/Mock/StreamingFile.php | 102 + .../sabre/dav/tests/Sabre/DAV/MockLogger.php | 36 + .../dav/tests/Sabre/DAV/Mount/PluginTest.php | 58 + .../dav/tests/Sabre/DAV/ObjectTreeTest.php | 100 + .../sabre/dav/tests/Sabre/DAV/PSR3Test.php | 87 + .../Sabre/DAV/PartialUpdate/FileMock.php | 122 + .../Sabre/DAV/PartialUpdate/PluginTest.php | 135 + .../DAV/PartialUpdate/SpecificationTest.php | 94 + .../dav/tests/Sabre/DAV/PropFindTest.php | 76 + .../dav/tests/Sabre/DAV/PropPatchTest.php | 351 ++ .../Backend/AbstractPDOTest.php | 193 ++ .../DAV/PropertyStorage/Backend/Mock.php | 117 + .../PropertyStorage/Backend/PDOMysqlTest.php | 9 + .../PropertyStorage/Backend/PDOPgSqlTest.php | 9 + .../PropertyStorage/Backend/PDOSqliteTest.php | 9 + .../Sabre/DAV/PropertyStorage/PluginTest.php | 117 + .../dav/tests/Sabre/DAV/ServerEventsTest.php | 126 + .../dav/tests/Sabre/DAV/ServerMKCOLTest.php | 366 +++ .../dav/tests/Sabre/DAV/ServerPluginTest.php | 108 + .../Sabre/DAV/ServerPreconditionTest.php | 344 ++ .../DAV/ServerPropsInfiniteDepthTest.php | 163 + .../dav/tests/Sabre/DAV/ServerPropsTest.php | 201 ++ .../dav/tests/Sabre/DAV/ServerRangeTest.php | 262 ++ .../dav/tests/Sabre/DAV/ServerSimpleTest.php | 475 +++ .../Sabre/DAV/ServerUpdatePropertiesTest.php | 102 + .../tests/Sabre/DAV/Sharing/PluginTest.php | 190 ++ .../Sabre/DAV/Sharing/ShareResourceTest.php | 210 ++ .../dav/tests/Sabre/DAV/SimpleFileTest.php | 19 + .../dav/tests/Sabre/DAV/StringUtilTest.php | 129 + .../Sabre/DAV/Sync/MockSyncCollection.php | 169 + .../dav/tests/Sabre/DAV/Sync/PluginTest.php | 523 +++ .../tests/Sabre/DAV/SyncTokenPropertyTest.php | 106 + .../Sabre/DAV/TemporaryFileFilterTest.php | 199 ++ .../sabre/dav/tests/Sabre/DAV/TestPlugin.php | 37 + .../sabre/dav/tests/Sabre/DAV/TreeTest.php | 242 ++ .../dav/tests/Sabre/DAV/UUIDUtilTest.php | 25 + .../tests/Sabre/DAV/Xml/Element/PropTest.php | 154 + .../Sabre/DAV/Xml/Element/ResponseTest.php | 313 ++ .../Sabre/DAV/Xml/Element/ShareeTest.php | 98 + .../tests/Sabre/DAV/Xml/Property/HrefTest.php | 109 + .../Sabre/DAV/Xml/Property/InviteTest.php | 76 + .../DAV/Xml/Property/LastModifiedTest.php | 59 + .../Sabre/DAV/Xml/Property/LocalHrefTest.php | 69 + .../DAV/Xml/Property/LockDiscoveryTest.php | 86 + .../DAV/Xml/Property/ShareAccessTest.php | 121 + .../Xml/Property/SupportedMethodSetTest.php | 45 + .../Xml/Property/SupportedReportSetTest.php | 115 + .../Sabre/DAV/Xml/Request/PropFindTest.php | 48 + .../Sabre/DAV/Xml/Request/PropPatchTest.php | 53 + .../DAV/Xml/Request/ShareResourceTest.php | 75 + .../DAV/Xml/Request/SyncCollectionTest.php | 94 + .../sabre/dav/tests/Sabre/DAV/Xml/XmlTest.php | 48 + .../dav/tests/Sabre/DAVACL/ACLMethodTest.php | 337 ++ .../DAVACL/AclPrincipalPropSetReportTest.php | 69 + .../tests/Sabre/DAVACL/AllowAccessTest.php | 132 + .../tests/Sabre/DAVACL/BlockAccessTest.php | 215 ++ .../DAVACL/Exception/AceConflictTest.php | 39 + .../Exception/NeedPrivilegesExceptionTest.php | 49 + .../Sabre/DAVACL/Exception/NoAbstractTest.php | 39 + .../Exception/NotRecognizedPrincipalTest.php | 39 + .../Exception/NotSupportedPrivilegeTest.php | 39 + .../Sabre/DAVACL/ExpandPropertiesTest.php | 317 ++ .../tests/Sabre/DAVACL/FS/CollectionTest.php | 44 + .../dav/tests/Sabre/DAVACL/FS/FileTest.php | 73 + .../Sabre/DAVACL/FS/HomeCollectionTest.php | 116 + .../dav/tests/Sabre/DAVACL/MockACLNode.php | 55 + .../dav/tests/Sabre/DAVACL/MockPrincipal.php | 64 + .../tests/Sabre/DAVACL/PluginAdminTest.php | 79 + .../Sabre/DAVACL/PluginPropertiesTest.php | 415 +++ .../DAVACL/PluginUpdatePropertiesTest.php | 116 + .../PrincipalBackend/AbstractPDOTest.php | 217 ++ .../Sabre/DAVACL/PrincipalBackend/Mock.php | 168 + .../DAVACL/PrincipalBackend/PDOMySQLTest.php | 9 + .../DAVACL/PrincipalBackend/PDOPgSqlTest.php | 9 + .../DAVACL/PrincipalBackend/PDOSqliteTest.php | 9 + .../Sabre/DAVACL/PrincipalCollectionTest.php | 57 + .../tests/Sabre/DAVACL/PrincipalMatchTest.php | 123 + .../DAVACL/PrincipalPropertySearchTest.php | 397 +++ .../DAVACL/PrincipalSearchPropertySetTest.php | 140 + .../dav/tests/Sabre/DAVACL/PrincipalTest.php | 208 ++ .../tests/Sabre/DAVACL/SimplePluginTest.php | 321 ++ .../Sabre/DAVACL/Xml/Property/ACLTest.php | 342 ++ .../Xml/Property/AclRestrictionsTest.php | 30 + .../Property/CurrentUserPrivilegeSetTest.php | 86 + .../DAVACL/Xml/Property/PrincipalTest.php | 191 ++ .../Property/SupportedPrivilegeSetTest.php | 103 + .../Request/AclPrincipalPropSetReportTest.php | 30 + .../Xml/Request/PrincipalMatchReportTest.php | 51 + .../sabre/dav/tests/Sabre/DAVServerTest.php | 306 ++ .../dav/tests/Sabre/HTTP/ResponseMock.php | 22 + .../sabre/dav/tests/Sabre/HTTP/SapiMock.php | 30 + .../sabre/sabre/dav/tests/Sabre/TestUtil.php | 71 + .../sabre/sabre/dav/tests/bootstrap.php | 38 + .../sabre/sabre/dav/tests/phpcs/ruleset.xml | 57 + .../sabre/sabre/dav/tests/phpunit.xml.dist | 46 + htdocs/includes/sabre/sabre/event/.gitignore | 14 + htdocs/includes/sabre/sabre/event/.travis.yml | 26 + .../includes/sabre/sabre/event/CHANGELOG.md | 78 + htdocs/includes/sabre/sabre/event/LICENSE | 27 + htdocs/includes/sabre/sabre/event/README.md | 50 + htdocs/includes/sabre/sabre/event/bin/.empty | 0 .../includes/sabre/sabre/event/composer.json | 47 + .../sabre/sabre/event/examples/promise.php | 100 + .../sabre/sabre/event/examples/tail.php | 28 + .../sabre/sabre/event/lib/EventEmitter.php | 18 + .../sabre/event/lib/EventEmitterInterface.php | 100 + .../sabre/event/lib/EventEmitterTrait.php | 211 ++ .../sabre/sabre/event/lib/Loop/Loop.php | 386 +++ .../sabre/sabre/event/lib/Loop/functions.php | 183 ++ .../sabre/sabre/event/lib/Promise.php | 320 ++ .../sabre/event/lib/Promise/functions.php | 135 + .../lib/PromiseAlreadyResolvedException.php | 15 + .../sabre/sabre/event/lib/Version.php | 19 + .../sabre/sabre/event/lib/coroutine.php | 120 + .../sabre/sabre/event/phpunit.xml.dist | 18 + .../event/tests/ContinueCallbackTest.php | 76 + .../sabre/sabre/event/tests/CoroutineTest.php | 262 ++ .../sabre/event/tests/EventEmitterTest.php | 318 ++ .../sabre/event/tests/Loop/FunctionsTest.php | 160 + .../sabre/sabre/event/tests/Loop/LoopTest.php | 180 + .../event/tests/Promise/FunctionsTest.php | 184 ++ .../sabre/event/tests/Promise/PromiseTest.php | 341 ++ .../sabre/sabre/event/tests/PromiseTest.php | 386 +++ .../sabre/event/tests/benchmark/bench.php | 116 + htdocs/includes/sabre/sabre/http/.gitignore | 15 + htdocs/includes/sabre/sabre/http/.travis.yml | 26 + htdocs/includes/sabre/sabre/http/CHANGELOG.md | 256 ++ htdocs/includes/sabre/sabre/http/LICENSE | 27 + htdocs/includes/sabre/sabre/http/README.md | 746 +++++ htdocs/includes/sabre/sabre/http/bin/.empty | 0 .../includes/sabre/sabre/http/composer.json | 44 + .../sabre/sabre/http/examples/asyncclient.php | 65 + .../sabre/sabre/http/examples/basicauth.php | 55 + .../sabre/sabre/http/examples/client.php | 38 + .../sabre/sabre/http/examples/digestauth.php | 56 + .../sabre/http/examples/reverseproxy.php | 50 + .../sabre/sabre/http/examples/stringify.php | 51 + .../sabre/sabre/http/lib/Auth/AWS.php | 234 ++ .../sabre/http/lib/Auth/AbstractAuth.php | 73 + .../sabre/sabre/http/lib/Auth/Basic.php | 63 + .../sabre/sabre/http/lib/Auth/Bearer.php | 56 + .../sabre/sabre/http/lib/Auth/Digest.php | 231 ++ .../includes/sabre/sabre/http/lib/Client.php | 601 ++++ .../sabre/sabre/http/lib/ClientException.php | 15 + .../sabre/http/lib/ClientHttpException.php | 58 + .../sabre/sabre/http/lib/HttpException.php | 30 + .../includes/sabre/sabre/http/lib/Message.php | 314 ++ .../sabre/http/lib/MessageDecoratorTrait.php | 251 ++ .../sabre/sabre/http/lib/MessageInterface.php | 178 + .../includes/sabre/sabre/http/lib/Request.php | 316 ++ .../sabre/sabre/http/lib/RequestDecorator.php | 231 ++ .../sabre/sabre/http/lib/RequestInterface.php | 147 + .../sabre/sabre/http/lib/Response.php | 193 ++ .../sabre/http/lib/ResponseDecorator.php | 84 + .../sabre/http/lib/ResponseInterface.php | 45 + htdocs/includes/sabre/sabre/http/lib/Sapi.php | 194 ++ .../includes/sabre/sabre/http/lib/URLUtil.php | 103 + htdocs/includes/sabre/sabre/http/lib/Util.php | 74 + .../includes/sabre/sabre/http/lib/Version.php | 19 + .../sabre/sabre/http/lib/functions.php | 445 +++ .../sabre/http/tests/HTTP/Auth/AWSTest.php | 235 ++ .../sabre/http/tests/HTTP/Auth/BasicTest.php | 69 + .../sabre/http/tests/HTTP/Auth/BearerTest.php | 57 + .../sabre/http/tests/HTTP/Auth/DigestTest.php | 191 ++ .../sabre/http/tests/HTTP/ClientTest.php | 474 +++ .../sabre/http/tests/HTTP/FunctionsTest.php | 121 + .../http/tests/HTTP/MessageDecoratorTest.php | 93 + .../sabre/http/tests/HTTP/MessageTest.php | 246 ++ .../http/tests/HTTP/RequestDecoratorTest.php | 112 + .../sabre/http/tests/HTTP/RequestTest.php | 167 + .../http/tests/HTTP/ResponseDecoratorTest.php | 37 + .../sabre/http/tests/HTTP/ResponseTest.php | 48 + .../sabre/sabre/http/tests/HTTP/SapiTest.php | 167 + .../sabre/http/tests/HTTP/URLUtilTest.php | 187 ++ .../sabre/sabre/http/tests/HTTP/UtilTest.php | 206 ++ .../sabre/sabre/http/tests/bootstrap.php | 8 + .../sabre/sabre/http/tests/phpcs/ruleset.xml | 57 + .../sabre/sabre/http/tests/phpunit.xml | 18 + htdocs/includes/sabre/sabre/uri/.gitignore | 13 + htdocs/includes/sabre/sabre/uri/.travis.yml | 14 + htdocs/includes/sabre/sabre/uri/CHANGELOG.md | 51 + htdocs/includes/sabre/sabre/uri/LICENSE | 27 + htdocs/includes/sabre/sabre/uri/README.md | 55 + htdocs/includes/sabre/sabre/uri/composer.json | 41 + .../sabre/uri/lib/InvalidUriException.php | 17 + .../includes/sabre/sabre/uri/lib/Version.php | 19 + .../sabre/sabre/uri/lib/functions.php | 373 +++ .../sabre/sabre/uri/tests/BuildTest.php | 41 + .../sabre/sabre/uri/tests/NormalizeTest.php | 42 + .../sabre/sabre/uri/tests/ParseTest.php | 179 + .../sabre/sabre/uri/tests/ResolveTest.php | 83 + .../sabre/sabre/uri/tests/SplitTest.php | 41 + .../sabre/sabre/uri/tests/phpcs/ruleset.xml | 57 + .../sabre/sabre/uri/tests/phpunit.xml.dist | 18 + .../includes/sabre/sabre/vobject/.gitignore | 21 + .../includes/sabre/sabre/vobject/.travis.yml | 20 + .../includes/sabre/sabre/vobject/CHANGELOG.md | 782 +++++ htdocs/includes/sabre/sabre/vobject/LICENSE | 27 + htdocs/includes/sabre/sabre/vobject/README.md | 55 + .../sabre/sabre/vobject/bin/bench.php | 12 + .../vobject/bin/bench_freebusygenerator.php | 62 + .../vobject/bin/bench_manipulatevcard.php | 69 + .../sabre/vobject/bin/fetch_windows_zones.php | 51 + .../sabre/sabre/vobject/bin/generate_vcards | 241 ++ .../vobject/bin/generateicalendardata.php | 88 + .../sabre/vobject/bin/mergeduplicates.php | 184 ++ .../sabre/sabre/vobject/bin/rrulebench.php | 32 + .../includes/sabre/sabre/vobject/bin/vobject | 27 + .../sabre/sabre/vobject/composer.json | 88 + .../vobject/lib/BirthdayCalendarGenerator.php | 191 ++ .../includes/sabre/sabre/vobject/lib/Cli.php | 771 +++++ .../sabre/sabre/vobject/lib/Component.php | 700 ++++ .../sabre/vobject/lib/Component/Available.php | 126 + .../sabre/vobject/lib/Component/VAlarm.php | 142 + .../vobject/lib/Component/VAvailability.php | 156 + .../sabre/vobject/lib/Component/VCalendar.php | 561 ++++ .../sabre/vobject/lib/Component/VCard.php | 553 ++++ .../sabre/vobject/lib/Component/VEvent.php | 153 + .../sabre/vobject/lib/Component/VFreeBusy.php | 102 + .../sabre/vobject/lib/Component/VJournal.php | 107 + .../sabre/vobject/lib/Component/VTimeZone.php | 66 + .../sabre/vobject/lib/Component/VTodo.php | 193 ++ .../sabre/vobject/lib/DateTimeParser.php | 580 ++++ .../sabre/sabre/vobject/lib/Document.php | 270 ++ .../sabre/sabre/vobject/lib/ElementList.php | 54 + .../sabre/sabre/vobject/lib/EofException.php | 15 + .../sabre/sabre/vobject/lib/FreeBusyData.php | 193 ++ .../sabre/vobject/lib/FreeBusyGenerator.php | 604 ++++ .../sabre/sabre/vobject/lib/ITip/Broker.php | 989 ++++++ .../sabre/vobject/lib/ITip/ITipException.php | 15 + .../sabre/sabre/vobject/lib/ITip/Message.php | 141 + ...SameOrganizerForAllComponentsException.php | 18 + .../vobject/lib/InvalidDataException.php | 14 + .../includes/sabre/sabre/vobject/lib/Node.php | 265 ++ .../sabre/vobject/lib/PHPUnitAssertions.php | 82 + .../sabre/sabre/vobject/lib/Parameter.php | 394 +++ .../sabre/vobject/lib/ParseException.php | 13 + .../sabre/sabre/vobject/lib/Parser/Json.php | 197 ++ .../sabre/vobject/lib/Parser/MimeDir.php | 696 ++++ .../sabre/sabre/vobject/lib/Parser/Parser.php | 80 + .../sabre/sabre/vobject/lib/Parser/XML.php | 428 +++ .../lib/Parser/XML/Element/KeyValue.php | 70 + .../sabre/sabre/vobject/lib/Property.php | 662 ++++ .../sabre/vobject/lib/Property/Binary.php | 128 + .../sabre/vobject/lib/Property/Boolean.php | 84 + .../sabre/vobject/lib/Property/FlatText.php | 50 + .../sabre/vobject/lib/Property/FloatValue.php | 142 + .../lib/Property/ICalendar/CalAddress.php | 61 + .../vobject/lib/Property/ICalendar/Date.php | 18 + .../lib/Property/ICalendar/DateTime.php | 404 +++ .../lib/Property/ICalendar/Duration.php | 85 + .../vobject/lib/Property/ICalendar/Period.php | 155 + .../vobject/lib/Property/ICalendar/Recur.php | 359 ++ .../vobject/lib/Property/IntegerValue.php | 88 + .../sabre/sabre/vobject/lib/Property/Text.php | 413 +++ .../sabre/sabre/vobject/lib/Property/Time.php | 144 + .../sabre/vobject/lib/Property/Unknown.php | 44 + .../sabre/sabre/vobject/lib/Property/Uri.php | 122 + .../sabre/vobject/lib/Property/UtcOffset.php | 77 + .../sabre/vobject/lib/Property/VCard/Date.php | 43 + .../lib/Property/VCard/DateAndOrTime.php | 405 +++ .../vobject/lib/Property/VCard/DateTime.php | 30 + .../lib/Property/VCard/LanguageTag.php | 60 + .../vobject/lib/Property/VCard/TimeStamp.php | 86 + .../sabre/sabre/vobject/lib/Reader.php | 98 + .../sabre/vobject/lib/Recur/EventIterator.php | 513 +++ .../Recur/MaxInstancesExceededException.php | 16 + .../lib/Recur/NoInstancesException.php | 18 + .../sabre/vobject/lib/Recur/RDateIterator.php | 182 ++ .../sabre/vobject/lib/Recur/RRuleIterator.php | 1013 ++++++ .../sabre/sabre/vobject/lib/Settings.php | 56 + .../sabre/vobject/lib/Splitter/ICalendar.php | 113 + .../lib/Splitter/SplitterInterface.php | 39 + .../sabre/vobject/lib/Splitter/VCard.php | 78 + .../sabre/sabre/vobject/lib/StringUtil.php | 66 + .../sabre/sabre/vobject/lib/TimeZoneUtil.php | 276 ++ .../sabre/sabre/vobject/lib/UUIDUtil.php | 69 + .../sabre/vobject/lib/VCardConverter.php | 467 +++ .../sabre/sabre/vobject/lib/Version.php | 19 + .../sabre/sabre/vobject/lib/Writer.php | 81 + .../lib/timezonedata/exchangezones.php | 93 + .../vobject/lib/timezonedata/lotuszones.php | 101 + .../sabre/vobject/lib/timezonedata/php-bc.php | 154 + .../lib/timezonedata/php-workaround.php | 46 + .../vobject/lib/timezonedata/windowszones.php | 143 + .../sabre/vobject/resources/schema/xcal.rng | 1192 +++++++ .../sabre/vobject/resources/schema/xcard.rng | 388 +++ .../vobject/tests/VObject/AttachIssueTest.php | 22 + .../VObject/BirthdayCalendarGeneratorTest.php | 562 ++++ .../sabre/vobject/tests/VObject/CliTest.php | 642 ++++ .../tests/VObject/Component/AvailableTest.php | 73 + .../tests/VObject/Component/VAlarmTest.php | 177 + .../VObject/Component/VAvailabilityTest.php | 490 +++ .../tests/VObject/Component/VCalendarTest.php | 782 +++++ .../tests/VObject/Component/VCardTest.php | 304 ++ .../tests/VObject/Component/VEventTest.php | 95 + .../tests/VObject/Component/VFreeBusyTest.php | 66 + .../tests/VObject/Component/VJournalTest.php | 100 + .../tests/VObject/Component/VTimeZoneTest.php | 56 + .../tests/VObject/Component/VTodoTest.php | 178 + .../vobject/tests/VObject/ComponentTest.php | 527 +++ .../tests/VObject/DateTimeParserTest.php | 699 ++++ .../vobject/tests/VObject/DocumentTest.php | 91 + .../vobject/tests/VObject/ElementListTest.php | 33 + .../vobject/tests/VObject/EmClientTest.php | 56 + .../tests/VObject/EmptyParameterTest.php | 69 + .../tests/VObject/EmptyValueIssueTest.php | 30 + .../tests/VObject/FreeBusyDataTest.php | 318 ++ .../tests/VObject/FreeBusyGeneratorTest.php | 751 +++++ .../tests/VObject/GoogleColonEscapingTest.php | 31 + .../VObject/ICalendar/AttachParseTest.php | 31 + .../VObject/ITip/BrokerAttendeeReplyTest.php | 1146 +++++++ .../VObject/ITip/BrokerDeleteEventTest.php | 344 ++ .../tests/VObject/ITip/BrokerNewEventTest.php | 496 +++ .../VObject/ITip/BrokerProcessMessageTest.php | 164 + .../VObject/ITip/BrokerProcessReplyTest.php | 496 +++ .../tests/VObject/ITip/BrokerTester.php | 96 + ...ezoneInParseEventInfoWithoutMasterTest.php | 77 + .../VObject/ITip/BrokerUpdateEventTest.php | 846 +++++ .../tests/VObject/ITip/EvolutionTest.php | 2653 +++++++++++++++ .../tests/VObject/ITip/MessageTest.php | 32 + .../vobject/tests/VObject/Issue153Test.php | 14 + .../vobject/tests/VObject/Issue259Test.php | 21 + .../tests/VObject/Issue36WorkAroundTest.php | 39 + .../vobject/tests/VObject/Issue40Test.php | 32 + .../vobject/tests/VObject/Issue64Test.php | 19 + .../vobject/tests/VObject/Issue96Test.php | 24 + .../tests/VObject/IssueUndefinedIndexTest.php | 29 + .../sabre/vobject/tests/VObject/JCalTest.php | 149 + .../sabre/vobject/tests/VObject/JCardTest.php | 195 ++ .../tests/VObject/LineFoldingIssueTest.php | 23 + .../vobject/tests/VObject/ParameterTest.php | 135 + .../vobject/tests/VObject/Parser/JsonTest.php | 395 +++ .../tests/VObject/Parser/MimeDirTest.php | 143 + .../VObject/Parser/QuotedPrintableTest.php | 108 + .../vobject/tests/VObject/Parser/XmlTest.php | 2893 +++++++++++++++++ .../tests/VObject/Property/BinaryTest.php | 19 + .../tests/VObject/Property/BooleanTest.php | 22 + .../tests/VObject/Property/CompoundTest.php | 50 + .../tests/VObject/Property/FloatTest.php | 30 + .../Property/ICalendar/CalAddressTest.php | 32 + .../Property/ICalendar/DateTimeTest.php | 371 +++ .../Property/ICalendar/DurationTest.php | 20 + .../VObject/Property/ICalendar/RecurTest.php | 453 +++ .../tests/VObject/Property/TextTest.php | 96 + .../tests/VObject/Property/UriTest.php | 27 + .../Property/VCard/DateAndOrTimeTest.php | 269 ++ .../Property/VCard/LanguageTagTest.php | 48 + .../vobject/tests/VObject/PropertyTest.php | 410 +++ .../vobject/tests/VObject/ReaderTest.php | 491 +++ .../EventIterator/ByMonthInDailyTest.php | 58 + .../Recur/EventIterator/BySetPosHangTest.php | 60 + .../EventIterator/ExpandFloatingTimesTest.php | 122 + .../EventIterator/FifthTuesdayProblemTest.php | 54 + .../EventIterator/HandleRDateExpandTest.php | 60 + .../EventIterator/IncorrectExpandTest.php | 62 + .../EventIterator/InfiniteLoopProblemTest.php | 98 + .../Recur/EventIterator/Issue26Test.php | 34 + .../Recur/EventIterator/Issue48Test.php | 48 + .../Recur/EventIterator/Issue50Test.php | 127 + .../VObject/Recur/EventIterator/MainTest.php | 1452 +++++++++ .../Recur/EventIterator/MaxInstancesTest.php | 41 + .../EventIterator/MissingOverriddenTest.php | 62 + .../Recur/EventIterator/NoInstancesTest.php | 40 + .../EventIterator/OverrideFirstEventTest.php | 121 + .../SameDateForRecurringEventsTest.php | 55 + .../tests/VObject/Recur/RDateIteratorTest.php | 78 + .../tests/VObject/Recur/RRuleIteratorTest.php | 995 ++++++ .../UntilRespectsTimezoneTest.ics | 39 + .../vobject/tests/VObject/SlashRTest.php | 20 + .../tests/VObject/Splitter/ICalendarTest.php | 325 ++ .../tests/VObject/Splitter/VCardTest.php | 193 ++ .../vobject/tests/VObject/StringUtilTest.php | 55 + .../tests/VObject/TimeZoneUtilTest.php | 377 +++ .../vobject/tests/VObject/UUIDUtilTest.php | 37 + .../vobject/tests/VObject/VCard21Test.php | 52 + .../tests/VObject/VCardConverterTest.php | 533 +++ .../vobject/tests/VObject/VersionTest.php | 14 + .../vobject/tests/VObject/WriterTest.php | 41 + .../sabre/vobject/tests/VObject/issue153.vcf | 352 ++ .../sabre/vobject/tests/VObject/issue64.vcf | 351 ++ .../sabre/sabre/vobject/tests/bootstrap.php | 25 + .../sabre/sabre/vobject/tests/phpunit.xml | 23 + htdocs/includes/sabre/sabre/xml/.gitignore | 9 + htdocs/includes/sabre/sabre/xml/.travis.yml | 26 + htdocs/includes/sabre/sabre/xml/CHANGELOG.md | 228 ++ htdocs/includes/sabre/sabre/xml/LICENSE | 27 + htdocs/includes/sabre/sabre/xml/README.md | 25 + htdocs/includes/sabre/sabre/xml/bin/.empty | 0 htdocs/includes/sabre/sabre/xml/composer.json | 53 + .../sabre/sabre/xml/lib/ContextStackTrait.php | 123 + .../sabre/xml/lib/Deserializer/functions.php | 258 ++ .../includes/sabre/sabre/xml/lib/Element.php | 20 + .../sabre/sabre/xml/lib/Element/Base.php | 91 + .../sabre/sabre/xml/lib/Element/Cdata.php | 64 + .../sabre/sabre/xml/lib/Element/Elements.php | 108 + .../sabre/sabre/xml/lib/Element/KeyValue.php | 108 + .../sabre/sabre/xml/lib/Element/Uri.php | 104 + .../sabre/xml/lib/Element/XmlFragment.php | 147 + .../sabre/sabre/xml/lib/LibXMLException.php | 53 + .../sabre/sabre/xml/lib/ParseException.php | 17 + .../includes/sabre/sabre/xml/lib/Reader.php | 330 ++ .../sabre/xml/lib/Serializer/functions.php | 249 ++ .../includes/sabre/sabre/xml/lib/Service.php | 297 ++ .../includes/sabre/sabre/xml/lib/Version.php | 19 + .../includes/sabre/sabre/xml/lib/Writer.php | 266 ++ .../sabre/sabre/xml/lib/XmlDeserializable.php | 38 + .../sabre/sabre/xml/lib/XmlSerializable.php | 36 + .../xml/tests/Sabre/Xml/ContextStackTest.php | 44 + .../tests/Sabre/Xml/Deserializer/EnumTest.php | 62 + .../Sabre/Xml/Deserializer/KeyValueTest.php | 112 + .../Deserializer/RepeatingElementsTest.php | 35 + .../Xml/Deserializer/ValueObjectTest.php | 169 + .../xml/tests/Sabre/Xml/Element/CDataTest.php | 58 + .../xml/tests/Sabre/Xml/Element/Eater.php | 78 + .../tests/Sabre/Xml/Element/ElementsTest.php | 129 + .../tests/Sabre/Xml/Element/KeyValueTest.php | 210 ++ .../xml/tests/Sabre/Xml/Element/Mock.php | 60 + .../xml/tests/Sabre/Xml/Element/UriTest.php | 76 + .../Sabre/Xml/Element/XmlFragmentTest.php | 143 + .../xml/tests/Sabre/Xml/InfiteLoopTest.php | 50 + .../sabre/xml/tests/Sabre/Xml/ReaderTest.php | 585 ++++ .../tests/Sabre/Xml/Serializer/EnumTest.php | 36 + .../Xml/Serializer/RepeatingElementsTest.php | 35 + .../sabre/xml/tests/Sabre/Xml/ServiceTest.php | 328 ++ .../sabre/xml/tests/Sabre/Xml/WriterTest.php | 439 +++ .../sabre/sabre/xml/tests/phpcs/ruleset.xml | 56 + .../sabre/sabre/xml/tests/phpunit.xml.dist | 17 + .../core/modules/modMyModule.class.php | 2 +- 851 files changed, 139891 insertions(+), 1 deletion(-) create mode 100644 htdocs/core/modules/modDav.class.php create mode 100644 htdocs/dav/fileserver.php create mode 100644 htdocs/includes/sabre/autoload.php create mode 100644 htdocs/includes/sabre/composer/ClassLoader.php create mode 100644 htdocs/includes/sabre/composer/LICENSE create mode 100644 htdocs/includes/sabre/composer/autoload_classmap.php create mode 100644 htdocs/includes/sabre/composer/autoload_files.php create mode 100644 htdocs/includes/sabre/composer/autoload_namespaces.php create mode 100644 htdocs/includes/sabre/composer/autoload_psr4.php create mode 100644 htdocs/includes/sabre/composer/autoload_real.php create mode 100644 htdocs/includes/sabre/composer/autoload_static.php create mode 100644 htdocs/includes/sabre/composer/installed.json create mode 100644 htdocs/includes/sabre/psr/log/.gitignore create mode 100644 htdocs/includes/sabre/psr/log/LICENSE create mode 100644 htdocs/includes/sabre/psr/log/Psr/Log/AbstractLogger.php create mode 100644 htdocs/includes/sabre/psr/log/Psr/Log/InvalidArgumentException.php create mode 100644 htdocs/includes/sabre/psr/log/Psr/Log/LogLevel.php create mode 100644 htdocs/includes/sabre/psr/log/Psr/Log/LoggerAwareInterface.php create mode 100644 htdocs/includes/sabre/psr/log/Psr/Log/LoggerAwareTrait.php create mode 100644 htdocs/includes/sabre/psr/log/Psr/Log/LoggerInterface.php create mode 100644 htdocs/includes/sabre/psr/log/Psr/Log/LoggerTrait.php create mode 100644 htdocs/includes/sabre/psr/log/Psr/Log/NullLogger.php create mode 100644 htdocs/includes/sabre/psr/log/Psr/Log/Test/LoggerInterfaceTest.php create mode 100644 htdocs/includes/sabre/psr/log/README.md create mode 100644 htdocs/includes/sabre/psr/log/composer.json create mode 100644 htdocs/includes/sabre/sabre/dav/.gitignore create mode 100644 htdocs/includes/sabre/sabre/dav/.travis.yml create mode 100644 htdocs/includes/sabre/sabre/dav/CONTRIBUTING.md create mode 100644 htdocs/includes/sabre/sabre/dav/bin/build.php create mode 100644 htdocs/includes/sabre/sabre/dav/bin/googlecode_upload.py create mode 100644 htdocs/includes/sabre/sabre/dav/bin/migrateto20.php create mode 100644 htdocs/includes/sabre/sabre/dav/bin/migrateto21.php create mode 100644 htdocs/includes/sabre/sabre/dav/bin/migrateto30.php create mode 100644 htdocs/includes/sabre/sabre/dav/bin/migrateto32.php create mode 100644 htdocs/includes/sabre/sabre/dav/bin/naturalselection create mode 100644 htdocs/includes/sabre/sabre/dav/bin/sabredav create mode 100644 htdocs/includes/sabre/sabre/dav/bin/sabredav.php create mode 100644 htdocs/includes/sabre/sabre/dav/composer.json create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Backend/BackendInterface.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Backend/NotificationSupport.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Backend/PDO.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Backend/SharingSupport.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Backend/SimplePDO.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Backend/SyncSupport.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Calendar.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/CalendarHome.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/CalendarObject.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/CalendarQueryValidator.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/CalendarRoot.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/ICSExportPlugin.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/ICalendar.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/ICalendarObject.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/ICalendarObjectContainer.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/ISharedCalendar.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Notifications/Collection.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Notifications/ICollection.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Notifications/INode.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Notifications/Node.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Notifications/Plugin.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Plugin.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Principal/Collection.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Principal/IProxyRead.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Principal/IProxyWrite.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Principal/ProxyRead.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Principal/ProxyWrite.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Principal/User.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Schedule/IInbox.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Schedule/IMipPlugin.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Schedule/IOutbox.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Schedule/ISchedulingObject.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Schedule/Inbox.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Schedule/Outbox.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Schedule/Plugin.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/SharedCalendar.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/SharingPlugin.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Subscriptions/ISubscription.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Subscriptions/Plugin.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Filter/CalendarData.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Filter/CompFilter.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Filter/ParamFilter.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Filter/PropFilter.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Notification/NotificationInterface.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Notification/SystemStatus.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Property/AllowedSharingModes.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Property/EmailAddressSet.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Property/Invite.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Property/ScheduleCalendarTransp.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarComponentSet.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarData.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Property/SupportedCollationSet.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Request/CalendarMultiGetReport.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Request/CalendarQueryReport.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Request/FreeBusyQueryReport.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Request/MkCalendar.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Request/Share.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CardDAV/AddressBook.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CardDAV/AddressBookHome.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CardDAV/AddressBookRoot.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CardDAV/Backend/AbstractBackend.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CardDAV/Backend/BackendInterface.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CardDAV/Backend/PDO.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CardDAV/Backend/SyncSupport.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CardDAV/Card.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CardDAV/IAddressBook.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CardDAV/ICard.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CardDAV/IDirectory.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CardDAV/Plugin.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CardDAV/VCFExportPlugin.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CardDAV/Xml/Filter/AddressData.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CardDAV/Xml/Filter/ParamFilter.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CardDAV/Xml/Filter/PropFilter.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CardDAV/Xml/Property/SupportedAddressData.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CardDAV/Xml/Property/SupportedCollationSet.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CardDAV/Xml/Request/AddressBookMultiGetReport.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/CardDAV/Xml/Request/AddressBookQueryReport.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/AbstractBasic.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/AbstractBearer.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/AbstractDigest.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/Apache.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/BackendInterface.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/BasicCallBack.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/File.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/PDO.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Plugin.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/GuessContentType.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/HtmlOutput.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/HtmlOutputHelper.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/MapGetToPropFind.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/Plugin.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/PropFindAll.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/favicon.ico create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/ICON-LICENSE create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.css create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.eot create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.otf create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.svg create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.ttf create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.woff create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/sabredav.css create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/sabredav.png create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Client.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Collection.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/CorePlugin.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Exception.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/BadRequest.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/Conflict.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/ConflictingLock.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/Forbidden.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/InsufficientStorage.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/InvalidResourceType.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/InvalidSyncToken.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/LengthRequired.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/LockTokenMatchesRequestUri.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/Locked.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/NotAuthenticated.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/NotFound.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/NotImplemented.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/PaymentRequired.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/PreconditionFailed.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/ReportNotSupported.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/RequestedRangeNotSatisfiable.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/ServiceUnavailable.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/TooManyMatches.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/UnsupportedMediaType.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/FS/Directory.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/FS/File.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/FS/Node.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/FSExt/Directory.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/FSExt/File.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/File.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/ICollection.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/IExtendedCollection.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/IFile.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/IMoveTarget.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/IMultiGet.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/INode.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/IProperties.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/IQuota.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Locks/Backend/AbstractBackend.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Locks/Backend/BackendInterface.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Locks/Backend/File.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Locks/Backend/PDO.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Locks/LockInfo.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Locks/Plugin.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/MkCol.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Mount/Plugin.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Node.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/PartialUpdate/IPatchSupport.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/PartialUpdate/Plugin.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/PropFind.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/PropPatch.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/PropertyStorage/Backend/BackendInterface.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/PropertyStorage/Plugin.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Server.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/ServerPlugin.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Sharing/ISharedNode.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Sharing/Plugin.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/SimpleCollection.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/SimpleFile.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/StringUtil.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Sync/ISyncCollection.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Sync/Plugin.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/TemporaryFileFilterPlugin.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Tree.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/UUIDUtil.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Version.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Element/Prop.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Element/Response.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Element/Sharee.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/Complex.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/GetLastModified.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/Href.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/Invite.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/LocalHref.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/LockDiscovery.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/ResourceType.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/ShareAccess.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/SupportedLock.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/SupportedMethodSet.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/SupportedReportSet.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Request/Lock.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Request/MkCol.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Request/PropFind.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Request/PropPatch.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Request/ShareResource.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Request/SyncCollectionReport.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Response/MultiStatus.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Service.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/ACLTrait.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/AbstractPrincipalCollection.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/Exception/AceConflict.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/Exception/NeedPrivileges.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/Exception/NoAbstract.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/Exception/NotRecognizedPrincipal.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/Exception/NotSupportedPrivilege.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/FS/Collection.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/FS/File.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/FS/HomeCollection.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/IACL.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/IPrincipal.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/IPrincipalCollection.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/Plugin.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/Principal.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/PrincipalBackend/AbstractBackend.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/PrincipalBackend/BackendInterface.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/PrincipalBackend/CreatePrincipalSupport.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/PrincipalBackend/PDO.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/PrincipalCollection.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Property/Acl.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Property/AclRestrictions.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Property/CurrentUserPrivilegeSet.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Property/Principal.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Request/AclPrincipalPropSetReport.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Request/ExpandPropertyReport.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Request/PrincipalMatchReport.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Request/PrincipalPropertySearchReport.php create mode 100644 htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Request/PrincipalSearchPropertySetReport.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/AbstractPDOTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/AbstractTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/Mock.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/MockScheduling.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/MockSharing.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/MockSubscriptionSupport.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/PDOMySQLTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/PDOPgSqlTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/PDOSqliteTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/SimplePDOTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarHomeNotificationsTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarHomeSharedCalendarsTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarHomeSubscriptionsTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarHomeTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarObjectTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarQueryVAlarmTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarQueryValidatorTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/ExpandEventsDTSTARTandDTENDTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/ExpandEventsDTSTARTandDTENDbyDayTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/ExpandEventsDoubleEventsTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/ExpandEventsFloatingTimeTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/FreeBusyReportTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/GetEventsByTimerangeTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/ICSExportPluginTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Issue166Test.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Issue172Test.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Issue203Test.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Issue205Test.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Issue211Test.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Issue220Test.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Issue228Test.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/JCalTransformTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Notifications/CollectionTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Notifications/NodeTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Notifications/PluginTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/PluginTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Principal/CollectionTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Principal/ProxyReadTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Principal/ProxyWriteTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Principal/UserTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/DeliverNewEventTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/FreeBusyRequestTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/IMip/MockPlugin.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/IMipPluginTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/InboxTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/OutboxPostTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/OutboxTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/PluginBasicTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/PluginPropertiesTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/PluginPropertiesWithSharedCalendarTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/ScheduleDeliverTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/SchedulingObjectTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/SharedCalendarTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/SharingPluginTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Subscriptions/CreateSubscriptionTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Subscriptions/PluginTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Subscriptions/SubscriptionTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/TestUtil.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/ValidateICalTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Notification/InviteReplyTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Notification/InviteTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Notification/SystemStatusTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Property/AllowedSharingModesTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Property/EmailAddressSetTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Property/InviteTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Property/ScheduleCalendarTranspTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Property/SupportedCalendarComponentSetTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Property/SupportedCalendarDataTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Property/SupportedCollationSetTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Request/CalendarQueryReportTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Request/InviteReplyTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Request/ShareTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/AbstractPluginTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/AddressBookHomeTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/AddressBookQueryTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/AddressBookRootTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/AddressBookTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Backend/AbstractPDOTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Backend/Mock.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Backend/PDOMySQLTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Backend/PDOPgSqlTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Backend/PDOSqliteTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/CardTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/IDirectoryTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/MultiGetTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/PluginTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/SogoStripContentTypeTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/TestUtil.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/VCFExportTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/ValidateFilterTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/ValidateVCardTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Xml/Property/SupportedAddressDataTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Xml/Property/SupportedCollationSetTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Xml/Request/AddressBookMultiGetTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Xml/Request/AddressBookQueryReportTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/AbstractServer.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractBasicTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractBearerTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractDigestTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractPDOTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/ApacheTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/BasicCallBackTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/FileTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/Mock.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/PDOMySQLTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/PDOPgSqlTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/PDOSqliteTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/PluginTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/BasicNodeTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Browser/GuessContentTypeTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Browser/MapGetToPropFindTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Browser/PluginTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Browser/PropFindAllTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ClientMock.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ClientTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/CorePluginTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/DbTestHelperTrait.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Exception/LockedTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Exception/PaymentRequiredTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Exception/ServiceUnavailableTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Exception/TooManyMatchesTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ExceptionTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/FSExt/DirectoryTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/FSExt/FileTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/FSExt/ServerTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/GetIfConditionsTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/HTTPPreferParsingTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/HttpCopyTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/HttpDeleteTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/HttpGetTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/HttpHeadTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/HttpMoveTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/HttpPutTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Issue33Test.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Backend/AbstractTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Backend/FileTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Backend/Mock.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOMySQLTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOPgSqlTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOSqliteTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/MSWordTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Plugin2Test.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/PluginTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Mock/Collection.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Mock/File.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Mock/PropertiesCollection.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Mock/SharedNode.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Mock/StreamingFile.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/MockLogger.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Mount/PluginTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ObjectTreeTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PSR3Test.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PartialUpdate/FileMock.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PartialUpdate/PluginTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PartialUpdate/SpecificationTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropFindTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropPatchTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/AbstractPDOTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/Mock.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/PDOMysqlTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/PDOPgSqlTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/PDOSqliteTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropertyStorage/PluginTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerEventsTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerMKCOLTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerPluginTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerPreconditionTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerPropsInfiniteDepthTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerPropsTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerRangeTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerSimpleTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerUpdatePropertiesTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Sharing/PluginTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Sharing/ShareResourceTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/SimpleFileTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/StringUtilTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Sync/MockSyncCollection.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Sync/PluginTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/SyncTokenPropertyTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/TemporaryFileFilterTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/TestPlugin.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/TreeTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/UUIDUtilTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Element/PropTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Element/ResponseTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Element/ShareeTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/HrefTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/InviteTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/LastModifiedTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/LocalHrefTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/LockDiscoveryTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/ShareAccessTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/SupportedMethodSetTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/SupportedReportSetTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Request/PropFindTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Request/PropPatchTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Request/ShareResourceTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Request/SyncCollectionTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/XmlTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/ACLMethodTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/AclPrincipalPropSetReportTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/AllowAccessTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/BlockAccessTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Exception/AceConflictTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Exception/NeedPrivilegesExceptionTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Exception/NoAbstractTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Exception/NotRecognizedPrincipalTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Exception/NotSupportedPrivilegeTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/ExpandPropertiesTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/FS/CollectionTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/FS/FileTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/FS/HomeCollectionTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/MockACLNode.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/MockPrincipal.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PluginAdminTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PluginPropertiesTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PluginUpdatePropertiesTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/AbstractPDOTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/Mock.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/PDOMySQLTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/PDOPgSqlTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/PDOSqliteTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalCollectionTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalMatchTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalPropertySearchTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalSearchPropertySetTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/SimplePluginTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Xml/Property/ACLTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Xml/Property/AclRestrictionsTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Xml/Property/CurrentUserPrivilegeSetTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Xml/Property/PrincipalTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Xml/Property/SupportedPrivilegeSetTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Xml/Request/AclPrincipalPropSetReportTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Xml/Request/PrincipalMatchReportTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVServerTest.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/HTTP/ResponseMock.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/HTTP/SapiMock.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/Sabre/TestUtil.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/bootstrap.php create mode 100644 htdocs/includes/sabre/sabre/dav/tests/phpcs/ruleset.xml create mode 100644 htdocs/includes/sabre/sabre/dav/tests/phpunit.xml.dist create mode 100644 htdocs/includes/sabre/sabre/event/.gitignore create mode 100644 htdocs/includes/sabre/sabre/event/.travis.yml create mode 100644 htdocs/includes/sabre/sabre/event/CHANGELOG.md create mode 100644 htdocs/includes/sabre/sabre/event/LICENSE create mode 100644 htdocs/includes/sabre/sabre/event/README.md create mode 100644 htdocs/includes/sabre/sabre/event/bin/.empty create mode 100644 htdocs/includes/sabre/sabre/event/composer.json create mode 100644 htdocs/includes/sabre/sabre/event/examples/promise.php create mode 100644 htdocs/includes/sabre/sabre/event/examples/tail.php create mode 100644 htdocs/includes/sabre/sabre/event/lib/EventEmitter.php create mode 100644 htdocs/includes/sabre/sabre/event/lib/EventEmitterInterface.php create mode 100644 htdocs/includes/sabre/sabre/event/lib/EventEmitterTrait.php create mode 100644 htdocs/includes/sabre/sabre/event/lib/Loop/Loop.php create mode 100644 htdocs/includes/sabre/sabre/event/lib/Loop/functions.php create mode 100644 htdocs/includes/sabre/sabre/event/lib/Promise.php create mode 100644 htdocs/includes/sabre/sabre/event/lib/Promise/functions.php create mode 100644 htdocs/includes/sabre/sabre/event/lib/PromiseAlreadyResolvedException.php create mode 100644 htdocs/includes/sabre/sabre/event/lib/Version.php create mode 100644 htdocs/includes/sabre/sabre/event/lib/coroutine.php create mode 100644 htdocs/includes/sabre/sabre/event/phpunit.xml.dist create mode 100644 htdocs/includes/sabre/sabre/event/tests/ContinueCallbackTest.php create mode 100644 htdocs/includes/sabre/sabre/event/tests/CoroutineTest.php create mode 100644 htdocs/includes/sabre/sabre/event/tests/EventEmitterTest.php create mode 100644 htdocs/includes/sabre/sabre/event/tests/Loop/FunctionsTest.php create mode 100644 htdocs/includes/sabre/sabre/event/tests/Loop/LoopTest.php create mode 100644 htdocs/includes/sabre/sabre/event/tests/Promise/FunctionsTest.php create mode 100644 htdocs/includes/sabre/sabre/event/tests/Promise/PromiseTest.php create mode 100644 htdocs/includes/sabre/sabre/event/tests/PromiseTest.php create mode 100644 htdocs/includes/sabre/sabre/event/tests/benchmark/bench.php create mode 100644 htdocs/includes/sabre/sabre/http/.gitignore create mode 100644 htdocs/includes/sabre/sabre/http/.travis.yml create mode 100644 htdocs/includes/sabre/sabre/http/CHANGELOG.md create mode 100644 htdocs/includes/sabre/sabre/http/LICENSE create mode 100644 htdocs/includes/sabre/sabre/http/README.md create mode 100644 htdocs/includes/sabre/sabre/http/bin/.empty create mode 100644 htdocs/includes/sabre/sabre/http/composer.json create mode 100644 htdocs/includes/sabre/sabre/http/examples/asyncclient.php create mode 100644 htdocs/includes/sabre/sabre/http/examples/basicauth.php create mode 100644 htdocs/includes/sabre/sabre/http/examples/client.php create mode 100644 htdocs/includes/sabre/sabre/http/examples/digestauth.php create mode 100644 htdocs/includes/sabre/sabre/http/examples/reverseproxy.php create mode 100644 htdocs/includes/sabre/sabre/http/examples/stringify.php create mode 100644 htdocs/includes/sabre/sabre/http/lib/Auth/AWS.php create mode 100644 htdocs/includes/sabre/sabre/http/lib/Auth/AbstractAuth.php create mode 100644 htdocs/includes/sabre/sabre/http/lib/Auth/Basic.php create mode 100644 htdocs/includes/sabre/sabre/http/lib/Auth/Bearer.php create mode 100644 htdocs/includes/sabre/sabre/http/lib/Auth/Digest.php create mode 100644 htdocs/includes/sabre/sabre/http/lib/Client.php create mode 100644 htdocs/includes/sabre/sabre/http/lib/ClientException.php create mode 100644 htdocs/includes/sabre/sabre/http/lib/ClientHttpException.php create mode 100644 htdocs/includes/sabre/sabre/http/lib/HttpException.php create mode 100644 htdocs/includes/sabre/sabre/http/lib/Message.php create mode 100644 htdocs/includes/sabre/sabre/http/lib/MessageDecoratorTrait.php create mode 100644 htdocs/includes/sabre/sabre/http/lib/MessageInterface.php create mode 100644 htdocs/includes/sabre/sabre/http/lib/Request.php create mode 100644 htdocs/includes/sabre/sabre/http/lib/RequestDecorator.php create mode 100644 htdocs/includes/sabre/sabre/http/lib/RequestInterface.php create mode 100644 htdocs/includes/sabre/sabre/http/lib/Response.php create mode 100644 htdocs/includes/sabre/sabre/http/lib/ResponseDecorator.php create mode 100644 htdocs/includes/sabre/sabre/http/lib/ResponseInterface.php create mode 100644 htdocs/includes/sabre/sabre/http/lib/Sapi.php create mode 100644 htdocs/includes/sabre/sabre/http/lib/URLUtil.php create mode 100644 htdocs/includes/sabre/sabre/http/lib/Util.php create mode 100644 htdocs/includes/sabre/sabre/http/lib/Version.php create mode 100644 htdocs/includes/sabre/sabre/http/lib/functions.php create mode 100644 htdocs/includes/sabre/sabre/http/tests/HTTP/Auth/AWSTest.php create mode 100644 htdocs/includes/sabre/sabre/http/tests/HTTP/Auth/BasicTest.php create mode 100644 htdocs/includes/sabre/sabre/http/tests/HTTP/Auth/BearerTest.php create mode 100644 htdocs/includes/sabre/sabre/http/tests/HTTP/Auth/DigestTest.php create mode 100644 htdocs/includes/sabre/sabre/http/tests/HTTP/ClientTest.php create mode 100644 htdocs/includes/sabre/sabre/http/tests/HTTP/FunctionsTest.php create mode 100644 htdocs/includes/sabre/sabre/http/tests/HTTP/MessageDecoratorTest.php create mode 100644 htdocs/includes/sabre/sabre/http/tests/HTTP/MessageTest.php create mode 100644 htdocs/includes/sabre/sabre/http/tests/HTTP/RequestDecoratorTest.php create mode 100644 htdocs/includes/sabre/sabre/http/tests/HTTP/RequestTest.php create mode 100644 htdocs/includes/sabre/sabre/http/tests/HTTP/ResponseDecoratorTest.php create mode 100644 htdocs/includes/sabre/sabre/http/tests/HTTP/ResponseTest.php create mode 100644 htdocs/includes/sabre/sabre/http/tests/HTTP/SapiTest.php create mode 100644 htdocs/includes/sabre/sabre/http/tests/HTTP/URLUtilTest.php create mode 100644 htdocs/includes/sabre/sabre/http/tests/HTTP/UtilTest.php create mode 100644 htdocs/includes/sabre/sabre/http/tests/bootstrap.php create mode 100644 htdocs/includes/sabre/sabre/http/tests/phpcs/ruleset.xml create mode 100644 htdocs/includes/sabre/sabre/http/tests/phpunit.xml create mode 100644 htdocs/includes/sabre/sabre/uri/.gitignore create mode 100644 htdocs/includes/sabre/sabre/uri/.travis.yml create mode 100644 htdocs/includes/sabre/sabre/uri/CHANGELOG.md create mode 100644 htdocs/includes/sabre/sabre/uri/LICENSE create mode 100644 htdocs/includes/sabre/sabre/uri/README.md create mode 100644 htdocs/includes/sabre/sabre/uri/composer.json create mode 100644 htdocs/includes/sabre/sabre/uri/lib/InvalidUriException.php create mode 100644 htdocs/includes/sabre/sabre/uri/lib/Version.php create mode 100644 htdocs/includes/sabre/sabre/uri/lib/functions.php create mode 100644 htdocs/includes/sabre/sabre/uri/tests/BuildTest.php create mode 100644 htdocs/includes/sabre/sabre/uri/tests/NormalizeTest.php create mode 100644 htdocs/includes/sabre/sabre/uri/tests/ParseTest.php create mode 100644 htdocs/includes/sabre/sabre/uri/tests/ResolveTest.php create mode 100644 htdocs/includes/sabre/sabre/uri/tests/SplitTest.php create mode 100644 htdocs/includes/sabre/sabre/uri/tests/phpcs/ruleset.xml create mode 100644 htdocs/includes/sabre/sabre/uri/tests/phpunit.xml.dist create mode 100644 htdocs/includes/sabre/sabre/vobject/.gitignore create mode 100644 htdocs/includes/sabre/sabre/vobject/.travis.yml create mode 100644 htdocs/includes/sabre/sabre/vobject/CHANGELOG.md create mode 100644 htdocs/includes/sabre/sabre/vobject/LICENSE create mode 100644 htdocs/includes/sabre/sabre/vobject/README.md create mode 100644 htdocs/includes/sabre/sabre/vobject/bin/bench.php create mode 100644 htdocs/includes/sabre/sabre/vobject/bin/bench_freebusygenerator.php create mode 100644 htdocs/includes/sabre/sabre/vobject/bin/bench_manipulatevcard.php create mode 100644 htdocs/includes/sabre/sabre/vobject/bin/fetch_windows_zones.php create mode 100644 htdocs/includes/sabre/sabre/vobject/bin/generate_vcards create mode 100644 htdocs/includes/sabre/sabre/vobject/bin/generateicalendardata.php create mode 100644 htdocs/includes/sabre/sabre/vobject/bin/mergeduplicates.php create mode 100644 htdocs/includes/sabre/sabre/vobject/bin/rrulebench.php create mode 100644 htdocs/includes/sabre/sabre/vobject/bin/vobject create mode 100644 htdocs/includes/sabre/sabre/vobject/composer.json create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/BirthdayCalendarGenerator.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Cli.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Component.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Component/Available.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Component/VAlarm.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Component/VAvailability.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Component/VCalendar.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Component/VCard.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Component/VEvent.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Component/VFreeBusy.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Component/VJournal.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Component/VTimeZone.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Component/VTodo.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/DateTimeParser.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Document.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/ElementList.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/EofException.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/FreeBusyData.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/FreeBusyGenerator.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/ITip/Broker.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/ITip/ITipException.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/ITip/Message.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/ITip/SameOrganizerForAllComponentsException.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/InvalidDataException.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Node.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/PHPUnitAssertions.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Parameter.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/ParseException.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Parser/Json.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Parser/MimeDir.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Parser/Parser.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Parser/XML.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Parser/XML/Element/KeyValue.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Property.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Property/Binary.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Property/Boolean.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Property/FlatText.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Property/FloatValue.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Property/ICalendar/CalAddress.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Property/ICalendar/Date.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Property/ICalendar/DateTime.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Property/ICalendar/Duration.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Property/ICalendar/Period.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Property/ICalendar/Recur.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Property/IntegerValue.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Property/Text.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Property/Time.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Property/Unknown.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Property/Uri.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Property/UtcOffset.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Property/VCard/Date.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Property/VCard/DateAndOrTime.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Property/VCard/DateTime.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Property/VCard/LanguageTag.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Property/VCard/TimeStamp.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Reader.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Recur/EventIterator.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Recur/MaxInstancesExceededException.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Recur/NoInstancesException.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Recur/RDateIterator.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Recur/RRuleIterator.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Settings.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Splitter/ICalendar.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Splitter/SplitterInterface.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Splitter/VCard.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/StringUtil.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/TimeZoneUtil.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/UUIDUtil.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/VCardConverter.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Version.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/Writer.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/timezonedata/exchangezones.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/timezonedata/lotuszones.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/timezonedata/php-bc.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/timezonedata/php-workaround.php create mode 100644 htdocs/includes/sabre/sabre/vobject/lib/timezonedata/windowszones.php create mode 100644 htdocs/includes/sabre/sabre/vobject/resources/schema/xcal.rng create mode 100644 htdocs/includes/sabre/sabre/vobject/resources/schema/xcard.rng create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/AttachIssueTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/BirthdayCalendarGeneratorTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/CliTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/AvailableTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VAlarmTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VAvailabilityTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VCalendarTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VCardTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VEventTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VFreeBusyTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VJournalTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VTimeZoneTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VTodoTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/ComponentTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/DateTimeParserTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/DocumentTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/ElementListTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/EmClientTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/EmptyParameterTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/EmptyValueIssueTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/FreeBusyDataTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/FreeBusyGeneratorTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/GoogleColonEscapingTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/ICalendar/AttachParseTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerAttendeeReplyTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerDeleteEventTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerNewEventTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerProcessMessageTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerProcessReplyTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerTester.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerTimezoneInParseEventInfoWithoutMasterTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerUpdateEventTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/EvolutionTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/MessageTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Issue153Test.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Issue259Test.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Issue36WorkAroundTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Issue40Test.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Issue64Test.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Issue96Test.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/IssueUndefinedIndexTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/JCalTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/JCardTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/LineFoldingIssueTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/ParameterTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Parser/JsonTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Parser/MimeDirTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Parser/QuotedPrintableTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Parser/XmlTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/BinaryTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/BooleanTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/CompoundTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/FloatTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/ICalendar/CalAddressTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/ICalendar/DateTimeTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/ICalendar/DurationTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/ICalendar/RecurTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/TextTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/UriTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/VCard/DateAndOrTimeTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/VCard/LanguageTagTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/PropertyTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/ReaderTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/ByMonthInDailyTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/BySetPosHangTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/ExpandFloatingTimesTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/FifthTuesdayProblemTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/HandleRDateExpandTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/IncorrectExpandTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/InfiniteLoopProblemTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/Issue26Test.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/Issue48Test.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/Issue50Test.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/MainTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/MaxInstancesTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/MissingOverriddenTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/NoInstancesTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/OverrideFirstEventTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/SameDateForRecurringEventsTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/RDateIteratorTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/RRuleIteratorTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/RecurrenceIterator/UntilRespectsTimezoneTest.ics create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/SlashRTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Splitter/ICalendarTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/Splitter/VCardTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/StringUtilTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/TimeZoneUtilTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/UUIDUtilTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/VCard21Test.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/VCardConverterTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/VersionTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/WriterTest.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/issue153.vcf create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/VObject/issue64.vcf create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/bootstrap.php create mode 100644 htdocs/includes/sabre/sabre/vobject/tests/phpunit.xml create mode 100644 htdocs/includes/sabre/sabre/xml/.gitignore create mode 100644 htdocs/includes/sabre/sabre/xml/.travis.yml create mode 100644 htdocs/includes/sabre/sabre/xml/CHANGELOG.md create mode 100644 htdocs/includes/sabre/sabre/xml/LICENSE create mode 100644 htdocs/includes/sabre/sabre/xml/README.md create mode 100644 htdocs/includes/sabre/sabre/xml/bin/.empty create mode 100644 htdocs/includes/sabre/sabre/xml/composer.json create mode 100644 htdocs/includes/sabre/sabre/xml/lib/ContextStackTrait.php create mode 100644 htdocs/includes/sabre/sabre/xml/lib/Deserializer/functions.php create mode 100644 htdocs/includes/sabre/sabre/xml/lib/Element.php create mode 100644 htdocs/includes/sabre/sabre/xml/lib/Element/Base.php create mode 100644 htdocs/includes/sabre/sabre/xml/lib/Element/Cdata.php create mode 100644 htdocs/includes/sabre/sabre/xml/lib/Element/Elements.php create mode 100644 htdocs/includes/sabre/sabre/xml/lib/Element/KeyValue.php create mode 100644 htdocs/includes/sabre/sabre/xml/lib/Element/Uri.php create mode 100644 htdocs/includes/sabre/sabre/xml/lib/Element/XmlFragment.php create mode 100644 htdocs/includes/sabre/sabre/xml/lib/LibXMLException.php create mode 100644 htdocs/includes/sabre/sabre/xml/lib/ParseException.php create mode 100644 htdocs/includes/sabre/sabre/xml/lib/Reader.php create mode 100644 htdocs/includes/sabre/sabre/xml/lib/Serializer/functions.php create mode 100644 htdocs/includes/sabre/sabre/xml/lib/Service.php create mode 100644 htdocs/includes/sabre/sabre/xml/lib/Version.php create mode 100644 htdocs/includes/sabre/sabre/xml/lib/Writer.php create mode 100644 htdocs/includes/sabre/sabre/xml/lib/XmlDeserializable.php create mode 100644 htdocs/includes/sabre/sabre/xml/lib/XmlSerializable.php create mode 100644 htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/ContextStackTest.php create mode 100644 htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Deserializer/EnumTest.php create mode 100644 htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Deserializer/KeyValueTest.php create mode 100644 htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Deserializer/RepeatingElementsTest.php create mode 100644 htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Deserializer/ValueObjectTest.php create mode 100644 htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Element/CDataTest.php create mode 100644 htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Element/Eater.php create mode 100644 htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Element/ElementsTest.php create mode 100644 htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Element/KeyValueTest.php create mode 100644 htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Element/Mock.php create mode 100644 htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Element/UriTest.php create mode 100644 htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Element/XmlFragmentTest.php create mode 100644 htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/InfiteLoopTest.php create mode 100644 htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/ReaderTest.php create mode 100644 htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Serializer/EnumTest.php create mode 100644 htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Serializer/RepeatingElementsTest.php create mode 100644 htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/ServiceTest.php create mode 100644 htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/WriterTest.php create mode 100644 htdocs/includes/sabre/sabre/xml/tests/phpcs/ruleset.xml create mode 100644 htdocs/includes/sabre/sabre/xml/tests/phpunit.xml.dist diff --git a/build/rpm/dolibarr_fedora.spec b/build/rpm/dolibarr_fedora.spec index 6df6ccf052e..df00a982f29 100755 --- a/build/rpm/dolibarr_fedora.spec +++ b/build/rpm/dolibarr_fedora.spec @@ -176,6 +176,7 @@ done >>%{name}.lang %_datadir/dolibarr/htdocs/core %_datadir/dolibarr/htdocs/cron %_datadir/dolibarr/htdocs/custom +%_datadir/dolibarr/htdocs/dav %_datadir/dolibarr/htdocs/don %_datadir/dolibarr/htdocs/ecm %_datadir/dolibarr/htdocs/expedition diff --git a/build/rpm/dolibarr_generic.spec b/build/rpm/dolibarr_generic.spec index f99836b7f74..908a7335415 100755 --- a/build/rpm/dolibarr_generic.spec +++ b/build/rpm/dolibarr_generic.spec @@ -256,6 +256,7 @@ done >>%{name}.lang %_datadir/dolibarr/htdocs/core %_datadir/dolibarr/htdocs/cron %_datadir/dolibarr/htdocs/custom +%_datadir/dolibarr/htdocs/dav %_datadir/dolibarr/htdocs/don %_datadir/dolibarr/htdocs/ecm %_datadir/dolibarr/htdocs/expedition diff --git a/build/rpm/dolibarr_mandriva.spec b/build/rpm/dolibarr_mandriva.spec index 1034615c80a..4a07c838b78 100755 --- a/build/rpm/dolibarr_mandriva.spec +++ b/build/rpm/dolibarr_mandriva.spec @@ -173,6 +173,7 @@ done >>%{name}.lang %_datadir/dolibarr/htdocs/core %_datadir/dolibarr/htdocs/cron %_datadir/dolibarr/htdocs/custom +%_datadir/dolibarr/htdocs/dav %_datadir/dolibarr/htdocs/don %_datadir/dolibarr/htdocs/ecm %_datadir/dolibarr/htdocs/expedition diff --git a/build/rpm/dolibarr_opensuse.spec b/build/rpm/dolibarr_opensuse.spec index eb1887f229f..6f1632a57e3 100755 --- a/build/rpm/dolibarr_opensuse.spec +++ b/build/rpm/dolibarr_opensuse.spec @@ -184,6 +184,7 @@ done >>%{name}.lang %_datadir/dolibarr/htdocs/core %_datadir/dolibarr/htdocs/cron %_datadir/dolibarr/htdocs/custom +%_datadir/dolibarr/htdocs/dav %_datadir/dolibarr/htdocs/don %_datadir/dolibarr/htdocs/ecm %_datadir/dolibarr/htdocs/expedition diff --git a/htdocs/core/modules/modDav.class.php b/htdocs/core/modules/modDav.class.php new file mode 100644 index 00000000000..0492209f3f0 --- /dev/null +++ b/htdocs/core/modules/modDav.class.php @@ -0,0 +1,340 @@ + + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * \defgroup dav Module dav + * \brief dav module descriptor. + * + * \file htdocs/dav/core/modules/modDav.class.php + * \ingroup dav + * \brief Description and activation file for module dav + */ +include_once DOL_DOCUMENT_ROOT .'/core/modules/DolibarrModules.class.php'; + + +// The class name should start with a lower case mod for Dolibarr to pick it up +// so we ignore the Squiz.Classes.ValidClassName.NotCamelCaps rule. +// @codingStandardsIgnoreStart +/** + * Description and activation class for module dav + */ +class modDav extends DolibarrModules +{ + // @codingStandardsIgnoreEnd + /** + * Constructor. Define names, constants, directories, boxes, permissions + * + * @param DoliDB $db Database handler + */ + public function __construct($db) + { + global $langs,$conf; + + $this->db = $db; + + // Id for module (must be unique). + // Use here a free id (See in Home -> System information -> Dolibarr for list of used modules id). + $this->numero = 50310; // TODO Go on page https://wiki.dolibarr.org/index.php/List_of_modules_id to reserve id number for your module + // Key text used to identify module (for permissions, menus, etc...) + $this->rights_class = 'dav'; + + // Family can be 'base' (core modules),'crm','financial','hr','projects','products','ecm','technic' (transverse modules),'interface' (link with external tools),'other','...' + // It is used to group modules by family in module setup page + $this->family = "interface"; + // Module position in the family on 2 digits ('01', '10', '20', ...) + $this->module_position = '90'; + // Gives the possibility to the module, to provide his own family info and position of this family (Overwrite $this->family and $this->module_position. Avoid this) + //$this->familyinfo = array('myownfamily' => array('position' => '01', 'label' => $langs->trans("MyOwnFamily"))); + + // Module label (no space allowed), used if translation string 'ModuledavName' not found (MyModue is name of module). + $this->name = preg_replace('/^mod/i','',get_class($this)); + // Module description, used if translation string 'ModuledavDesc' not found (MyModue is name of module). + $this->description = "davDescription"; + // Used only if file README.md and README-LL.md not found. + $this->descriptionlong = "davDescription (Long)"; + + // Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated' or a version string like 'x.y.z' + $this->version = 'experimental'; + // Key used in llx_const table to save module status enabled/disabled (where DAV is value of property name of module in uppercase) + $this->const_name = 'MAIN_MODULE_'.strtoupper($this->name); + // Name of image file used for this module. + // If file is in theme/yourtheme/img directory under name object_pictovalue.png, use this->picto='pictovalue' + // If file is in module/img directory under name object_pictovalue.png, use this->picto='pictovalue@module' + $this->picto='generic'; + + // Defined all module parts (triggers, login, substitutions, menus, css, etc...) + // for default path (eg: /dav/core/xxxxx) (0=disable, 1=enable) + // for specific path of parts (eg: /dav/core/modules/barcode) + // for specific css file (eg: /dav/css/dav.css.php) + $this->module_parts = array( + 'triggers' => 0, // Set this to 1 if module has its own trigger directory (core/triggers) + 'login' => 0, // Set this to 1 if module has its own login method file (core/login) + 'substitutions' => 0, // Set this to 1 if module has its own substitution function file (core/substitutions) + 'menus' => 0, // Set this to 1 if module has its own menus handler directory (core/menus) + 'theme' => 0, // Set this to 1 if module has its own theme directory (theme) + 'tpl' => 0, // Set this to 1 if module overwrite template dir (core/tpl) + 'barcode' => 0, // Set this to 1 if module has its own barcode directory (core/modules/barcode) + 'models' => 0, // Set this to 1 if module has its own models directory (core/modules/xxx) + 'css' => array(''), // Set this to relative path of css file if module has its own css file + 'js' => array(''), // Set this to relative path of js file if module must load a js on all pages + 'hooks' => array() // Set here all hooks context managed by module. To find available hook context, make a "grep -r '>initHooks(' *" on source code. You can also set hook context 'all' + ); + + // Data directories to create when module is enabled. + // Example: this->dirs = array("/dav/temp","/dav/subdir"); + $this->dirs = array("/dav/temp","/dav/public"); + + // Config pages. Put here list of php page, stored into dav/admin directory, to use to setup module. + $this->config_page_url = array("dav.php"); + + // Dependencies + $this->hidden = false; // A condition to hide module + $this->depends = array(); // List of module class names as string that must be enabled if this module is enabled + $this->requiredby = array(); // List of module ids to disable if this one is disabled + $this->conflictwith = array(); // List of module class names as string this module is in conflict with + $this->langfiles = array("admin"); + $this->phpmin = array(5,4); // Minimum version of PHP required by module + $this->need_dolibarr_version = array(7,0); // Minimum version of Dolibarr required by module + $this->warnings_activation = array(); // Warning to show when we activate module. array('always'='text') or array('FR'='textfr','ES'='textes'...) + $this->warnings_activation_ext = array(); // Warning to show when we activate an external module. array('always'='text') or array('FR'='textfr','ES'='textes'...) + //$this->automatic_activation = array('FR'=>'davWasAutomaticallyActivatedBecauseOfYourCountryChoice'); + //$this->always_enabled = true; // If true, can't be disabled + + // Constants + // List of particular constants to add when module is enabled (key, 'chaine', value, desc, visible, 'current' or 'allentities', deleteonunactive) + // Example: $this->const=array(0=>array('DAV_MYNEWCONST1','chaine','myvalue','This is a constant to add',1), + // 1=>array('DAV_MYNEWCONST2','chaine','myvalue','This is another constant to add',0, 'current', 1) + // ); + $this->const = array( + 1=>array('DAV_MYCONSTANT', 'chaine', 'avalue', 'This is a constant to add', 1, 'allentities', 1) + ); + + + if (! isset($conf->dav) || ! isset($conf->dav->enabled)) + { + $conf->dav=new stdClass(); + $conf->dav->enabled=0; + } + + + // Array to add new pages in new tabs + $this->tabs = array(); + // Example: + // $this->tabs[] = array('data'=>'objecttype:+tabname1:Title1:mylangfile@dav:$user->rights->dav->read:/dav/mynewtab1.php?id=__ID__'); // To add a new tab identified by code tabname1 + // $this->tabs[] = array('data'=>'objecttype:+tabname2:SUBSTITUTION_Title2:mylangfile@dav:$user->rights->othermodule->read:/dav/mynewtab2.php?id=__ID__', // To add another new tab identified by code tabname2. Label will be result of calling all substitution functions on 'Title2' key. + // $this->tabs[] = array('data'=>'objecttype:-tabname:NU:conditiontoremove'); // To remove an existing tab identified by code tabname + // + // Where objecttype can be + // 'categories_x' to add a tab in category view (replace 'x' by type of category (0=product, 1=supplier, 2=customer, 3=member) + // 'contact' to add a tab in contact view + // 'contract' to add a tab in contract view + // 'group' to add a tab in group view + // 'intervention' to add a tab in intervention view + // 'invoice' to add a tab in customer invoice view + // 'invoice_supplier' to add a tab in supplier invoice view + // 'member' to add a tab in fundation member view + // 'opensurveypoll' to add a tab in opensurvey poll view + // 'order' to add a tab in customer order view + // 'order_supplier' to add a tab in supplier order view + // 'payment' to add a tab in payment view + // 'payment_supplier' to add a tab in supplier payment view + // 'product' to add a tab in product view + // 'propal' to add a tab in propal view + // 'project' to add a tab in project view + // 'stock' to add a tab in stock view + // 'thirdparty' to add a tab in third party view + // 'user' to add a tab in user view + + + // Dictionaries + $this->dictionaries=array(); + /* Example: + $this->dictionaries=array( + 'langs'=>'mylangfile@dav', + 'tabname'=>array(MAIN_DB_PREFIX."table1",MAIN_DB_PREFIX."table2",MAIN_DB_PREFIX."table3"), // List of tables we want to see into dictonnary editor + 'tablib'=>array("Table1","Table2","Table3"), // Label of tables + 'tabsql'=>array('SELECT f.rowid as rowid, f.code, f.label, f.active FROM '.MAIN_DB_PREFIX.'table1 as f','SELECT f.rowid as rowid, f.code, f.label, f.active FROM '.MAIN_DB_PREFIX.'table2 as f','SELECT f.rowid as rowid, f.code, f.label, f.active FROM '.MAIN_DB_PREFIX.'table3 as f'), // Request to select fields + 'tabsqlsort'=>array("label ASC","label ASC","label ASC"), // Sort order + 'tabfield'=>array("code,label","code,label","code,label"), // List of fields (result of select to show dictionary) + 'tabfieldvalue'=>array("code,label","code,label","code,label"), // List of fields (list of fields to edit a record) + 'tabfieldinsert'=>array("code,label","code,label","code,label"), // List of fields (list of fields for insert) + 'tabrowid'=>array("rowid","rowid","rowid"), // Name of columns with primary key (try to always name it 'rowid') + 'tabcond'=>array($conf->dav->enabled,$conf->dav->enabled,$conf->dav->enabled) // Condition to show each dictionary + ); + */ + + + // Boxes/Widgets + // Add here list of php file(s) stored in dav/core/boxes that contains class to show a widget. + $this->boxes = array( + //0=>array('file'=>'davwidget1.php@dav','note'=>'Widget provided by dav','enabledbydefaulton'=>'Home'), + //1=>array('file'=>'davwidget2.php@dav','note'=>'Widget provided by dav'), + //2=>array('file'=>'davwidget3.php@dav','note'=>'Widget provided by dav') + ); + + + // Cronjobs (List of cron jobs entries to add when module is enabled) + // unit_frequency must be 60 for minute, 3600 for hour, 86400 for day, 604800 for week + $this->cronjobs = array( + //0=>array('label'=>'MyJob label', 'jobtype'=>'method', 'class'=>'/dav/class/myobject.class.php', 'objectname'=>'MyObject', 'method'=>'doScheduledJob', 'parameters'=>'', 'comment'=>'Comment', 'frequency'=>2, 'unitfrequency'=>3600, 'status'=>0, 'test'=>true) + ); + // Example: $this->cronjobs=array(0=>array('label'=>'My label', 'jobtype'=>'method', 'class'=>'/dir/class/file.class.php', 'objectname'=>'MyClass', 'method'=>'myMethod', 'parameters'=>'param1, param2', 'comment'=>'Comment', 'frequency'=>2, 'unitfrequency'=>3600, 'status'=>0, 'test'=>true), + // 1=>array('label'=>'My label', 'jobtype'=>'command', 'command'=>'', 'parameters'=>'param1, param2', 'comment'=>'Comment', 'frequency'=>1, 'unitfrequency'=>3600*24, 'status'=>0, 'test'=>true) + // ); + + + // Permissions + $this->rights = array(); // Permission array used by this module + + /* + $r=0; + $this->rights[$r][0] = $this->numero + $r; // Permission id (must not be already used) + $this->rights[$r][1] = 'Read myobject of dav'; // Permission label + $this->rights[$r][3] = 1; // Permission by default for new user (0/1) + $this->rights[$r][4] = 'read'; // In php code, permission will be checked by test if ($user->rights->dav->level1->level2) + $this->rights[$r][5] = ''; // In php code, permission will be checked by test if ($user->rights->dav->level1->level2) + + $r++; + $this->rights[$r][0] = $this->numero + $r; // Permission id (must not be already used) + $this->rights[$r][1] = 'Create/Update myobject of dav'; // Permission label + $this->rights[$r][3] = 1; // Permission by default for new user (0/1) + $this->rights[$r][4] = 'write'; // In php code, permission will be checked by test if ($user->rights->dav->level1->level2) + $this->rights[$r][5] = ''; // In php code, permission will be checked by test if ($user->rights->dav->level1->level2) + + $r++; + $this->rights[$r][0] = $this->numero + $r; // Permission id (must not be already used) + $this->rights[$r][1] = 'Delete myobject of dav'; // Permission label + $this->rights[$r][3] = 1; // Permission by default for new user (0/1) + $this->rights[$r][4] = 'delete'; // In php code, permission will be checked by test if ($user->rights->dav->level1->level2) + $this->rights[$r][5] = ''; // In php code, permission will be checked by test if ($user->rights->dav->level1->level2) + */ + + // Main menu entries + $this->menu = array(); // List of menus to add + $r=0; + + // Add here entries to declare new menus + + /* BEGIN MODULEBUILDER TOPMENU */ + /*$this->menu[$r++]=array('fk_menu'=>'', // '' if this is a top menu. For left menu, use 'fk_mainmenu=xxx' or 'fk_mainmenu=xxx,fk_leftmenu=yyy' where xxx is mainmenucode and yyy is a leftmenucode + 'type'=>'top', // This is a Top menu entry + 'titre'=>'dav', + 'mainmenu'=>'dav', + 'leftmenu'=>'', + 'url'=>'/dav/davindex.php', + 'langs'=>'dav@dav', // Lang file to use (without .lang) by module. File must be in langs/code_CODE/ directory. + 'position'=>1000+$r, + 'enabled'=>'$conf->dav->enabled', // Define condition to show or hide menu entry. Use '$conf->dav->enabled' if entry must be visible if module is enabled. + 'perms'=>'1', // Use 'perms'=>'$user->rights->dav->level1->level2' if you want your menu with a permission rules + 'target'=>'', + 'user'=>2); // 0=Menu for internal users, 1=external users, 2=both + */ + /* END MODULEBUILDER TOPMENU */ + + /* BEGIN MODULEBUILDER LEFTMENU MYOBJECT + $this->menu[$r++]=array( 'fk_menu'=>'fk_mainmenu=dav', // '' if this is a top menu. For left menu, use 'fk_mainmenu=xxx' or 'fk_mainmenu=xxx,fk_leftmenu=yyy' where xxx is mainmenucode and yyy is a leftmenucode + 'type'=>'left', // This is a Left menu entry + 'titre'=>'List MyObject', + 'mainmenu'=>'dav', + 'leftmenu'=>'dav_myobject_list', + 'url'=>'/dav/myobject_list.php', + 'langs'=>'dav@dav', // Lang file to use (without .lang) by module. File must be in langs/code_CODE/ directory. + 'position'=>1000+$r, + 'enabled'=>'$conf->dav->enabled', // Define condition to show or hide menu entry. Use '$conf->dav->enabled' if entry must be visible if module is enabled. Use '$leftmenu==\'system\'' to show if leftmenu system is selected. + 'perms'=>'1', // Use 'perms'=>'$user->rights->dav->level1->level2' if you want your menu with a permission rules + 'target'=>'', + 'user'=>2); // 0=Menu for internal users, 1=external users, 2=both + $this->menu[$r++]=array( 'fk_menu'=>'fk_mainmenu=dav,fk_leftmenu=dav', // '' if this is a top menu. For left menu, use 'fk_mainmenu=xxx' or 'fk_mainmenu=xxx,fk_leftmenu=yyy' where xxx is mainmenucode and yyy is a leftmenucode + 'type'=>'left', // This is a Left menu entry + 'titre'=>'New MyObject', + 'mainmenu'=>'dav', + 'leftmenu'=>'dav_myobject_new', + 'url'=>'/dav/myobject_page.php?action=create', + 'langs'=>'dav@dav', // Lang file to use (without .lang) by module. File must be in langs/code_CODE/ directory. + 'position'=>1000+$r, + 'enabled'=>'$conf->dav->enabled', // Define condition to show or hide menu entry. Use '$conf->dav->enabled' if entry must be visible if module is enabled. Use '$leftmenu==\'system\'' to show if leftmenu system is selected. + 'perms'=>'1', // Use 'perms'=>'$user->rights->dav->level1->level2' if you want your menu with a permission rules + 'target'=>'', + 'user'=>2); // 0=Menu for internal users, 1=external users, 2=both + END MODULEBUILDER LEFTMENU MYOBJECT */ + + + // Exports + $r=1; + + /* BEGIN MODULEBUILDER EXPORT MYOBJECT */ + /* + $langs->load("dav@dav"); + $this->export_code[$r]=$this->rights_class.'_'.$r; + $this->export_label[$r]='MyObjectLines'; // Translation key (used only if key ExportDataset_xxx_z not found) + $this->export_icon[$r]='myobject@dav'; + $keyforclass = 'MyObject'; $keyforclassfile='/mymobule/class/myobject.class.php'; $keyforelement='myobject'; + include DOL_DOCUMENT_ROOT.'/core/commonfieldsinexport.inc.php'; + $keyforselect='myobject'; $keyforaliasextra='extra'; $keyforelement='myobject'; + include DOL_DOCUMENT_ROOT.'/core/extrafieldsinexport.inc.php'; + //$this->export_dependencies_array[$r]=array('mysubobject'=>'ts.rowid', 't.myfield'=>array('t.myfield2','t.myfield3')); // To force to activate one or several fields if we select some fields that need same (like to select a unique key if we ask a field of a child to avoid the DISTINCT to discard them, or for computed field than need several other fields) + $this->export_sql_start[$r]='SELECT DISTINCT '; + $this->export_sql_end[$r] =' FROM '.MAIN_DB_PREFIX.'myobject as t'; + $this->export_sql_end[$r] .=' WHERE 1 = 1'; + $this->export_sql_end[$r] .=' AND t.entity IN ('.getEntity('myobject').')'; + $r++; */ + /* END MODULEBUILDER EXPORT MYOBJECT */ + } + + /** + * Function called when module is enabled. + * The init function add constants, boxes, permissions and menus (defined in constructor) into Dolibarr database. + * It also creates data directories + * + * @param string $options Options when enabling module ('', 'noboxes') + * @return int 1 if OK, 0 if KO + */ + public function init($options='') + { + $this->_load_tables(); + + // Create extrafields + include_once DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php'; + $extrafields = new ExtraFields($this->db); + + //$result1=$extrafields->addExtraField('myattr1', "New Attr 1 label", 'boolean', 1, 3, 'thirdparty', 0, 0, '', '', 1, '', 0, 0, '', '', 'dav@dav', '$conf->dav->enabled'); + //$result2=$extrafields->addExtraField('myattr2', "New Attr 2 label", 'varchar', 1, 10, 'project', 0, 0, '', '', 1, '', 0, 0, '', '', 'dav@dav', '$conf->dav->enabled'); + //$result3=$extrafields->addExtraField('myattr3', "New Attr 3 label", 'varchar', 1, 10, 'bank_account', 0, 0, '', '', 1, '', 0, 0, '', '', 'dav@dav', '$conf->dav->enabled'); + //$result4=$extrafields->addExtraField('myattr4', "New Attr 4 label", 'select', 1, 3, 'thirdparty', 0, 1, '', array('options'=>array('code1'=>'Val1','code2'=>'Val2','code3'=>'Val3')), 1 '', 0, 0, '', '', 'dav@dav', '$conf->dav->enabled'); + //$result5=$extrafields->addExtraField('myattr5', "New Attr 5 label", 'text', 1, 10, 'user', 0, 0, '', '', 1, '', 0, 0, '', '', 'dav@dav', '$conf->dav->enabled'); + + $sql = array(); + + return $this->_init($sql, $options); + } + + /** + * Function called when module is disabled. + * Remove from database constants, boxes and permissions from Dolibarr database. + * Data directories are not deleted + * + * @param string $options Options when enabling module ('', 'noboxes') + * @return int 1 if OK, 0 if KO + */ + public function remove($options = '') + { + $sql = array(); + + return $this->_remove($sql, $options); + } + +} diff --git a/htdocs/dav/fileserver.php b/htdocs/dav/fileserver.php new file mode 100644 index 00000000000..b5169b207b9 --- /dev/null +++ b/htdocs/dav/fileserver.php @@ -0,0 +1,103 @@ + + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * \file htdocs/dav/fileserver.php + * \ingroup dav + * \brief Server DAV + */ + +require ("../main.inc.php"); +require_once DOL_DOCUMENT_ROOT.'/core/lib/security2.lib.php'; +require_once DOL_DOCUMENT_ROOT.'/core/class/html.formcompany.class.php'; + +// Files we need +require_once DOL_DOCUMENT_ROOT.'/includes/sabre/autoload.php'; + +$user = new User($db); +if(isset($_SERVER['PHP_AUTH_USER']) && $_SERVER['PHP_AUTH_USER']!='') +{ + $user->fetch('',$_SERVER['PHP_AUTH_USER']); + $user->getrights(); +} + +$langs->loadLangs(array("main","other")); + + +//if(empty($conf->dav->enabled)) +// accessforbidden(); + +// If you want to run the SabreDAV server in a custom location (using mod_rewrite for instance) +// You can override the baseUri here. +$baseUri = DOL_URL_ROOT.'/dav/fileserver.php'; + + +// settings +$publicDir = $conf->dav->dir_output.'/public'; +$tmpDir = $conf->dav->dir_output.'/tmp'; + + +// Create the root node +// Setting up the directory tree // +$nodes = array( + // /principals + //new \Sabre\DAVACL\PrincipalCollection($principalBackend), + // /addressbook + //new \Sabre\CardDAV\AddressBookRoot($principalBackend, $carddavBackend), + // /calendars + //new \Sabre\CalDAV\CalendarRoot($principalBackend, $caldavBackend), + // / Public docs + new \Sabre\DAV\FS\Directory($dolibarr_main_data_root. '/dav/public') +); + +// The rootnode needs in turn to be passed to the server class +$server = new \Sabre\DAV\Server($nodes); + +if (isset($baseUri)) + $server->setBaseUri($baseUri); + +// Support for LOCK and UNLOCK +$lockBackend = new \Sabre\DAV\Locks\Backend\File($tmpDir . '/.locksdb'); +$lockPlugin = new \Sabre\DAV\Locks\Plugin($lockBackend); +$server->addPlugin($lockPlugin); + +// Support for html frontend +$browser = new \Sabre\DAV\Browser\Plugin(); +$server->addPlugin($browser); + +//$server->addPlugin(new \Sabre\CardDAV\Plugin()); +//$server->addPlugin(new \Sabre\CalDAV\Plugin()); +//$server->addPlugin(new \Sabre\DAVACL\Plugin()); + +// Automatically guess (some) contenttypes, based on extension +$server->addPlugin(new \Sabre\DAV\Browser\GuessContentType()); + +// Authentication backend +/*$authBackend = new \Sabre\DAV\Auth\Backend\File('.htdigest'); +$auth = new \Sabre\DAV\Auth\Plugin($authBackend); +$server->addPlugin($auth); +*/ + +// Temporary file filter +/*$tempFF = new \Sabre\DAV\TemporaryFileFilterPlugin($tmpDir); +$server->addPlugin($tempFF); +*/ + +// And off we go! +$server->exec(); + +if (is_object($db)) $db->close(); diff --git a/htdocs/includes/sabre/autoload.php b/htdocs/includes/sabre/autoload.php new file mode 100644 index 00000000000..d8ccc5f35c3 --- /dev/null +++ b/htdocs/includes/sabre/autoload.php @@ -0,0 +1,7 @@ + + * 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 http://www.php-fig.org/psr/psr-0/ + * @see http://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + private $classMapAuthoritative = false; + private $missingClasses = array(); + private $apcuPrefix; + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', $this->prefixesPsr0); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + 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 array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + 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 array|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + 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 array|string $paths The PSR-0 base directories + */ + 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 array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + 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 + */ + 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 + */ + 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 + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $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 + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * 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; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) { + if (0 === strpos($class, $prefix)) { + foreach ($this->prefixDirsPsr4[$prefix] as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) { + 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; + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/htdocs/includes/sabre/composer/LICENSE b/htdocs/includes/sabre/composer/LICENSE new file mode 100644 index 00000000000..1a28124886d --- /dev/null +++ b/htdocs/includes/sabre/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) 2016 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/htdocs/includes/sabre/composer/autoload_classmap.php b/htdocs/includes/sabre/composer/autoload_classmap.php new file mode 100644 index 00000000000..7a91153b0d8 --- /dev/null +++ b/htdocs/includes/sabre/composer/autoload_classmap.php @@ -0,0 +1,9 @@ + $vendorDir . '/sabre/uri/lib/functions.php', + '3569eecfeed3bcf0bad3c998a494ecb8' => $vendorDir . '/sabre/xml/lib/Deserializer/functions.php', + '93aa591bc4ca510c520999e34229ee79' => $vendorDir . '/sabre/xml/lib/Serializer/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', + 'ebdb698ed4152ae445614b69b5e4bb6a' => $vendorDir . '/sabre/http/lib/functions.php', +); diff --git a/htdocs/includes/sabre/composer/autoload_namespaces.php b/htdocs/includes/sabre/composer/autoload_namespaces.php new file mode 100644 index 00000000000..b7fc0125dbc --- /dev/null +++ b/htdocs/includes/sabre/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ + array($vendorDir . '/sabre/xml/lib'), + 'Sabre\\VObject\\' => array($vendorDir . '/sabre/vobject/lib'), + 'Sabre\\Uri\\' => array($vendorDir . '/sabre/uri/lib'), + 'Sabre\\HTTP\\' => array($vendorDir . '/sabre/http/lib'), + 'Sabre\\Event\\' => array($vendorDir . '/sabre/event/lib'), + 'Sabre\\DAV\\' => array($vendorDir . '/sabre/dav/lib/DAV'), + 'Sabre\\DAVACL\\' => array($vendorDir . '/sabre/dav/lib/DAVACL'), + 'Sabre\\CardDAV\\' => array($vendorDir . '/sabre/dav/lib/CardDAV'), + 'Sabre\\CalDAV\\' => array($vendorDir . '/sabre/dav/lib/CalDAV'), + 'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'), +); diff --git a/htdocs/includes/sabre/composer/autoload_real.php b/htdocs/includes/sabre/composer/autoload_real.php new file mode 100644 index 00000000000..bb9949a91c4 --- /dev/null +++ b/htdocs/includes/sabre/composer/autoload_real.php @@ -0,0 +1,70 @@ += 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); + if ($useStaticLoader) { + require_once __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInit60b9ac98a8448ede6c445b0fd4bd31e0::getInitializer($loader)); + } else { + $map = require __DIR__ . '/autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } + + $loader->register(true); + + if ($useStaticLoader) { + $includeFiles = Composer\Autoload\ComposerStaticInit60b9ac98a8448ede6c445b0fd4bd31e0::$files; + } else { + $includeFiles = require __DIR__ . '/autoload_files.php'; + } + foreach ($includeFiles as $fileIdentifier => $file) { + composerRequire60b9ac98a8448ede6c445b0fd4bd31e0($fileIdentifier, $file); + } + + return $loader; + } +} + +function composerRequire60b9ac98a8448ede6c445b0fd4bd31e0($fileIdentifier, $file) +{ + if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { + require $file; + + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; + } +} diff --git a/htdocs/includes/sabre/composer/autoload_static.php b/htdocs/includes/sabre/composer/autoload_static.php new file mode 100644 index 00000000000..d76d2c4c34b --- /dev/null +++ b/htdocs/includes/sabre/composer/autoload_static.php @@ -0,0 +1,89 @@ + __DIR__ . '/..' . '/sabre/uri/lib/functions.php', + '3569eecfeed3bcf0bad3c998a494ecb8' => __DIR__ . '/..' . '/sabre/xml/lib/Deserializer/functions.php', + '93aa591bc4ca510c520999e34229ee79' => __DIR__ . '/..' . '/sabre/xml/lib/Serializer/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', + '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\\DAV\\' => 10, + 'Sabre\\DAVACL\\' => 13, + 'Sabre\\CardDAV\\' => 14, + 'Sabre\\CalDAV\\' => 13, + ), + '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\\DAV\\' => + array ( + 0 => __DIR__ . '/..' . '/sabre/dav/lib/DAV', + ), + 'Sabre\\DAVACL\\' => + array ( + 0 => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL', + ), + 'Sabre\\CardDAV\\' => + array ( + 0 => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV', + ), + 'Sabre\\CalDAV\\' => + array ( + 0 => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV', + ), + 'Psr\\Log\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/log/Psr/Log', + ), + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInit60b9ac98a8448ede6c445b0fd4bd31e0::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInit60b9ac98a8448ede6c445b0fd4bd31e0::$prefixDirsPsr4; + + }, null, ClassLoader::class); + } +} diff --git a/htdocs/includes/sabre/composer/installed.json b/htdocs/includes/sabre/composer/installed.json new file mode 100644 index 00000000000..c226d3ef4a6 --- /dev/null +++ b/htdocs/includes/sabre/composer/installed.json @@ -0,0 +1,470 @@ +[ + { + "name": "sabre/uri", + "version": "1.2.0", + "version_normalized": "1.2.0.0", + "source": { + "type": "git", + "url": "https://github.com/fruux/sabre-uri.git", + "reference": "8545a3335f741d4b7700bb14efb41b4c03775dcd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruux/sabre-uri/zipball/8545a3335f741d4b7700bb14efb41b4c03775dcd", + "reference": "8545a3335f741d4b7700bb14efb41b4c03775dcd", + "shasum": "" + }, + "require": { + "php": ">=5.4.7" + }, + "require-dev": { + "phpunit/phpunit": "*", + "sabre/cs": "~1.0.0" + }, + "time": "2016-12-07T01:17:59+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" + ] + }, + { + "name": "sabre/xml", + "version": "1.5.0", + "version_normalized": "1.5.0.0", + "source": { + "type": "git", + "url": "https://github.com/fruux/sabre-xml.git", + "reference": "59b20e5bbace9912607481634f97d05a776ffca7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruux/sabre-xml/zipball/59b20e5bbace9912607481634f97d05a776ffca7", + "reference": "59b20e5bbace9912607481634f97d05a776ffca7", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "lib-libxml": ">=2.6.20", + "php": ">=5.5.5", + "sabre/uri": ">=1.0,<3.0.0" + }, + "require-dev": { + "phpunit/phpunit": "*", + "sabre/cs": "~1.0.0" + }, + "time": "2016-10-09T22:57:52+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Sabre\\Xml\\": "lib/" + }, + "files": [ + "lib/Deserializer/functions.php", + "lib/Serializer/functions.php" + ] + }, + "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" + ] + }, + { + "name": "sabre/vobject", + "version": "4.1.2", + "version_normalized": "4.1.2.0", + "source": { + "type": "git", + "url": "https://github.com/fruux/sabre-vobject.git", + "reference": "d0fde2fafa2a3dad1f559c2d1c2591d4fd75ae3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruux/sabre-vobject/zipball/d0fde2fafa2a3dad1f559c2d1c2591d4fd75ae3c", + "reference": "d0fde2fafa2a3dad1f559c2d1c2591d4fd75ae3c", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.5", + "sabre/xml": ">=1.5 <3.0" + }, + "require-dev": { + "phpunit/phpunit": "*", + "sabre/cs": "^1.0.0" + }, + "suggest": { + "hoa/bench": "If you would like to run the benchmark scripts" + }, + "time": "2016-12-06T04:14:09+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" + ] + }, + { + "name": "sabre/event", + "version": "3.0.0", + "version_normalized": "3.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/fruux/sabre-event.git", + "reference": "831d586f5a442dceacdcf5e9c4c36a4db99a3534" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruux/sabre-event/zipball/831d586f5a442dceacdcf5e9c4c36a4db99a3534", + "reference": "831d586f5a442dceacdcf5e9c4c36a4db99a3534", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "*", + "sabre/cs": "~0.0.4" + }, + "time": "2015-11-05T20:14:39+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Sabre\\Event\\": "lib/" + }, + "files": [ + "lib/coroutine.php", + "lib/Loop/functions.php", + "lib/Promise/functions.php" + ] + }, + "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", + "events", + "hooks", + "plugin", + "promise", + "signal" + ] + }, + { + "name": "sabre/http", + "version": "4.2.2", + "version_normalized": "4.2.2.0", + "source": { + "type": "git", + "url": "https://github.com/fruux/sabre-http.git", + "reference": "dd50e7260356f4599d40270826f9548b23efa204" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruux/sabre-http/zipball/dd50e7260356f4599d40270826f9548b23efa204", + "reference": "dd50e7260356f4599d40270826f9548b23efa204", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-mbstring": "*", + "php": ">=5.4", + "sabre/event": ">=1.0.0,<4.0.0", + "sabre/uri": "~1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.3", + "sabre/cs": "~0.0.1" + }, + "suggest": { + "ext-curl": " to make http requests with the Client class" + }, + "time": "2017-01-02T19:38:42+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" + ] + }, + { + "name": "psr/log", + "version": "1.0.2", + "version_normalized": "1.0.2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "time": "2016-10-10T12:19:37+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ] + }, + { + "name": "sabre/dav", + "version": "3.2.2", + "version_normalized": "3.2.2.0", + "source": { + "type": "git", + "url": "https://github.com/fruux/sabre-dav.git", + "reference": "e987775e619728f12205606c9cc3ee565ffb1516" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruux/sabre-dav/zipball/e987775e619728f12205606c9cc3ee565ffb1516", + "reference": "e987775e619728f12205606c9cc3ee565ffb1516", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-dom": "*", + "ext-iconv": "*", + "ext-mbstring": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "ext-spl": "*", + "lib-libxml": ">=2.7.0", + "php": ">=5.5.0", + "psr/log": "^1.0", + "sabre/event": ">=2.0.0, <4.0.0", + "sabre/http": "^4.2.1", + "sabre/uri": "^1.0.1", + "sabre/vobject": "^4.1.0", + "sabre/xml": "^1.4.0" + }, + "require-dev": { + "evert/phpdoc-md": "~0.1.0", + "monolog/monolog": "^1.18", + "phpunit/phpunit": "> 4.8, <6.0.0", + "sabre/cs": "^1.0.0" + }, + "suggest": { + "ext-curl": "*", + "ext-pdo": "*" + }, + "time": "2017-02-15T03:06:08+00:00", + "bin": [ + "bin/sabredav", + "bin/naturalselection" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.0-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Sabre\\DAV\\": "lib/DAV/", + "Sabre\\DAVACL\\": "lib/DAVACL/", + "Sabre\\CalDAV\\": "lib/CalDAV/", + "Sabre\\CardDAV\\": "lib/CardDAV/" + } + }, + "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" + ] + } +] diff --git a/htdocs/includes/sabre/psr/log/.gitignore b/htdocs/includes/sabre/psr/log/.gitignore new file mode 100644 index 00000000000..22d0d82f809 --- /dev/null +++ b/htdocs/includes/sabre/psr/log/.gitignore @@ -0,0 +1 @@ +vendor diff --git a/htdocs/includes/sabre/psr/log/LICENSE b/htdocs/includes/sabre/psr/log/LICENSE new file mode 100644 index 00000000000..474c952b4b5 --- /dev/null +++ b/htdocs/includes/sabre/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/htdocs/includes/sabre/psr/log/Psr/Log/AbstractLogger.php b/htdocs/includes/sabre/psr/log/Psr/Log/AbstractLogger.php new file mode 100644 index 00000000000..90e721af2d3 --- /dev/null +++ b/htdocs/includes/sabre/psr/log/Psr/Log/AbstractLogger.php @@ -0,0 +1,128 @@ +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. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function alert($message, array $context = array()) + { + $this->log(LogLevel::ALERT, $message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function critical($message, array $context = array()) + { + $this->log(LogLevel::CRITICAL, $message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function error($message, array $context = array()) + { + $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. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function warning($message, array $context = array()) + { + $this->log(LogLevel::WARNING, $message, $context); + } + + /** + * Normal but significant events. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function notice($message, array $context = array()) + { + $this->log(LogLevel::NOTICE, $message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function info($message, array $context = array()) + { + $this->log(LogLevel::INFO, $message, $context); + } + + /** + * Detailed debug information. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function debug($message, array $context = array()) + { + $this->log(LogLevel::DEBUG, $message, $context); + } +} diff --git a/htdocs/includes/sabre/psr/log/Psr/Log/InvalidArgumentException.php b/htdocs/includes/sabre/psr/log/Psr/Log/InvalidArgumentException.php new file mode 100644 index 00000000000..67f852d1dbc --- /dev/null +++ b/htdocs/includes/sabre/psr/log/Psr/Log/InvalidArgumentException.php @@ -0,0 +1,7 @@ +logger = $logger; + } +} diff --git a/htdocs/includes/sabre/psr/log/Psr/Log/LoggerInterface.php b/htdocs/includes/sabre/psr/log/Psr/Log/LoggerInterface.php new file mode 100644 index 00000000000..5ea72438b56 --- /dev/null +++ b/htdocs/includes/sabre/psr/log/Psr/Log/LoggerInterface.php @@ -0,0 +1,123 @@ +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. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function alert($message, array $context = array()) + { + $this->log(LogLevel::ALERT, $message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function critical($message, array $context = array()) + { + $this->log(LogLevel::CRITICAL, $message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function error($message, array $context = array()) + { + $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. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function warning($message, array $context = array()) + { + $this->log(LogLevel::WARNING, $message, $context); + } + + /** + * Normal but significant events. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function notice($message, array $context = array()) + { + $this->log(LogLevel::NOTICE, $message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function info($message, array $context = array()) + { + $this->log(LogLevel::INFO, $message, $context); + } + + /** + * Detailed debug information. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function debug($message, array $context = array()) + { + $this->log(LogLevel::DEBUG, $message, $context); + } + + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + * + * @return void + */ + abstract public function log($level, $message, array $context = array()); +} diff --git a/htdocs/includes/sabre/psr/log/Psr/Log/NullLogger.php b/htdocs/includes/sabre/psr/log/Psr/Log/NullLogger.php new file mode 100644 index 00000000000..d8cd682c8f9 --- /dev/null +++ b/htdocs/includes/sabre/psr/log/Psr/Log/NullLogger.php @@ -0,0 +1,28 @@ +logger) { }` + * blocks. + */ +class NullLogger extends AbstractLogger +{ + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + * + * @return void + */ + public function log($level, $message, array $context = array()) + { + // noop + } +} diff --git a/htdocs/includes/sabre/psr/log/Psr/Log/Test/LoggerInterfaceTest.php b/htdocs/includes/sabre/psr/log/Psr/Log/Test/LoggerInterfaceTest.php new file mode 100644 index 00000000000..a0391a52b8f --- /dev/null +++ b/htdocs/includes/sabre/psr/log/Psr/Log/Test/LoggerInterfaceTest.php @@ -0,0 +1,140 @@ + ". + * + * Example ->error('Foo') would yield "error Foo". + * + * @return string[] + */ + abstract public function getLogs(); + + public function testImplements() + { + $this->assertInstanceOf('Psr\Log\LoggerInterface', $this->getLogger()); + } + + /** + * @dataProvider provideLevelsAndMessages + */ + public function testLogsAtAllLevels($level, $message) + { + $logger = $this->getLogger(); + $logger->{$level}($message, array('user' => 'Bob')); + $logger->log($level, $message, array('user' => 'Bob')); + + $expected = array( + $level.' message of level '.$level.' with context: Bob', + $level.' message of level '.$level.' with context: Bob', + ); + $this->assertEquals($expected, $this->getLogs()); + } + + public function provideLevelsAndMessages() + { + return array( + LogLevel::EMERGENCY => array(LogLevel::EMERGENCY, 'message of level emergency with context: {user}'), + LogLevel::ALERT => array(LogLevel::ALERT, 'message of level alert with context: {user}'), + LogLevel::CRITICAL => array(LogLevel::CRITICAL, 'message of level critical with context: {user}'), + LogLevel::ERROR => array(LogLevel::ERROR, 'message of level error with context: {user}'), + LogLevel::WARNING => array(LogLevel::WARNING, 'message of level warning with context: {user}'), + LogLevel::NOTICE => array(LogLevel::NOTICE, 'message of level notice with context: {user}'), + LogLevel::INFO => array(LogLevel::INFO, 'message of level info with context: {user}'), + LogLevel::DEBUG => array(LogLevel::DEBUG, 'message of level debug with context: {user}'), + ); + } + + /** + * @expectedException \Psr\Log\InvalidArgumentException + */ + public function testThrowsOnInvalidLevel() + { + $logger = $this->getLogger(); + $logger->log('invalid level', 'Foo'); + } + + public function testContextReplacement() + { + $logger = $this->getLogger(); + $logger->info('{Message {nothing} {user} {foo.bar} a}', array('user' => 'Bob', 'foo.bar' => 'Bar')); + + $expected = array('info {Message {nothing} Bob Bar a}'); + $this->assertEquals($expected, $this->getLogs()); + } + + public function testObjectCastToString() + { + if (method_exists($this, 'createPartialMock')) { + $dummy = $this->createPartialMock('Psr\Log\Test\DummyTest', array('__toString')); + } else { + $dummy = $this->getMock('Psr\Log\Test\DummyTest', array('__toString')); + } + $dummy->expects($this->once()) + ->method('__toString') + ->will($this->returnValue('DUMMY')); + + $this->getLogger()->warning($dummy); + + $expected = array('warning DUMMY'); + $this->assertEquals($expected, $this->getLogs()); + } + + public function testContextCanContainAnything() + { + $context = array( + 'bool' => true, + 'null' => null, + 'string' => 'Foo', + 'int' => 0, + 'float' => 0.5, + 'nested' => array('with object' => new DummyTest), + 'object' => new \DateTime, + 'resource' => fopen('php://memory', 'r'), + ); + + $this->getLogger()->warning('Crazy context data', $context); + + $expected = array('warning Crazy context data'); + $this->assertEquals($expected, $this->getLogs()); + } + + public function testContextExceptionKeyCanBeExceptionOrOtherValues() + { + $logger = $this->getLogger(); + $logger->warning('Random message', array('exception' => 'oops')); + $logger->critical('Uncaught Exception!', array('exception' => new \LogicException('Fail'))); + + $expected = array( + 'warning Random message', + 'critical Uncaught Exception!' + ); + $this->assertEquals($expected, $this->getLogs()); + } +} + +class DummyTest +{ + public function __toString() + { + } +} diff --git a/htdocs/includes/sabre/psr/log/README.md b/htdocs/includes/sabre/psr/log/README.md new file mode 100644 index 00000000000..574bc1cb2a8 --- /dev/null +++ b/htdocs/includes/sabre/psr/log/README.md @@ -0,0 +1,45 @@ +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. + +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'); + } + + // 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/htdocs/includes/sabre/psr/log/composer.json b/htdocs/includes/sabre/psr/log/composer.json new file mode 100644 index 00000000000..87934d707e7 --- /dev/null +++ b/htdocs/includes/sabre/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": "http://www.php-fig.org/" + } + ], + "require": { + "php": ">=5.3.0" + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + } +} diff --git a/htdocs/includes/sabre/sabre/dav/.gitignore b/htdocs/includes/sabre/sabre/dav/.gitignore new file mode 100644 index 00000000000..6cf24588320 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/.gitignore @@ -0,0 +1,43 @@ +# Unit tests +tests/temp +tests/.sabredav +tests/cov + +# Custom settings for tests +tests/config.user.php + +# ViM +*.swp + +# Composer +composer.lock +vendor + +# Composer binaries +bin/phing +bin/phpunit +bin/vobject +bin/generate_vcards +bin/phpdocmd +bin/phpunit +bin/php-cs-fixer +bin/sabre-cs-fixer + +# Assuming every .php file in the root is for testing +/*.php + +# Other testing stuff +/tmpdata +/data +/public + +# Build +build +build.properties + +# Docs +docs/api +docs/wikidocs + +# Mac +.DS_Store diff --git a/htdocs/includes/sabre/sabre/dav/.travis.yml b/htdocs/includes/sabre/sabre/dav/.travis.yml new file mode 100644 index 00000000000..85637048ad4 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/.travis.yml @@ -0,0 +1,36 @@ +language: php +php: + - 5.5 + - 5.6 + - 7.0 + - 7.1 + + +env: + matrix: + - LOWEST_DEPS="" TEST_DEPS="" + - LOWEST_DEPS="--prefer-lowest" TEST_DEPS="tests/Sabre/" + +services: + - mysql + - postgresql + +sudo: false + +before_script: + - mysql -e 'create database sabredav_test' + - psql -c "create database sabredav_test" -U postgres + - psql -c "create user sabredav with PASSWORD 'sabredav';GRANT ALL PRIVILEGES ON DATABASE sabredav_test TO sabredav" -U postgres + # - composer self-update + - composer update --prefer-dist $LOWEST_DEPS + +# addons: +# postgresql: "9.5" + +script: + - ./bin/phpunit --configuration tests/phpunit.xml.dist $TEST_DEPS + - ./bin/sabre-cs-fixer fix . --dry-run --diff + +cache: + directories: + - $HOME/.composer/cache diff --git a/htdocs/includes/sabre/sabre/dav/CONTRIBUTING.md b/htdocs/includes/sabre/sabre/dav/CONTRIBUTING.md new file mode 100644 index 00000000000..425ee19ba85 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/CONTRIBUTING.md @@ -0,0 +1,87 @@ +Contributing to sabre projects +============================== + +Want to contribute to sabre/dav? Here are some guidelines to ensure your patch +gets accepted. + + +Building a new feature? Contact us first +---------------------------------------- + +We may not want to accept every feature that comes our way. Sometimes +features are out of scope for our projects. + +We don't want to waste your time, so by having a quick chat with us first, +you may find out quickly if the feature makes sense to us, and we can give +some tips on how to best build the feature. + +If we don't accept the feature, it could be for a number of reasons. For +instance, we've rejected features in the past because we felt uncomfortable +assuming responsibility for maintaining the feature. + +In those cases, it's often possible to keep the feature separate from the +sabre projects. sabre/dav for instance has a plugin system, and there's no +reason the feature can't live in a project you own. + +In that case, definitely let us know about your plugin as well, so we can +feature it on [sabre.io][4]. + +We are often on [IRC][5], in the #sabredav channel on freenode. If there's +no one there, post a message on the [mailing list][6]. + + +Coding standards +---------------- + +sabre projects follow: + +1. [PSR-1][1] +2. [PSR-4][2] + +sabre projects don't follow [PSR-2][3]. + +In addition to that, here's a list of basic rules: + +1. PHP 5.4 array syntax must be used every where. This means you use `[` and + `]` instead of `array(` and `)`. +2. Use PHP namespaces everywhere. +3. Use 4 spaces for indentation. +4. Try to keep your lines under 80 characters. This is not a hard rule, as + there are many places in the source where it felt more sensibile to not + do so. In particular, function declarations are never split over multiple + lines. +5. Opening braces (`{`) are _always_ on the same line as the `class`, `if`, + `function`, etc. they belong to. +6. `public` must be omitted from method declarations. It must also be omitted + for static properties. +7. All files should use unix-line endings (`\n`). +8. Files must omit the closing php tag (`?>`). +9. `true`, `false` and `null` are always lower-case. +10. Constants are always upper-case. +11. Any of the rules stated before may be broken where this is the pragmatic + thing to do. + + +Unit test requirements +---------------------- + +Any new feature or change requires unittests. We use [PHPUnit][7] for all our +tests. + +Adding unittests will greatly increase the likelyhood of us quickly accepting +your pull request. If unittests are not included though for whatever reason, +we'd still _love_ your pull request. + +We may have to write the tests ourselves, which can increase the time it takes +to accept the patch, but we'd still really like your contribution! + +To run the testsuite jump into the directory `cd tests` and trigger `phpunit`. +Make sure you did a `composer install` beforehand. + +[1]: http://www.php-fig.org/psr/psr-1/ +[2]: http://www.php-fig.org/psr/psr-4/ +[3]: http://www.php-fig.org/psr/psr-2/ +[4]: http://sabre.io/ +[5]: irc://freenode.net/#sabredav +[6]: http://groups.google.com/group/sabredav-discuss +[7]: http://phpunit.de/ diff --git a/htdocs/includes/sabre/sabre/dav/bin/build.php b/htdocs/includes/sabre/sabre/dav/bin/build.php new file mode 100644 index 00000000000..c4ba2094161 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/bin/build.php @@ -0,0 +1,177 @@ +#!/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"; + die(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"; + die(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 ($code != 0) { + echo "PHPUnit reported error code $code\n"; + die(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 ($code !== 0) { + echo "Composer reported error code $code\n"; + die(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/htdocs/includes/sabre/sabre/dav/bin/googlecode_upload.py b/htdocs/includes/sabre/sabre/dav/bin/googlecode_upload.py new file mode 100644 index 00000000000..caafd5dedac --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/bin/googlecode_upload.py @@ -0,0 +1,248 @@ +#!/usr/bin/env python +# +# Copyright 2006, 2007 Google Inc. All Rights Reserved. +# Author: danderson@google.com (David Anderson) +# +# Script for uploading files to a Google Code project. +# +# This is intended to be both a useful script for people who want to +# streamline project uploads and a reference implementation for +# uploading files to Google Code projects. +# +# To upload a file to Google Code, you need to provide a path to the +# file on your local machine, a small summary of what the file is, a +# project name, and a valid account that is a member or owner of that +# project. You can optionally provide a list of labels that apply to +# the file. The file will be uploaded under the same name that it has +# in your local filesystem (that is, the "basename" or last path +# component). Run the script with '--help' to get the exact syntax +# and available options. +# +# Note that the upload script requests that you enter your +# googlecode.com password. This is NOT your Gmail account password! +# This is the password you use on googlecode.com for committing to +# Subversion and uploading files. You can find your password by going +# to http://code.google.com/hosting/settings when logged in with your +# Gmail account. If you have already committed to your project's +# Subversion repository, the script will automatically retrieve your +# credentials from there (unless disabled, see the output of '--help' +# for details). +# +# If you are looking at this script as a reference for implementing +# your own Google Code file uploader, then you should take a look at +# the upload() function, which is the meat of the uploader. You +# basically need to build a multipart/form-data POST request with the +# right fields and send it to https://PROJECT.googlecode.com/files . +# Authenticate the request using HTTP Basic authentication, as is +# shown below. +# +# Licensed under the terms of the Apache Software License 2.0: +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Questions, comments, feature requests and patches are most welcome. +# Please direct all of these to the Google Code users group: +# http://groups.google.com/group/google-code-hosting + +"""Google Code file uploader script. +""" + +__author__ = 'danderson@google.com (David Anderson)' + +import httplib +import os.path +import optparse +import getpass +import base64 +import sys + + +def upload(file, project_name, user_name, password, summary, labels=None): + """Upload a file to a Google Code project's file server. + + Args: + file: The local path to the file. + project_name: The name of your project on Google Code. + user_name: Your Google account name. + password: The googlecode.com password for your account. + Note that this is NOT your global Google Account password! + summary: A small description for the file. + labels: an optional list of label strings with which to tag the file. + + Returns: a tuple: + http_status: 201 if the upload succeeded, something else if an + error occurred. + http_reason: The human-readable string associated with http_status + file_url: If the upload succeeded, the URL of the file on Google + Code, None otherwise. + """ + # The login is the user part of user@gmail.com. If the login provided + # is in the full user@domain form, strip it down. + if user_name.endswith('@gmail.com'): + user_name = user_name[:user_name.index('@gmail.com')] + + form_fields = [('summary', summary)] + if labels is not None: + form_fields.extend([('label', l.strip()) for l in labels]) + + content_type, body = encode_upload_request(form_fields, file) + + upload_host = '%s.googlecode.com' % project_name + upload_uri = '/files' + auth_token = base64.b64encode('%s:%s'% (user_name, password)) + headers = { + 'Authorization': 'Basic %s' % auth_token, + 'User-Agent': 'Googlecode.com uploader v0.9.4', + 'Content-Type': content_type, + } + + server = httplib.HTTPSConnection(upload_host) + server.request('POST', upload_uri, body, headers) + resp = server.getresponse() + server.close() + + if resp.status == 201: + location = resp.getheader('Location', None) + else: + location = None + return resp.status, resp.reason, location + + +def encode_upload_request(fields, file_path): + """Encode the given fields and file into a multipart form body. + + fields is a sequence of (name, value) pairs. file is the path of + the file to upload. The file will be uploaded to Google Code with + the same file name. + + Returns: (content_type, body) ready for httplib.HTTP instance + """ + BOUNDARY = '----------Googlecode_boundary_reindeer_flotilla' + CRLF = '\r\n' + + body = [] + + # Add the metadata about the upload first + for key, value in fields: + body.extend( + ['--' + BOUNDARY, + 'Content-Disposition: form-data; name="%s"' % key, + '', + value, + ]) + + # Now add the file itself + file_name = os.path.basename(file_path) + f = open(file_path, 'rb') + file_content = f.read() + f.close() + + body.extend( + ['--' + BOUNDARY, + 'Content-Disposition: form-data; name="filename"; filename="%s"' + % file_name, + # The upload server determines the mime-type, no need to set it. + 'Content-Type: application/octet-stream', + '', + file_content, + ]) + + # Finalize the form body + body.extend(['--' + BOUNDARY + '--', '']) + + return 'multipart/form-data; boundary=%s' % BOUNDARY, CRLF.join(body) + + +def upload_find_auth(file_path, project_name, summary, labels=None, + user_name=None, password=None, tries=3): + """Find credentials and upload a file to a Google Code project's file server. + + file_path, project_name, summary, and labels are passed as-is to upload. + + Args: + file_path: The local path to the file. + project_name: The name of your project on Google Code. + summary: A small description for the file. + labels: an optional list of label strings with which to tag the file. + config_dir: Path to Subversion configuration directory, 'none', or None. + user_name: Your Google account name. + tries: How many attempts to make. + """ + + while tries > 0: + if user_name is None: + # Read username if not specified or loaded from svn config, or on + # subsequent tries. + sys.stdout.write('Please enter your googlecode.com username: ') + sys.stdout.flush() + user_name = sys.stdin.readline().rstrip() + if password is None: + # Read password if not loaded from svn config, or on subsequent tries. + print 'Please enter your googlecode.com password.' + print '** Note that this is NOT your Gmail account password! **' + print 'It is the password you use to access Subversion repositories,' + print 'and can be found here: http://code.google.com/hosting/settings' + password = getpass.getpass() + + status, reason, url = upload(file_path, project_name, user_name, password, + summary, labels) + # Returns 403 Forbidden instead of 401 Unauthorized for bad + # credentials as of 2007-07-17. + if status in [httplib.FORBIDDEN, httplib.UNAUTHORIZED]: + # Rest for another try. + user_name = password = None + tries = tries - 1 + else: + # We're done. + break + + return status, reason, url + + +def main(): + parser = optparse.OptionParser(usage='googlecode-upload.py -s SUMMARY ' + '-p PROJECT [options] FILE') + parser.add_option('-s', '--summary', dest='summary', + help='Short description of the file') + parser.add_option('-p', '--project', dest='project', + help='Google Code project name') + parser.add_option('-u', '--user', dest='user', + help='Your Google Code username') + parser.add_option('-w', '--password', dest='password', + help='Your Google Code password') + parser.add_option('-l', '--labels', dest='labels', + help='An optional list of comma-separated labels to attach ' + 'to the file') + + options, args = parser.parse_args() + + if not options.summary: + parser.error('File summary is missing.') + elif not options.project: + parser.error('Project name is missing.') + elif len(args) < 1: + parser.error('File to upload not provided.') + elif len(args) > 1: + parser.error('Only one file may be specified.') + + file_path = args[0] + + if options.labels: + labels = options.labels.split(',') + else: + labels = None + + status, reason, url = upload_find_auth(file_path, options.project, + options.summary, labels, + options.user, options.password) + if url: + print 'The file was uploaded successfully.' + print 'URL: %s' % url + return 0 + else: + print 'An error occurred. Your file was not uploaded.' + print 'Google Code upload server said: %s (%s)' % (reason, status) + return 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/htdocs/includes/sabre/sabre/dav/bin/migrateto20.php b/htdocs/includes/sabre/sabre/dav/bin/migrateto20.php new file mode 100644 index 00000000000..77236804f35 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/bin/migrateto20.php @@ -0,0 +1,453 @@ +#!/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"; + die(-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/htdocs/includes/sabre/sabre/dav/bin/migrateto21.php b/htdocs/includes/sabre/sabre/dav/bin/migrateto21.php new file mode 100644 index 00000000000..c81ee5cca1a --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/bin/migrateto21.php @@ -0,0 +1,176 @@ +#!/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"; + die(-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/htdocs/includes/sabre/sabre/dav/bin/migrateto30.php b/htdocs/includes/sabre/sabre/dav/bin/migrateto30.php new file mode 100644 index 00000000000..9ca77c13c3c --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/bin/migrateto30.php @@ -0,0 +1,171 @@ +#!/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"; + die(-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/htdocs/includes/sabre/sabre/dav/bin/migrateto32.php b/htdocs/includes/sabre/sabre/dav/bin/migrateto32.php new file mode 100644 index 00000000000..7567aeb60f7 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/bin/migrateto32.php @@ -0,0 +1,268 @@ +#!/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"; + die(-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 ($columnCount === 3) { + 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/htdocs/includes/sabre/sabre/dav/bin/sabredav b/htdocs/includes/sabre/sabre/dav/bin/sabredav new file mode 100644 index 00000000000..032371ba8bc --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/bin/sabredav @@ -0,0 +1,2 @@ +#!/bin/sh +php -S 0.0.0.0:8080 `dirname $0`/sabredav.php diff --git a/htdocs/includes/sabre/sabre/dav/bin/sabredav.php b/htdocs/includes/sabre/sabre/dav/bin/sabredav.php new file mode 100644 index 00000000000..950075d1af7 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/bin/sabredav.php @@ -0,0 +1,53 @@ +stream = fopen('php://stdout', 'w'); + + } + + function log($msg) { + fwrite($this->stream, $msg . "\n"); + } + +} + +$log = new CliLog(); + +if (php_sapi_name() !== 'cli-server') { + die("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/htdocs/includes/sabre/sabre/dav/composer.json b/htdocs/includes/sabre/sabre/dav/composer.json new file mode 100644 index 00000000000..fca0e07fbd4 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/composer.json @@ -0,0 +1,68 @@ +{ + "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": ">=5.5.0", + "sabre/vobject": "^4.1.0", + "sabre/event" : ">=2.0.0, <4.0.0", + "sabre/xml" : "^1.4.0", + "sabre/http" : "^4.2.1", + "sabre/uri" : "^1.0.1", + "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" + }, + "require-dev" : { + "phpunit/phpunit" : "> 4.8, <6.0.0", + "evert/phpdoc-md" : "~0.1.0", + "sabre/cs" : "^1.0.0", + "monolog/monolog": "^1.18" + }, + "suggest" : { + "ext-curl" : "*", + "ext-pdo" : "*" + }, + "autoload": { + "psr-4" : { + "Sabre\\DAV\\" : "lib/DAV/", + "Sabre\\DAVACL\\" : "lib/DAVACL/", + "Sabre\\CalDAV\\" : "lib/CalDAV/", + "Sabre\\CardDAV\\" : "lib/CardDAV/" + } + }, + "support" : { + "forum" : "https://groups.google.com/group/sabredav-discuss", + "source" : "https://github.com/fruux/sabre-dav" + }, + "bin" : [ + "bin/sabredav", + "bin/naturalselection" + ], + "config" : { + "bin-dir" : "./bin" + }, + "extra" : { + "branch-alias": { + "dev-master": "3.1.0-dev" + } + } +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php new file mode 100644 index 00000000000..311b1c41509 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php @@ -0,0 +1,226 @@ +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 adviced 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 interprete 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 + * @param array $filters + * @return array + */ + 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. + * + * @param array $object + * @param array $filters + * @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 + */ + 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/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Backend/BackendInterface.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Backend/BackendInterface.php new file mode 100644 index 00000000000..bd8ee760228 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Backend/BackendInterface.php @@ -0,0 +1,270 @@ + '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 + * + * @param \PDO $pdo + */ + 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 + */ + 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 compatbility. Might go away in + // the future. + $calendar['read-only'] = (int)$row['access'] === \Sabre\DAV\Sharing\Plugin::ACCESS_READ; + } + + 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 + * @param array $properties + * @return string + */ + 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'] = $properties[$transp]->getValue() === 'transparent' ? 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 + * @param \Sabre\DAV\PropPatch $propPatch + * @return void + */ + function updateCalendar($calendarId, \Sabre\DAV\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] = $propertyValue->getValue() === 'transparent'; + 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 + * @return void + */ + 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 ($access === \Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER) { + + /** + * 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 + */ + 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 + */ + 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 + * @param array $uris + * @return array + */ + 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 + */ + 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 + */ + 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 ($component->name !== 'VTIMEZONE') { + $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 ($componentType === 'VEVENT') { + $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 + * @return void + */ + 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 adviced 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 + * @param array $filters + * @return array + */ + 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 + if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) { + $requirePostFilter = false; + } + // There was a time-range filter + if ($componentType == 'VEVENT' && isset($filters['comp-filters'][0]['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['start'] || !$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 && $timeRange['start']) { + $query .= " AND lastoccurence > :startdate"; + $values['startdate'] = $timeRange['start']->getTimeStamp(); + } + if ($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 + */ + 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 = ? +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 + */ + 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; + + // 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, + 'added' => [], + 'modified' => [], + 'deleted' => [], + ]; + + if ($syncToken) { + + $query = "SELECT uri, operation FROM " . $this->calendarChangesTableName . " WHERE synctoken >= ? AND synctoken < ? AND calendarid = ? ORDER BY synctoken"; + if ($limit > 0) $query .= " LIMIT " . (int)$limit; + + // Fetching all changes + $stmt = $this->pdo->prepare($query); + $stmt->execute([$syncToken, $currentToken, $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['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->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. + * @return void + */ + 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 + */ + 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 + * @param array $properties + * @return mixed + */ + 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 + * @param \Sabre\DAV\PropPatch $propPatch + * @return void + */ + function updateSubscription($subscriptionId, DAV\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 ($propertyName === '{http://calendarserver.org/ns/}source') { + $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 + * @return void + */ + 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 + */ + 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 + */ + 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 + * @return void + */ + 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 $objectData + * @return void + */ + function createSchedulingObject($principalUri, $objectUri, $objectData) { + + $stmt = $this->pdo->prepare('INSERT INTO ' . $this->schedulingObjectTableName . ' (principaluri, calendardata, uri, lastmodified, etag, size) VALUES (?, ?, ?, ?, ?, ?)'); + $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 + * @return void + */ + 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 ($sharee->access === \Sabre\DAV\Sharing\Plugin::ACCESS_NOACCESS) { + // 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[] + */ + 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 + * @return void + */ + function setPublishStatus($calendarId, $value) { + + throw new DAV\Exception\NotImplemented('Not implemented'); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php new file mode 100644 index 00000000000..6ec0bf06b9f --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php @@ -0,0 +1,65 @@ +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 + */ + 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 + * @param array $properties + * @return string + */ + 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 + * @return void + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + * @return void + */ + function deleteCalendarObject($calendarId, $objectUri) { + + $stmt = $this->pdo->prepare('DELETE FROM simple_calendarobjects WHERE calendarid = ? AND uri = ?'); + $stmt->execute([$calendarId, $objectUri]); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php new file mode 100644 index 00000000000..d77a2fe0fd5 --- /dev/null +++ b/htdocs/includes/sabre/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 + */ + function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null); + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Calendar.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Calendar.php new file mode 100644 index 00000000000..7467900ccdc --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Calendar.php @@ -0,0 +1,472 @@ +caldavBackend = $caldavBackend; + $this->calendarInfo = $calendarInfo; + + } + + /** + * Returns the name of the calendar + * + * @return string + */ + 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. + * + * @param PropPatch $propPatch + * @return void + */ + function propPatch(PropPatch $propPatch) { + + return $this->caldavBackend->updateCalendar($this->calendarInfo['id'], $propPatch); + + } + + /** + * Returns the list of properties + * + * @param array $requestedProperties + * @return array + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + * @return void + */ + 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 $calendarData + * @return string|null + */ + function createFile($name, $calendarData = null) { + + if (is_resource($calendarData)) { + $calendarData = stream_get_contents($calendarData); + } + return $this->caldavBackend->createCalendarObject($this->calendarInfo['id'], $name, $calendarData); + + } + + /** + * Deletes the calendar. + * + * @return void + */ + 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 + * @return void + */ + function setName($newName) { + + throw new DAV\Exception\MethodNotAllowed('Renaming calendars is not yet supported'); + + } + + /** + * Returns the last modification date as a unix timestamp. + * + * @return null + */ + 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 + */ + 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 + */ + 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 + */ + 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. + * + * @param array $filters + * @return array + */ + 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 + */ + 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' + * ] + * ]; + * + * 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 + */ + 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/htdocs/includes/sabre/sabre/dav/lib/CalDAV/CalendarHome.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/CalendarHome.php new file mode 100644 index 00000000000..ffd7f72fb69 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/CalendarHome.php @@ -0,0 +1,378 @@ +caldavBackend = $caldavBackend; + $this->principalInfo = $principalInfo; + + } + + /** + * Returns the name of this object + * + * @return string + */ + function getName() { + + list(, $name) = URLUtil::splitPath($this->principalInfo['uri']); + return $name; + + } + + /** + * Updates the name of this object + * + * @param string $name + * @return void + */ + function setName($name) { + + throw new DAV\Exception\Forbidden(); + + } + + /** + * Deletes this object + * + * @return void + */ + function delete() { + + throw new DAV\Exception\Forbidden(); + + } + + /** + * Returns the last modification date + * + * @return int + */ + function getLastModified() { + + return null; + + } + + /** + * Creates a new file under this object. + * + * This is currently not allowed + * + * @param string $filename + * @param resource $data + * @return void + */ + function createFile($filename, $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 + * @return void + */ + 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 + */ + function getChild($name) { + + // Special nodes + if ($name === 'inbox' && $this->caldavBackend instanceof Backend\SchedulingSupport) { + return new Schedule\Inbox($this->caldavBackend, $this->principalInfo['uri']); + } + if ($name === 'outbox' && $this->caldavBackend instanceof Backend\SchedulingSupport) { + return new Schedule\Outbox($this->principalInfo['uri']); + } + if ($name === 'notifications' && $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 + */ + function childExists($name) { + + try { + return !!$this->getChild($name); + } catch (NotFound $e) { + return false; + } + + } + + /** + * Returns a list of calendars + * + * @return array + */ + 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 + * @param MkCol $mkCol + * @throws DAV\Exception\InvalidResourceType + * @return void + */ + 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 + */ + 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 + */ + 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 null|string + */ + 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 + */ + function getCalendarObjectByUID($uid) { + + return $this->caldavBackend->getCalendarObjectByUID($this->principalInfo['uri'], $uid); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CalDAV/CalendarObject.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/CalendarObject.php new file mode 100644 index 00000000000..9d6532a35b1 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/CalendarObject.php @@ -0,0 +1,237 @@ +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 + */ + function getName() { + + return $this->objectData['uri']; + + } + + /** + * Returns the ICalendar-formatted object + * + * @return string + */ + 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 + */ + 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 + * + * @return void + */ + function delete() { + + $this->caldavBackend->deleteCalendarObject($this->calendarInfo['id'], $this->objectData['uri']); + + } + + /** + * Returns the mime content-type + * + * @return string + */ + 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 + */ + 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 + */ + function getLastModified() { + + return $this->objectData['lastmodified']; + + } + + /** + * Returns the size of this object in bytes + * + * @return int + */ + 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 + */ + 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 + */ + 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/htdocs/includes/sabre/sabre/dav/lib/CalDAV/CalendarQueryValidator.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/CalendarQueryValidator.php new file mode 100644 index 00000000000..df8008fe276 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/CalendarQueryValidator.php @@ -0,0 +1,375 @@ +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. + * + * @param VObject\Component $parent + * @param array $filters + * @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 ($filter['time-range']) { + foreach ($parent->{$filter['name']} as $subComponent) { + if ($this->validateTimeRange($subComponent, $filter['time-range']['start'], $filter['time-range']['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. + * + * @param VObject\Component $parent + * @param array $filters + * @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 ($filter['time-range']) { + foreach ($parent->{$filter['name']} as $subComponent) { + if ($this->validateTimeRange($subComponent, $filter['time-range']['start'], $filter['time-range']['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. + * + * @param VObject\Property $parent + * @param array $filters + * @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. + * @param array $textMatch + * @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 VObject\Node $component + * @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 ($component->parent->name === 'VEVENT' && $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 ($expandedEvent->VALARM !== null) { + foreach ($expandedEvent->VALARM as $expandedAlarm) { + + $effectiveTrigger = $expandedAlarm->getEffectiveTriggerTime(); + if ($expandedAlarm->isInTimeRange($start, $end)) { + return true; + } + + if ((string)$expandedAlarm->TRIGGER['VALUE'] === 'DATE-TIME') { + // 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); + } + + 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/htdocs/includes/sabre/sabre/dav/lib/CalDAV/CalendarRoot.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/CalendarRoot.php new file mode 100644 index 00000000000..1d6b2ac9ff8 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/CalendarRoot.php @@ -0,0 +1,80 @@ +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 + */ + 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. + * + * @param array $principal + * @return \Sabre\DAV\INode + */ + function getChildForPrincipal(array $principal) { + + return new CalendarHome($this->caldavBackend, $principal); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php new file mode 100644 index 00000000000..7aff2edab55 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php @@ -0,0 +1,35 @@ +ownerDocument; + + $np = $doc->createElementNS(CalDAV\Plugin::NS_CALDAV, 'cal:supported-calendar-component'); + $errorNode->appendChild($np); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CalDAV/ICSExportPlugin.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/ICSExportPlugin.php new file mode 100644 index 00000000000..fc8b971f3b4 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/ICSExportPlugin.php @@ -0,0 +1,378 @@ +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. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + 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']) && !!$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\Util::Negotiate( + $request->getHeader('Accept'), + [ + 'text/calendar', + 'application/calendar+json', + ] + ); + + if (isset($queryParams['accept'])) { + if ($queryParams['accept'] === 'application/calendar+json' || $queryParams['accept'] === 'jcal') { + $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 + * @param ResponseInterface $response + */ + 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 + * @param array $inputObjects + * @return VObject\Component\VCalendar + */ + 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)) continue; + + $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 + */ + 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 + */ + 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/htdocs/includes/sabre/sabre/dav/lib/CalDAV/ICalendar.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/ICalendar.php new file mode 100644 index 00000000000..7cf4b12561a --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/ICalendar.php @@ -0,0 +1,18 @@ +caldavBackend = $caldavBackend; + $this->principalUri = $principalUri; + + } + + /** + * Returns all notifications for a principal + * + * @return array + */ + 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 + */ + 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 + */ + function getOwner() { + + return $this->principalUri; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Notifications/ICollection.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Notifications/ICollection.php new file mode 100644 index 00000000000..008e87435a0 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Notifications/ICollection.php @@ -0,0 +1,23 @@ +caldavBackend = $caldavBackend; + $this->principalUri = $principalUri; + $this->notification = $notification; + + } + + /** + * Returns the path name for this notification + * + * @return string + */ + function getName() { + + return $this->notification->getId() . '.xml'; + + } + + /** + * Returns the etag for the notification. + * + * The etag must be surrounded by litteral double-quotes. + * + * @return string + */ + function getETag() { + + return $this->notification->getETag(); + + } + + /** + * This method must return an xml element, using the + * Sabre\CalDAV\Xml\Notification\NotificationInterface classes. + * + * @return NotificationInterface + */ + function getNotificationType() { + + return $this->notification; + + } + + /** + * Deletes this notification + * + * @return void + */ + function delete() { + + $this->caldavBackend->deleteNotification($this->getOwner(), $this->notification); + + } + + /** + * Returns the owner principal + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + function getOwner() { + + return $this->principalUri; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Notifications/Plugin.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Notifications/Plugin.php new file mode 100644 index 00000000000..e742351f5be --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Notifications/Plugin.php @@ -0,0 +1,180 @@ +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 + * + * @param PropFind $propFind + * @param BaseINode $node + * @return void + */ + 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. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return void + */ + 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 + */ + 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/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Plugin.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Plugin.php new file mode 100644 index 00000000000..def11d52dff --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Plugin.php @@ -0,0 +1,1068 @@ +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 + */ + 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 (count($parts) !== 2) return; + if ($parts[0] !== 'principals') return; + + return self::CALENDAR_ROOT . '/' . $parts[1]; + + } + + /** + * Returns a list of features for the DAV: HTTP header. + * + * @return array + */ + 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 + */ + 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 + */ + 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 + * + * @param DAV\Server $server + * @return void + */ + 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 + */ + 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. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + 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(), null, $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. + * + * @param DAV\PropFind $propFind + * @param DAV\INode $node + * @return void + */ + 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 (substr($address, 0, 7) === 'mailto:') { + $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 ($propFind->getStatus($propRead) === 404 || $propFind->getStatus($propWrite) === 404) { + + $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 + * @return void + */ + function calendarMultiGetReport($report) { + + $needsJson = $report->contentType === 'application/calendar+json'; + + $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, $prefer['return'] === 'minimal')); + + } + + /** + * 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 + * @return void + */ + function calendarQueryReport($report) { + + $path = $this->server->getRequestUri(); + + $needsJson = $report->contentType === 'application/calendar+json'; + + $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 ($depth == 0 && $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 && $depth === 0) { + + if (strpos($this->server->httpRequest->getHeader('User-Agent'), 'MSFT-') === 0) { + // 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 && $depth == 1) { + + $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, $prefer['return'] === 'minimal')); + + } + + /** + * This method is responsible for parsing the request and generating the + * response for the CALDAV:free-busy-query REPORT. + * + * @param Xml\Request\FreeBusyQueryReport $report + * @return void + */ + 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 DAV\IFile $node + * @param resource $data + * @param bool $modified Should be set to true, if this event handler + * changed &$data. + * @return void + */ + function beforeWriteContent($path, DAV\IFile $node, &$data, &$modified) { + + if (!$node instanceof ICalendarObject) + return; + + // We're onyl 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 DAV\ICollection $parentNode + * @param bool $modified Should be set to true, if this event handler + * changed &$data. + * @return void + */ + 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. + * @return void + */ + 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 ($vobj->name !== 'VCALENDAR') { + 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 ($prefer['handling'] !== 'strict') { + $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 && strcmp($data, $before) !== 0) { + $modified = true; + } + + } + + // Destroy circular references so PHP will garbage collect the object. + $vobj->destroy(); + + } + + /** + * This method is triggered whenever a subsystem reqeuests the privileges + * that are supported on a particular node. + * + * @param INode $node + * @param array $supportedPrivilegeSet + */ + 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 DAV\INode $node + * @param string $output + * @return bool + */ + 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. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return void + */ + function httpAfterGet(RequestInterface $request, ResponseInterface $response) { + + if (strpos($response->getHeader('Content-Type'), 'text/calendar') === false) { + return; + } + + $result = HTTP\Util::negotiate( + $request->getHeader('Accept'), + ['text/calendar', 'application/calendar+json'] + ); + + if ($result !== 'application/calendar+json') { + // 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 + */ + function getPluginInfo() { + + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds support for CalDAV (rfc4791)', + 'link' => 'http://sabre.io/dav/caldav/', + ]; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Principal/Collection.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Principal/Collection.php new file mode 100644 index 00000000000..e19719a76d8 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Principal/Collection.php @@ -0,0 +1,33 @@ +principalBackend, $principalInfo); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Principal/IProxyRead.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Principal/IProxyRead.php new file mode 100644 index 00000000000..7dd3759329c --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Principal/IProxyRead.php @@ -0,0 +1,19 @@ +principalInfo = $principalInfo; + $this->principalBackend = $principalBackend; + + } + + /** + * Returns this principals name. + * + * @return string + */ + function getName() { + + return 'calendar-proxy-read'; + + } + + /** + * Returns the last modification time + * + * @return null + */ + function getLastModified() { + + return null; + + } + + /** + * Deletes the current node + * + * @throws DAV\Exception\Forbidden + * @return void + */ + 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 + * @return void + */ + 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 + */ + function getAlternateUriSet() { + + return []; + + } + + /** + * Returns the full principal url + * + * @return string + */ + 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 + */ + 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 + */ + 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. + * + * @param array $principals + * @return void + */ + 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 + */ + function getDisplayName() { + + return $this->getName(); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Principal/ProxyWrite.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Principal/ProxyWrite.php new file mode 100644 index 00000000000..43dd9bf07bf --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Principal/ProxyWrite.php @@ -0,0 +1,181 @@ +principalInfo = $principalInfo; + $this->principalBackend = $principalBackend; + + } + + /** + * Returns this principals name. + * + * @return string + */ + function getName() { + + return 'calendar-proxy-write'; + + } + + /** + * Returns the last modification time + * + * @return null + */ + function getLastModified() { + + return null; + + } + + /** + * Deletes the current node + * + * @throws DAV\Exception\Forbidden + * @return void + */ + 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 + * @return void + */ + 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 + */ + function getAlternateUriSet() { + + return []; + + } + + /** + * Returns the full principal url + * + * @return string + */ + 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 + */ + 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 + */ + 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. + * + * @param array $principals + * @return void + */ + 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 + */ + function getDisplayName() { + + return $this->getName(); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Principal/User.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Principal/User.php new file mode 100644 index 00000000000..6e97e7cca56 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Principal/User.php @@ -0,0 +1,135 @@ +principalBackend->getPrincipalByPath($this->getPrincipalURL() . '/' . $name); + if (!$principal) { + throw new DAV\Exception\NotFound('Node with name ' . $name . ' was not found'); + } + if ($name === 'calendar-proxy-read') + return new ProxyRead($this->principalBackend, $this->principalProperties); + + if ($name === 'calendar-proxy-write') + 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[] + */ + 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 + */ + 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 + */ + 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/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Schedule/IInbox.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Schedule/IInbox.php new file mode 100644 index 00000000000..c9fd77d9353 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Schedule/IInbox.php @@ -0,0 +1,15 @@ +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 + */ + 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 + */ + function getPluginName() { + + return 'imip'; + + } + + /** + * Event handler for the 'schedule' event. + * + * @param ITip\Message $iTipMessage + * @return void + */ + 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 (parse_url($iTipMessage->sender, PHP_URL_SCHEME) !== 'mailto') + return; + + if (parse_url($iTipMessage->recipient, PHP_URL_SCHEME) !== 'mailto') + return; + + $sender = substr($iTipMessage->sender, 7); + $recipient = substr($iTipMessage->recipient, 7); + + if ($iTipMessage->senderName) { + $sender = $iTipMessage->senderName . ' <' . $sender . '>'; + } + if ($iTipMessage->recipientName) { + $recipient = $iTipMessage->recipientName . ' <' . $recipient . '>'; + } + + $subject = 'SabreDAV iTIP message'; + switch (strtoupper($iTipMessage->method)) { + case 'REPLY' : + $subject = 'Re: ' . $summary; + break; + case 'REQUEST' : + $subject = $summary; + break; + case 'CANCEL' : + $subject = 'Cancelled: ' . $summary; + break; + } + + $headers = [ + 'Reply-To: ' . $sender, + 'From: ' . $this->senderEmail, + '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 + * @return void + */ + 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 + */ + function getPluginInfo() { + + return [ + 'name' => $this->getPluginName(), + 'description' => 'Email delivery (rfc6047) for CalDAV scheduling', + 'link' => 'http://sabre.io/dav/scheduling/', + ]; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Schedule/IOutbox.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Schedule/IOutbox.php new file mode 100644 index 00000000000..88fbdc4114b --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Schedule/IOutbox.php @@ -0,0 +1,15 @@ +caldavBackend = $caldavBackend; + $this->principalUri = $principalUri; + + } + + /** + * Returns the name of the node. + * + * This is used to generate the url. + * + * @return string + */ + function getName() { + + return 'inbox'; + + } + + /** + * Returns an array with all the child nodes + * + * @return \Sabre\DAV\INode[] + */ + 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 null|string + */ + 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 + */ + 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 + */ + 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. + * + * @param array $filters + * @return array + */ + 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/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Schedule/Outbox.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Schedule/Outbox.php new file mode 100644 index 00000000000..888ea308626 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Schedule/Outbox.php @@ -0,0 +1,123 @@ +principalUri = $principalUri; + + } + + /** + * Returns the name of the node. + * + * This is used to generate the url. + * + * @return string + */ + function getName() { + + return 'outbox'; + + } + + /** + * Returns an array with all the child nodes + * + * @return \Sabre\DAV\INode[] + */ + 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 + */ + 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 + */ + 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/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Schedule/Plugin.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Schedule/Plugin.php new file mode 100644 index 00000000000..0b991e61979 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Schedule/Plugin.php @@ -0,0 +1,1066 @@ +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 + */ + 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. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + function httpPost(RequestInterface $request, ResponseInterface $response) { + + // Checking if this is a text/calendar content type + $contentType = $request->getHeader('Content-Type'); + if (strpos($contentType, 'text/calendar') !== 0) { + 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. + * + * @param PropFind $propFind + * @param INode $node + * @return void + */ + 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 ($shareAccess !== Sharing\Plugin::ACCESS_NOTSHARED && $shareAccess !== Sharing\Plugin::ACCESS_SHAREDOWNER) { + // 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 + * @param PropPatch $propPatch + * @return void + */ + 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 + * @return void + */ + 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. + * + * @param ITip\Message $iTipMessage + * @return void + */ + 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 + * @return void + */ + function beforeUnbind($path) { + + // FIXME: We shouldn't trigger this functionality when we're issuing a + // MOVE. This is a hack. + if ($this->server->httpRequest->getMethod() === 'MOVE') 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 = new ITip\Broker(); + $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. + * + * @param ITip\Message $iTipMessage + * @return void + */ + 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 ($iTipMessage->method === 'REPLY') { + $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; + $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 = new ITip\Broker(); + $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 ($iTipMessage->method === 'REPLY') { + $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. + * + * @param INode $node + * @param array $supportedPrivilegeSet + */ + 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 $oldObject + * @param VCalendar $newObject + * @param array $addresses + * @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. + * @return void + */ + protected function processICalendarChange($oldObject = null, VCalendar $newObject, array $addresses, array $ignore = [], &$modified = false) { + + $broker = new ITip\Broker(); + $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. + * + * @param IOutbox $outboxNode + * @param RequestInterface $request + * @param ResponseInterface $response + * @return void + */ + 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 ($component->name !== 'VTIMEZONE') { + $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 ($componentType === 'VFREEBUSY' && $method === 'REQUEST') { + + $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 it's result. + * + * @param IOutbox $outbox + * @param VObject\Component $vObject + * @param RequestInterface $request + * @param ResponseInterface $response + * @return string + */ + 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 + * @param \DateTimeInterface $start + * @param \DateTimeInterface $end + * @param VObject\Component $request + * @return array + */ + protected function getFreeBusyForEmail($email, \DateTimeInterface $start, \DateTimeInterface $end, VObject\Component $request) { + + $caldavNS = '{' . self::NS_CALDAV . '}'; + + $aclPlugin = $this->server->getPlugin('acl'); + if (substr($email, 0, 7) === 'mailto:') $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]) && $props[$sct]->getValue() == ScheduleCalendarTransp::TRANSPARENT) { + // 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. + * + * @param RequestInterface $request + * @return bool + */ + private function scheduleReply(RequestInterface $request) { + + $scheduleReply = $request->getHeader('Schedule-Reply'); + return $scheduleReply !== 'F'; + + } + + /** + * 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 + */ + function getPluginInfo() { + + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds calendar-auto-schedule, as defined in rfc6638', + 'link' => 'http://sabre.io/dav/scheduling/', + ]; + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php new file mode 100644 index 00000000000..0cd05a965c3 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php @@ -0,0 +1,155 @@ +caldavBackend = $caldavBackend; + + if (!isset($objectData['uri'])) { + throw new \InvalidArgumentException('The objectData argument must contain an \'uri\' property'); + } + + $this->objectData = $objectData; + + } + + /** + * Returns the ICalendar-formatted object + * + * @return string + */ + 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->getSchedulingObject($this->objectData['principaluri'], $this->objectData['uri']); + } + return $this->objectData['calendardata']; + + } + + /** + * Updates the ICalendar-formatted object + * + * @param string|resource $calendarData + * @return string + */ + function put($calendarData) { + + throw new MethodNotAllowed('Updating scheduling objects is not supported'); + + } + + /** + * Deletes the scheduling message + * + * @return void + */ + 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 + */ + 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 + */ + 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/htdocs/includes/sabre/sabre/dav/lib/CalDAV/SharedCalendar.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/SharedCalendar.php new file mode 100644 index 00000000000..7a77616e353 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/SharedCalendar.php @@ -0,0 +1,229 @@ +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 + */ + 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 + * @return void + */ + 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[] + */ + 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 + * @return void + */ + 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 + */ + 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 + */ + function getChildACL() { + + $acl = []; + + switch ($this->getShareAccess()) { + case SPlugin::ACCESS_NOTSHARED : + // No break intentional + case SPlugin::ACCESS_SHAREDOWNER : + // 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:}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/htdocs/includes/sabre/sabre/dav/lib/CalDAV/SharingPlugin.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/SharingPlugin.php new file mode 100644 index 00000000000..5cce79678be --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/SharingPlugin.php @@ -0,0 +1,401 @@ +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. + * + * @param DAV\PropFind $propFind + * @param DAV\INode $node + * @return void + */ + function propFindEarly(DAV\PropFind $propFind, DAV\INode $node) { + + if ($node instanceof ISharedCalendar) { + + $propFind->handle('{' . Plugin::NS_CALENDARSERVER . '}invite', function() use ($node) { + + // Fetching owner information + $props = $this->server->getPropertiesForPath($node->getOwner(), [ + '{http://sabredav.org/ns}email-address', + '{DAV:}displayname', + ], 0); + + $ownerInfo = [ + 'href' => $node->getOwner(), + ]; + + if (isset($props[0][200])) { + + // We're mapping the internal webdav properties to the + // elements caldav-sharing expects. + if (isset($props[0][200]['{http://sabredav.org/ns}email-address'])) { + $ownerInfo['href'] = 'mailto:' . $props[0][200]['{http://sabredav.org/ns}email-address']; + } + if (isset($props[0][200]['{DAV:}displayname'])) { + $ownerInfo['commonName'] = $props[0][200]['{DAV:}displayname']; + } + + } + + return new Xml\Property\Invite( + $node->getInvites(), + $ownerInfo + ); + + }); + + } + + } + + /** + * This method is triggered *after* all properties have been retrieved. + * This allows us to inject the correct resourcetype for calendars that + * have been shared. + * + * @param DAV\PropFind $propFind + * @param DAV\INode $node + * @return void + */ + 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 trigged 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 + * @param DAV\PropPatch $propPatch + * @return void + */ + function propPatch($path, DAV\PropPatch $propPatch) { + + $node = $this->server->tree->getNodeForPath($path); + if (!$node instanceof ISharedCalendar) + return; + + if ($node->getShareAccess() === \Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER || $node->getShareAccess() === \Sabre\DAV\Sharing\Plugin::ACCESS_NOTSHARED) { + + $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. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return null|bool + */ + function httpPost(RequestInterface $request, ResponseInterface $response) { + + $path = $request->getPath(); + + // Only handling xml + $contentType = $request->getHeader('Content-Type'); + if (strpos($contentType, 'application/xml') === false && strpos($contentType, 'text/xml') === false) + 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->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 + */ + function getPluginInfo() { + + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds support for caldav-sharing.', + 'link' => 'http://sabre.io/dav/caldav-sharing/', + ]; + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Subscriptions/ISubscription.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Subscriptions/ISubscription.php new file mode 100644 index 00000000000..7ba259c7b68 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Subscriptions/ISubscription.php @@ -0,0 +1,40 @@ +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 + */ + function getFeatures() { + + return ['calendarserver-subscribed']; + + } + + /** + * Triggered after properties have been fetched. + * + * @param PropFind $propFind + * @param INode $node + * @return void + */ + 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 ($propFind->getStatus($prop) === 200) { + $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 + */ + 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 + */ + function getPluginInfo() { + + return [ + 'name' => $this->getPluginName(), + 'description' => 'This plugin allows users to store iCalendar subscriptions in their calendar-home.', + 'link' => null, + ]; + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php new file mode 100644 index 00000000000..6a1851ed868 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php @@ -0,0 +1,221 @@ +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 + */ + function getName() { + + return $this->subscriptionInfo['uri']; + + } + + /** + * Returns the last modification time + * + * @return int + */ + function getLastModified() { + + if (isset($this->subscriptionInfo['lastmodified'])) { + return $this->subscriptionInfo['lastmodified']; + } + + } + + /** + * Deletes the current node + * + * @return void + */ + function delete() { + + $this->caldavBackend->deleteSubscription( + $this->subscriptionInfo['id'] + ); + + } + + /** + * Returns an array with all the child nodes + * + * @return \Sabre\DAV\INode[] + */ + 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. + * + * @param PropPatch $propPatch + * @return void + */ + 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 + */ + 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 + */ + 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 + */ + 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/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Filter/CalendarData.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Filter/CalendarData.php new file mode 100644 index 00000000000..9669be304e1 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Filter/CalendarData.php @@ -0,0 +1,84 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + 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/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Filter/CompFilter.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Filter/CompFilter.php new file mode 100644 index 00000000000..c21ede66b5c --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Filter/CompFilter.php @@ -0,0 +1,97 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + 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 ($result['name'] === 'VCALENDAR') { + 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/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Filter/ParamFilter.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Filter/ParamFilter.php new file mode 100644 index 00000000000..bf422cf0545 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Filter/ParamFilter.php @@ -0,0 +1,82 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + 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']) && $elem['attributes']['negate-condition'] === 'yes', + 'collation' => isset($elem['attributes']['collation']) ? $elem['attributes']['collation'] : 'i;ascii-casemap', + 'value' => $elem['value'], + ]; + break; + + } + + } + + return $result; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Filter/PropFilter.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Filter/PropFilter.php new file mode 100644 index 00000000000..db9207295a2 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Filter/PropFilter.php @@ -0,0 +1,98 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + + $result = [ + 'name' => null, + 'is-not-defined' => false, + 'param-filters' => [], + 'text-match' => null, + '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 . '}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']) && $elem['attributes']['negate-condition'] === 'yes', + 'collation' => isset($elem['attributes']['collation']) ? $elem['attributes']['collation'] : 'i;ascii-casemap', + 'value' => $elem['value'], + ]; + break; + + } + + } + + return $result; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php new file mode 100644 index 00000000000..92a9ac7b715 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php @@ -0,0 +1,302 @@ + $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. + * + * @param Writer $writer + * @return void + */ + 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. + * + * @param Writer $writer + * @return void + */ + 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 (strtolower(substr($this->organizer, 0, 7)) === 'mailto:') { + $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 + */ + function getId() { + + return $this->id; + + } + + /** + * Returns the ETag for this notification. + * + * The ETag must be surrounded by literal double-quotes. + * + * @return string + */ + function getETag() { + + return $this->etag; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php new file mode 100644 index 00000000000..f4b10a396bc --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php @@ -0,0 +1,213 @@ + $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. + * + * @param Writer $writer + * @return void + */ + 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. + * + * @param Writer $writer + * @return void + */ + 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 + */ + function getId() { + + return $this->id; + + } + + /** + * Returns the ETag for this notification. + * + * The ETag must be surrounded by literal double-quotes. + * + * @return string + */ + function getETag() { + + return $this->etag; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Notification/NotificationInterface.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Notification/NotificationInterface.php new file mode 100644 index 00000000000..b98f9c88889 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Notification/NotificationInterface.php @@ -0,0 +1,45 @@ +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. + * + * @param Writer $writer + * @return void + */ + 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. + * + * @param Writer $writer + * @return void + */ + 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 + */ + function getId() { + + return $this->id; + + } + + /* + * Returns the ETag for this notification. + * + * The ETag must be surrounded by literal double-quotes. + * + * @return string + */ + function getETag() { + + return $this->etag; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Property/AllowedSharingModes.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Property/AllowedSharingModes.php new file mode 100644 index 00000000000..54e5a116a6a --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Property/AllowedSharingModes.php @@ -0,0 +1,87 @@ +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. + * + * @param Writer $writer + * @return void + */ + 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/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Property/EmailAddressSet.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Property/EmailAddressSet.php new file mode 100644 index 00000000000..fc6f1d505fa --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Property/EmailAddressSet.php @@ -0,0 +1,80 @@ +emails = $emails; + + } + + /** + * Returns the email addresses + * + * @return array + */ + 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. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Writer $writer) { + + foreach ($this->emails as $email) { + + $writer->writeElement('{http://calendarserver.org/ns/}email-address', $email); + + } + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Property/Invite.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Property/Invite.php new file mode 100644 index 00000000000..4f33c464cc3 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Property/Invite.php @@ -0,0 +1,128 @@ +sharees = $sharees; + + } + + /** + * Returns the list of users, as it was passed to the constructor. + * + * @return array + */ + 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. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Writer $writer) { + + $cs = '{' . Plugin::NS_CALENDARSERVER . '}'; + + foreach ($this->sharees as $sharee) { + + if ($sharee->access === DAV\Sharing\Plugin::ACCESS_SHAREDOWNER) { + $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/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Property/ScheduleCalendarTransp.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Property/ScheduleCalendarTransp.php new file mode 100644 index 00000000000..10c20be55c8 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Property/ScheduleCalendarTransp.php @@ -0,0 +1,130 @@ +value = $value; + + } + + /** + * Returns the current value + * + * @return string + */ + 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. + * + * @param Writer $writer + * @return void + */ + 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. + * + * @param Reader $reader + * @return mixed + */ + 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/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarComponentSet.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarComponentSet.php new file mode 100644 index 00000000000..7fc25c5f0c2 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarComponentSet.php @@ -0,0 +1,129 @@ +components = $components; + + } + + /** + * Returns the list of supported components + * + * @return array + */ + 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. + * + * @param Writer $writer + * @return void + */ + 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. + * + * @param Reader $reader + * @return mixed + */ + 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/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarData.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarData.php new file mode 100644 index 00000000000..d123ba4c0d2 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarData.php @@ -0,0 +1,60 @@ +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/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Property/SupportedCollationSet.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Property/SupportedCollationSet.php new file mode 100644 index 00000000000..af10860d077 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Property/SupportedCollationSet.php @@ -0,0 +1,57 @@ +writeElement('{' . Plugin::NS_CALDAV . '}supported-collation', $collation); + } + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Request/CalendarMultiGetReport.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Request/CalendarMultiGetReport.php new file mode 100644 index 00000000000..6d3c5d5089c --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Request/CalendarMultiGetReport.php @@ -0,0 +1,124 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + 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/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Request/CalendarQueryReport.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Request/CalendarQueryReport.php new file mode 100644 index 00000000000..e0b1c795032 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Request/CalendarQueryReport.php @@ -0,0 +1,139 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + 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/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Request/FreeBusyQueryReport.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Request/FreeBusyQueryReport.php new file mode 100644 index 00000000000..0f6c1e0741d --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Request/FreeBusyQueryReport.php @@ -0,0 +1,91 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + 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/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php new file mode 100644 index 00000000000..db32cc6a59e --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php @@ -0,0 +1,150 @@ +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. + * + * @param Reader $reader + * @return mixed + */ + 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 ($bla['name'] === '{DAV:}href') { + $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/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Request/MkCalendar.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Request/MkCalendar.php new file mode 100644 index 00000000000..ce7fafde910 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Request/MkCalendar.php @@ -0,0 +1,79 @@ +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. + * + * @param Reader $reader + * @return mixed + */ + 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 ($elem['name'] === '{DAV:}set') { + $self->properties = array_merge($self->properties, $elem['value']['{DAV:}prop']); + } + } + + return $self; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Request/Share.php b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Request/Share.php new file mode 100644 index 00000000000..e0bd8e0af31 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CalDAV/Xml/Request/Share.php @@ -0,0 +1,111 @@ +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. + * + * @param Reader $reader + * @return mixed + */ + 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/htdocs/includes/sabre/sabre/dav/lib/CardDAV/AddressBook.php b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/AddressBook.php new file mode 100644 index 00000000000..c9d28a091c8 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/AddressBook.php @@ -0,0 +1,357 @@ +carddavBackend = $carddavBackend; + $this->addressBookInfo = $addressBookInfo; + + } + + /** + * Returns the name of the addressbook + * + * @return string + */ + function getName() { + + return $this->addressBookInfo['uri']; + + } + + /** + * Returns a card + * + * @param string $name + * @return Card + */ + 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 + */ + 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 + */ + 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 + * @return void + */ + 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 $vcardData + * @return string|null + */ + function createFile($name, $vcardData = null) { + + if (is_resource($vcardData)) { + $vcardData = stream_get_contents($vcardData); + } + // Converting to UTF-8, if needed + $vcardData = DAV\StringUtil::ensureUTF8($vcardData); + + return $this->carddavBackend->createCard($this->addressBookInfo['id'], $name, $vcardData); + + } + + /** + * Deletes the entire addressbook. + * + * @return void + */ + function delete() { + + $this->carddavBackend->deleteAddressBook($this->addressBookInfo['id']); + + } + + /** + * Renames the addressbook + * + * @param string $newName + * @return void + */ + function setName($newName) { + + throw new DAV\Exception\MethodNotAllowed('Renaming addressbooks is not yet supported'); + + } + + /** + * Returns the last modification date as a unix timestamp. + * + * @return void + */ + 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. + * + * @param DAV\PropPatch $propPatch + * @return void + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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/htdocs/includes/sabre/sabre/dav/lib/CardDAV/AddressBookHome.php b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/AddressBookHome.php new file mode 100644 index 00000000000..d770c0ffe0f --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/AddressBookHome.php @@ -0,0 +1,191 @@ +carddavBackend = $carddavBackend; + $this->principalUri = $principalUri; + + } + + /** + * Returns the name of this object + * + * @return string + */ + function getName() { + + list(, $name) = Uri\split($this->principalUri); + return $name; + + } + + /** + * Updates the name of this object + * + * @param string $name + * @return void + */ + function setName($name) { + + throw new DAV\Exception\MethodNotAllowed(); + + } + + /** + * Deletes this object + * + * @return void + */ + function delete() { + + throw new DAV\Exception\MethodNotAllowed(); + + } + + /** + * Returns the last modification date + * + * @return int + */ + function getLastModified() { + + return null; + + } + + /** + * Creates a new file under this object. + * + * This is currently not allowed + * + * @param string $filename + * @param resource $data + * @return void + */ + function createFile($filename, $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 + * @return void + */ + 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 + */ + 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 + */ + 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 + * @param MkCol $mkCol + * @throws DAV\Exception\InvalidResourceType + * @return void + */ + 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 + */ + function getOwner() { + + return $this->principalUri; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CardDAV/AddressBookRoot.php b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/AddressBookRoot.php new file mode 100644 index 00000000000..a9f1183da49 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/AddressBookRoot.php @@ -0,0 +1,80 @@ +carddavBackend = $carddavBackend; + parent::__construct($principalBackend, $principalPrefix); + + } + + /** + * Returns the name of the node + * + * @return string + */ + 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. + * + * @param array $principal + * @return \Sabre\DAV\INode + */ + function getChildForPrincipal(array $principal) { + + return new AddressBookHome($this->carddavBackend, $principal['uri']); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Backend/AbstractBackend.php b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Backend/AbstractBackend.php new file mode 100644 index 00000000000..03d2346da08 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Backend/AbstractBackend.php @@ -0,0 +1,38 @@ +getCard($addressBookId, $uri); + }, $uris); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Backend/BackendInterface.php b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Backend/BackendInterface.php new file mode 100644 index 00000000000..18c0c0a9982 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Backend/BackendInterface.php @@ -0,0 +1,190 @@ +pdo = $pdo; + + } + + /** + * Returns the list of addressbooks for a specific user. + * + * @param string $principalUri + * @return array + */ + 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 + * @param \Sabre\DAV\PropPatch $propPatch + * @return void + */ + function updateAddressBook($addressBookId, \Sabre\DAV\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. + * @param array $properties + * @return int Last insert id + */ + 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 + * @return void + */ + 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 ommit carddata. + * This may speed up certain requests, especially with large cards. + * + * @param mixed $addressbookId + * @return array + */ + 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 + */ + 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 + * @param array $uris + * @return array + */ + 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 + */ + 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 + */ + 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 + */ + 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 $stmt->rowCount() === 1; + + } + + /** + * 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 + */ + 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 + * @return void + */ + 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/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Backend/SyncSupport.php b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Backend/SyncSupport.php new file mode 100644 index 00000000000..f80618a8e14 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Backend/SyncSupport.php @@ -0,0 +1,81 @@ + '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 + */ + function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null); + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Card.php b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Card.php new file mode 100644 index 00000000000..42a2d7b6a11 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Card.php @@ -0,0 +1,216 @@ +carddavBackend = $carddavBackend; + $this->addressBookInfo = $addressBookInfo; + $this->cardData = $cardData; + + } + + /** + * Returns the uri for this object + * + * @return string + */ + function getName() { + + return $this->cardData['uri']; + + } + + /** + * Returns the VCard-formatted object + * + * @return string + */ + 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 + */ + 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 + * + * @return void + */ + function delete() { + + $this->carddavBackend->deleteCard($this->addressBookInfo['id'], $this->cardData['uri']); + + } + + /** + * Returns the mime content-type + * + * @return string + */ + function getContentType() { + + return 'text/vcard; charset=utf-8'; + + } + + /** + * Returns an ETag for this object + * + * @return string + */ + 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 + */ + function getLastModified() { + + return isset($this->cardData['lastmodified']) ? $this->cardData['lastmodified'] : null; + + } + + /** + * Returns the size of this object in bytes + * + * @return int + */ + 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 + */ + 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 + */ + 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/htdocs/includes/sabre/sabre/dav/lib/CardDAV/IAddressBook.php b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/IAddressBook.php new file mode 100644 index 00000000000..f80e0557554 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/IAddressBook.php @@ -0,0 +1,18 @@ +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 + */ + 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 + */ + 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 + * + * @param DAV\PropFind $propFind + * @param DAV\INode $node + * @return void + */ + 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 + */ + 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) = \Sabre\HTTP\URLUtil::splitPath($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 + * @return void + */ + 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, $prefer['return'] === 'minimal')); + + } + + /** + * 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 DAV\IFile $node + * @param resource $data + * @param bool $modified Should be set to true, if this event handler + * changed &$data. + * @return void + */ + 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 DAV\ICollection $parentNode + * @param bool $modified Should be set to true, if this event handler + * changed &$data. + * @return void + */ + 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. + * @return void + */ + 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 ($vobj->name !== 'VCARD') { + throw new DAV\Exception\UnsupportedMediaType('This collection can only support vcard objects.'); + } + + $options = VObject\Node::PROFILE_CARDDAV; + $prefer = $this->server->getHTTPPrefer(); + + if ($prefer['handling'] !== 'strict') { + $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 && strcmp($data, $before) !== 0) { + // 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 + * @return void + */ + protected function addressbookQueryReport($report) { + + $depth = $this->server->getHTTPDepth(0); + + if ($depth == 0) { + $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 ($depth == 0) { + $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, $prefer['return'] === 'minimal')); + + } + + /** + * Validates if a vcard makes it throught a list of filters. + * + * @param string $vcardData + * @param array $filters + * @param string $test anyof or allof (which means OR or AND) + * @return bool + */ + 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 (count($results) === 1) { + $success = $results[0]; + } else { + if ($filter['test'] === 'anyof') { + $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 ($test === 'anyof' && $success) { + + // Destroy circular references to PHP will GC the object. + $vcard->destroy(); + + return true; + } + if ($test === 'allof' && !$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 $test === 'allof'; + + } + + /** + * 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 array $vProperties + * @param array $filters + * @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. + $success = DAV\StringUtil::textMatch($vProperty[$filter['name']]->getValue(), $filter['text-match']['value'], $filter['text-match']['collation'], $filter['text-match']['match-type']); + if ($success) break; + } + if ($filter['text-match']['negate-condition']) { + $success = !$success; + } + + } // else + + // There are two conditions where we can already determine whether + // or not this filter succeeds. + if ($test === 'anyof' && $success) { + return true; + } + if ($test === 'allof' && !$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 $test === 'allof'; + + } + + /** + * Validates if a text-filter can be applied to a specific property. + * + * @param array $texts + * @param array $filters + * @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']); + + // Breaking on the first match + if ($success) break; + } + if ($filter['negate-condition']) { + $success = !$success; + } + + if ($success && $test === 'anyof') + return true; + + if (!$success && $test == 'allof') + 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 $test === 'allof'; + + } + + /** + * This event is triggered when fetching properties. + * + * This event is scheduled late in the process, after most work for + * propfind has been done. + * + * @param DAV\PropFind $propFind + * @param DAV\INode $node + * @return void + */ + 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 (strpos($this->server->httpRequest->getHeader('User-Agent'), 'Thunderbird') === false) { + return; + } + $contentType = $propFind->get('{DAV:}getcontenttype'); + list($part) = explode(';', $contentType); + if ($part === 'text/x-vcard' || $part === 'text/vcard') { + $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 DAV\INode $node + * @param string $output + * @return bool + */ + 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. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return void + */ + function httpAfterGet(RequestInterface $request, ResponseInterface $response) { + + if (strpos($response->getHeader('Content-Type'), 'text/vcard') === false) { + 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\Util::negotiate( + $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 ($input->getDocumentType() === VObject\Document::VCARD30) { + // Do nothing + return $data; + } + $output = $input->convert(VObject\Document::VCARD30); + return $output->serialize(); + case 'vcard4' : + if ($input->getDocumentType() === VObject\Document::VCARD40) { + // 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 + */ + 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 + */ + function getPluginInfo() { + + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds support for CardDAV (rfc6352)', + 'link' => 'http://sabre.io/dav/carddav/', + ]; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CardDAV/VCFExportPlugin.php b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/VCFExportPlugin.php new file mode 100644 index 00000000000..2d61db6ac3d --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/VCFExportPlugin.php @@ -0,0 +1,172 @@ +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. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + 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 + * + * @param array $nodes + * @return string + */ + 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 + */ + 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 + */ + 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/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Xml/Filter/AddressData.php b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Xml/Filter/AddressData.php new file mode 100644 index 00000000000..a130cd61d2e --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Xml/Filter/AddressData.php @@ -0,0 +1,63 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + + $result = [ + 'contentType' => $reader->getAttribute('content-type') ?: 'text/vcard', + 'version' => $reader->getAttribute('version') ?: '3.0', + ]; + + $elems = (array)$reader->parseInnerTree(); + $result['addressDataProperties'] = array_map(function($element) { + return $element['attributes']['name']; + }, $elems); + + return $result; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Xml/Filter/ParamFilter.php b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Xml/Filter/ParamFilter.php new file mode 100644 index 00000000000..936e26917ce --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Xml/Filter/ParamFilter.php @@ -0,0 +1,89 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + 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']) && $elem['attributes']['negate-condition'] === 'yes', + 'collation' => isset($elem['attributes']['collation']) ? $elem['attributes']['collation'] : 'i;unicode-casemap', + 'value' => $elem['value'], + 'match-type' => $matchType, + ]; + break; + + } + + } + + return $result; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Xml/Filter/PropFilter.php b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Xml/Filter/PropFilter.php new file mode 100644 index 00000000000..d7799429db4 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Xml/Filter/PropFilter.php @@ -0,0 +1,98 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + 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']) && $att['test'] === 'allof') { + $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']) && $elem['attributes']['negate-condition'] === 'yes', + 'collation' => isset($elem['attributes']['collation']) ? $elem['attributes']['collation'] : 'i;unicode-casemap', + 'value' => $elem['value'], + 'match-type' => $matchType, + ]; + break; + + } + + } + + return $result; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Xml/Property/SupportedAddressData.php b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Xml/Property/SupportedAddressData.php new file mode 100644 index 00000000000..aecd8a09fc8 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Xml/Property/SupportedAddressData.php @@ -0,0 +1,83 @@ + '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. + * + * @param Writer $writer + * @return void + */ + 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/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Xml/Property/SupportedCollationSet.php b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Xml/Property/SupportedCollationSet.php new file mode 100644 index 00000000000..778aa2b64e5 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Xml/Property/SupportedCollationSet.php @@ -0,0 +1,47 @@ +writeElement('{urn:ietf:params:xml:ns:carddav}supported-collation', $coll); + } + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Xml/Request/AddressBookMultiGetReport.php b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Xml/Request/AddressBookMultiGetReport.php new file mode 100644 index 00000000000..0115a010722 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Xml/Request/AddressBookMultiGetReport.php @@ -0,0 +1,113 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + 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/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Xml/Request/AddressBookQueryReport.php b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Xml/Request/AddressBookQueryReport.php new file mode 100644 index 00000000000..09fad008adb --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/CardDAV/Xml/Request/AddressBookQueryReport.php @@ -0,0 +1,199 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + 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 ($newProps['test'] !== 'allof' && $newProps['test'] !== 'anyof') { + 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/htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/AbstractBasic.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/AbstractBasic.php new file mode 100644 index 00000000000..40a95f8bfb7 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/AbstractBasic.php @@ -0,0 +1,144 @@ +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] + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return array + */ + 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. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return void + */ + function challenge(RequestInterface $request, ResponseInterface $response) { + + $auth = new HTTP\Auth\Basic( + $this->realm, + $request, + $response + ); + $auth->requireLogin(); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/AbstractBearer.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/AbstractBearer.php new file mode 100644 index 00000000000..ae7a8a12f77 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/AbstractBearer.php @@ -0,0 +1,138 @@ +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] + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return array + */ + 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. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return void + */ + function challenge(RequestInterface $request, ResponseInterface $response) { + + $auth = new HTTP\Auth\Bearer( + $this->realm, + $request, + $response + ); + $auth->requireLogin(); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/AbstractDigest.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/AbstractDigest.php new file mode 100644 index 00000000000..4b47f56c9ff --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/AbstractDigest.php @@ -0,0 +1,168 @@ +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 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] + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return array + */ + 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 ($hash === false || 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. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return void + */ + 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/htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/Apache.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/Apache.php new file mode 100644 index 00000000000..e203d26855d --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/Apache.php @@ -0,0 +1,96 @@ +getRawServerValue('REMOTE_USER'); + if (is_null($remoteUser)) { + $remoteUser = $request->getRawServerValue('REDIRECT_REMOTE_USER'); + } + if (is_null($remoteUser)) { + return [false, 'No REMOTE_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. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return void + */ + function challenge(RequestInterface $request, ResponseInterface $response) { + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/BackendInterface.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/BackendInterface.php new file mode 100644 index 00000000000..0fb2210f488 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/BackendInterface.php @@ -0,0 +1,70 @@ +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. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return void + */ + function challenge(RequestInterface $request, ResponseInterface $response); + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/BasicCallBack.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/BasicCallBack.php new file mode 100644 index 00000000000..7ad8f48b2e1 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/BasicCallBack.php @@ -0,0 +1,58 @@ +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/htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/File.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/File.php new file mode 100644 index 00000000000..3a687d747b2 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/File.php @@ -0,0 +1,77 @@ +loadFile($filename); + + } + + /** + * Loads an htdigest-formatted file. This method can be called multiple times if + * more than 1 file is used. + * + * @param string $filename + * @return void + */ + function loadFile($filename) { + + foreach (file($filename, FILE_IGNORE_NEW_LINES) as $line) { + + if (substr_count($line, ":") !== 2) + 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 + */ + function getDigestHash($realm, $username) { + + return isset($this->users[$realm . ':' . $username]) ? $this->users[$realm . ':' . $username] : false; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/PDO.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/PDO.php new file mode 100644 index 00000000000..c2f6de97480 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Backend/PDO.php @@ -0,0 +1,57 @@ +pdo = $pdo; + + } + + /** + * Returns the digest hash for a user. + * + * @param string $realm + * @param string $username + * @return string|null + */ + 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/htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Plugin.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Plugin.php new file mode 100644 index 00000000000..bbb5d180d0c --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Auth/Plugin.php @@ -0,0 +1,285 @@ +addBackend($authBackend); + } + + } + + /** + * Adds an authentication backend to the plugin. + * + * @param Backend\BackendInterface $authBackend + * @return void + */ + function addBackend(Backend\BackendInterface $authBackend) { + + $this->backends[] = $authBackend; + + } + + /** + * Initializes the plugin. This function is automatically called by the server + * + * @param Server $server + * @return void + */ + 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 + */ + 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 + */ + function getCurrentPrincipal() { + + return $this->currentPrincipal; + + } + + /** + * This method is called before any HTTP method and forces users to be authenticated + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + 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. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return array + */ + 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) || count($result) !== 2 || !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. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return array + */ + 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 + */ + 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 + */ + function getPluginInfo() { + + return [ + 'name' => $this->getPluginName(), + 'description' => 'Generic authentication plugin', + 'link' => 'http://sabre.io/dav/authentication/', + ]; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/GuessContentType.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/GuessContentType.php new file mode 100644 index 00000000000..3ba2aee25b2 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/GuessContentType.php @@ -0,0 +1,101 @@ + 'image/jpeg', + 'gif' => 'image/gif', + 'png' => 'image/png', + + // groupware + 'ics' => 'text/calendar', + 'vcf' => 'text/vcard', + + // text + 'txt' => 'text/plain', + + ]; + + /** + * Initializes the plugin + * + * @param DAV\Server $server + * @return void + */ + 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. + * + * @param PropFind $propFind + * @param INode $node + * @return void + */ + function propFind(PropFind $propFind, INode $node) { + + $propFind->handle('{DAV:}getcontenttype', function() use ($propFind) { + + list(, $fileName) = URLUtil::splitPath($propFind->getPath()); + return $this->getContentType($fileName); + + }); + + } + + /** + * Simple method to return the contenttype + * + * @param string $fileName + * @return string + */ + protected function getContentType($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/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/HtmlOutput.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/HtmlOutput.php new file mode 100644 index 00000000000..f4be6b34841 --- /dev/null +++ b/htdocs/includes/sabre/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 + */ + function fullUrl($path) { + + return Uri\resolve($this->baseUri, $path); + + } + + /** + * Escape string for HTML output. + * + * @param string $input + * @return string + */ + function h($input) { + + return htmlspecialchars($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 + */ + 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 + */ + function xmlName($element) { + + list($ns, $localName) = XmlService::parseClarkNotation($element); + if (isset($this->namespaceMap[$ns])) { + $propName = $this->namespaceMap[$ns] . ':' . $localName; + } else { + $propName = $element; + } + return "h($element) . "\">" . $this->h($propName) . ""; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/MapGetToPropFind.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/MapGetToPropFind.php new file mode 100644 index 00000000000..61327c49a0c --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/MapGetToPropFind.php @@ -0,0 +1,60 @@ +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 + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + 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/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/Plugin.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/Plugin.php new file mode 100644 index 00000000000..545ad56337e --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/Plugin.php @@ -0,0 +1,802 @@ +enablePost = $enablePost; + + } + + /** + * Initializes the plugin and subscribes to events + * + * @param DAV\Server $server + * @return void + */ + 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 + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + function httpGetEarly(RequestInterface $request, ResponseInterface $response) { + + $params = $request->getQueryParameters(); + if (isset($params['sabreAction']) && $params['sabreAction'] === 'info') { + return $this->httpGet($request, $response); + } + + } + + /** + * This method intercepts GET requests to collections and returns the html + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + 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. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + function httpPOST(RequestInterface $request, ResponseInterface $response) { + + $contentType = $request->getHeader('Content-Type'); + list($contentType) = explode(';', $contentType); + if ($contentType !== 'application/x-www-form-urlencoded' && + $contentType !== 'multipart/form-data') { + 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) = URLUtil::splitPath(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) = URLUtil::splitPath(trim($file['name'])); + if (isset($postVars['name']) && trim($postVars['name'])) + $newName = trim($postVars['name']); + + // Making sure we only have a 'basename' component + list(, $newName) = URLUtil::splitPath($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 + */ + function escapeHTML($value) { + + return htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); + + } + + /** + * Generates the html directory index for a given url + * + * @param string $path + * @return string + */ + 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() . URLUtil::encodePath($subPath); + list(, $displayPath) = URLUtil::splitPath($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 .= '' . $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 + */ + 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 + */ + 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 + */ + function generateFooter() { + + $version = ''; + if (DAV\Server::$exposeVersion) { + $version = DAV\Version::VERSION; + } + return <<Generated by SabreDAV $version (c)2007-2016 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 DAV\INode $node + * @param mixed $output + * @param string $path + * @return void + */ + 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 (get_class($node) === 'Sabre\\DAV\\SimpleCollection') + return; + + $output .= << +

Create new folder

+ +
+ + +
+

Upload file

+ +
+
+ +
+HTML; + + } + + /** + * This method takes a path/name of an asset and turns it into url + * suiteable 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 (strpos($path, '/../') !== false || strrchr($path, '/') === '/..') { + throw new DAV\Exception\NotFound('Path does not exist, or escaping from the base path was detected'); + } + if (strpos(realpath($path), realpath($assetDir)) === 0 && 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 + * @return void + */ + 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 array $resourceTypes + * @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 + */ + 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 + */ + 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/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/PropFindAll.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/PropFindAll.php new file mode 100644 index 00000000000..c14b7f2f96d --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/PropFindAll.php @@ -0,0 +1,132 @@ +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 + * @return void + */ + 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 + * @return void + */ + 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 + */ + 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 + */ + 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 + */ + function get404Properties() { + + $result = []; + foreach ($this->result as $propertyName => $stuff) { + if ($stuff[0] === 404) { + $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/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/favicon.ico b/htdocs/includes/sabre/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/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/ICON-LICENSE b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/ICON-LICENSE new file mode 100644 index 00000000000..2199f4a6941 --- /dev/null +++ b/htdocs/includes/sabre/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/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.css b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.css new file mode 100644 index 00000000000..e7486740030 --- /dev/null +++ b/htdocs/includes/sabre/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/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.eot b/htdocs/includes/sabre/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/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.svg b/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.svg new file mode 100644 index 00000000000..0792c003a68 --- /dev/null +++ b/htdocs/includes/sabre/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/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.ttf b/htdocs/includes/sabre/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/htdocs/includes/sabre/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.woff b/htdocs/includes/sabre/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/htdocs/includes/sabre/sabre/dav/lib/DAV/Client.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Client.php new file mode 100644 index 00000000000..175ad1bc459 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Client.php @@ -0,0 +1,439 @@ +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. + * + * @param array $settings + */ + 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 + * + * The list of requested properties must be specified as an array, in clark + * notation. + * + * The returned array will contain a list of filenames as keys, and + * properties as values. + * + * The properties array will contain the list of properties. Only properties + * that are actually returned from the server (without error) will be + * returned, anything else is discarded. + * + * 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. + * + * @param string $url + * @param array $properties + * @param int $depth + * @return array + */ + function propFind($url, array $properties, $depth = 0) { + + $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 ($namespace === 'DAV:') { + $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); + } + + $result = $this->parseMultiStatus($response->getBodyAsString()); + + // If depth was 0, we only return the top item + if ($depth === 0) { + 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; + + } + + /** + * 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 + * @param array $properties + * @return bool + */ + 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 ($response->getStatus() === 207) { + // 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 + */ + 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 + * @param array $headers + * @throws ClientException, in case a curl error occurred. + * @return array + */ + 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 + */ + function getAbsoluteUrl($url) { + + return Uri\resolve( + $this->baseUri, + $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 + */ + 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/htdocs/includes/sabre/sabre/dav/lib/DAV/Collection.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Collection.php new file mode 100644 index 00000000000..35c90b5afae --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Collection.php @@ -0,0 +1,109 @@ +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 + */ + 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 null|string + */ + 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 + * @return void + */ + function createDirectory($name) { + + throw new Exception\Forbidden('Permission denied to create directory'); + + } + + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/CorePlugin.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/CorePlugin.php new file mode 100644 index 00000000000..676cdd04a29 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/CorePlugin.php @@ -0,0 +1,959 @@ +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 + */ + function getPluginName() { + + return 'core'; + + } + + /** + * This is the default implementation for the GET method. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + function httpGet(RequestInterface $request, ResponseInterface $response) { + + $path = $request->getPath(); + $node = $this->server->tree->getNodeForPath($path); + + if (!$node instanceof IFile) return; + + $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'] || fseek($body, $start, SEEK_SET) === -1) { + $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 + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + 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 + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + function httpHead(RequestInterface $request, ResponseInterface $response) { + + // This is implemented by changing the HEAD request to a GET request, + // and dropping the response body. + $subRequest = clone $request; + $subRequest->setMethod('GET'); + + try { + $this->server->invokeMethod($subRequest, $response, false); + $response->setBody(''); + } 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 + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return void + */ + 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 + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return void + */ + 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(), null, $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 && $depth != 0) $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 = $prefer['return'] === 'minimal'; + + $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 + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + 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(), null, $e); + } + $newProperties = $propPatch->properties; + + $result = $this->server->updateProperties($path, $newProperties); + + $prefer = $this->server->getHTTPPrefer(); + $response->setHeader('Vary', 'Brief,Prefer'); + + if ($prefer['return'] === 'minimal') { + + // 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 + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + 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 (strlen($firstByte) !== 1) { + 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 + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + function httpMkcol(RequestInterface $request, ResponseInterface $response) { + + $requestBody = $request->getBodyAsString(); + $path = $request->getPath(); + + if ($requestBody) { + + $contentType = $request->getHeader('Content-Type'); + if (strpos($contentType, 'application/xml') !== 0 && strpos($contentType, 'text/xml') !== 0) { + + // 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(), null, $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 + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + 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 + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + function httpCopy(RequestInterface $request, ResponseInterface $response) { + + $path = $request->getPath(); + + $copyInfo = $this->server->getCopyAndMoveInfo($request); + + if (!$this->server->emit('beforeBind', [$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('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. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + 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 + * @param PropPatch $propPatch + * @return void + */ + 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 + * @param PropPatch $propPatch + * @return void + */ + 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. + * + * @param PropFind $propFind + * @param INode $node + * @return void + */ + 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. + * + * @param PropFind $propFind + * @param INode $node + * @return void + */ + 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. + * + * @param PropFind $propFind + * @param INode $node + * @return void + */ + 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 + */ + 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 + */ + 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/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception.php new file mode 100644 index 00000000000..14f5bab2a53 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception.php @@ -0,0 +1,57 @@ +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/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/Forbidden.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/Forbidden.php new file mode 100644 index 00000000000..77df7ca9ecc --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/Forbidden.php @@ -0,0 +1,29 @@ +ownerDocument->createElementNS('DAV:', 'd:valid-resourcetype'); + $errorNode->appendChild($error); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/InvalidSyncToken.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/InvalidSyncToken.php new file mode 100644 index 00000000000..51a253b296b --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/InvalidSyncToken.php @@ -0,0 +1,38 @@ +ownerDocument->createElementNS('DAV:', 'd:valid-sync-token'); + $errorNode->appendChild($error); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/LengthRequired.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/LengthRequired.php new file mode 100644 index 00000000000..98971855834 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/LengthRequired.php @@ -0,0 +1,30 @@ +message = 'The locktoken supplied does not match any locks on this entity'; + + } + + /** + * This method allows the exception to include additional information into the WebDAV error response + * + * @param DAV\Server $server + * @param \DOMElement $errorNode + * @return void + */ + function serialize(DAV\Server $server, \DOMElement $errorNode) { + + $error = $errorNode->ownerDocument->createElementNS('DAV:', 'd:lock-token-matches-request-uri'); + $errorNode->appendChild($error); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/Locked.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/Locked.php new file mode 100644 index 00000000000..8176db46e87 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/Locked.php @@ -0,0 +1,72 @@ +lock = $lock; + + } + + /** + * Returns the HTTP statuscode for this exception + * + * @return int + */ + function getHTTPCode() { + + return 423; + + } + + /** + * This method allows the exception to include additional information into the WebDAV error response + * + * @param DAV\Server $server + * @param \DOMElement $errorNode + * @return void + */ + 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/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php new file mode 100644 index 00000000000..30c1c255345 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php @@ -0,0 +1,47 @@ +getAllowedMethods($server->getRequestUri()); + + return [ + 'Allow' => strtoupper(implode(', ', $methods)), + ]; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/NotAuthenticated.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/NotAuthenticated.php new file mode 100644 index 00000000000..e69a60c7549 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/NotAuthenticated.php @@ -0,0 +1,30 @@ +header = $header; + + } + + /** + * Returns the HTTP statuscode for this exception + * + * @return int + */ + function getHTTPCode() { + + return 412; + + } + + /** + * This method allows the exception to include additional information into the WebDAV error response + * + * @param DAV\Server $server + * @param \DOMElement $errorNode + * @return void + */ + 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/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/ReportNotSupported.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/ReportNotSupported.php new file mode 100644 index 00000000000..a8369562702 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/ReportNotSupported.php @@ -0,0 +1,32 @@ +ownerDocument->createElementNS('DAV:', 'd:supported-report'); + $errorNode->appendChild($error); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/RequestedRangeNotSatisfiable.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/RequestedRangeNotSatisfiable.php new file mode 100644 index 00000000000..c8ccfc062a9 --- /dev/null +++ b/htdocs/includes/sabre/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 + */ + function getHTTPCode() { + + return 503; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/TooManyMatches.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/TooManyMatches.php new file mode 100644 index 00000000000..d0f0f84e89b --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/TooManyMatches.php @@ -0,0 +1,38 @@ +ownerDocument->createElementNS('DAV:', 'd:number-of-matches-within-limits'); + $errorNode->appendChild($error); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/UnsupportedMediaType.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Exception/UnsupportedMediaType.php new file mode 100644 index 00000000000..f3d92842da5 --- /dev/null +++ b/htdocs/includes/sabre/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 + * @return void + */ + 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 + */ + 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[] + */ + 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 + */ + function childExists($name) { + + $path = $this->path . '/' . $name; + return file_exists($path); + + } + + /** + * Deletes all files in this directory, and then itself + * + * @return void + */ + function delete() { + + foreach ($this->getChildren() as $child) $child->delete(); + rmdir($this->path); + + } + + /** + * Returns available diskspace information + * + * @return array + */ + function getQuotaInfo() { + $absolute = realpath($this->path); + return [ + disk_total_space($absolute) - disk_free_space($absolute), + disk_free_space($absolute) + ]; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/FS/File.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/FS/File.php new file mode 100644 index 00000000000..4fc5af0574b --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/FS/File.php @@ -0,0 +1,95 @@ +path, $data); + clearstatcache(true, $this->path); + + } + + /** + * Returns the data + * + * @return resource + */ + function get() { + + return fopen($this->path, 'r'); + + } + + /** + * Delete the current file + * + * @return void + */ + function delete() { + + unlink($this->path); + + } + + /** + * Returns the size of the node, in bytes + * + * @return int + */ + 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 + */ + 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 + */ + function getContentType() { + + return null; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/FS/Node.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/FS/Node.php new file mode 100644 index 00000000000..424718f966e --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/FS/Node.php @@ -0,0 +1,80 @@ +path = $path; + + } + + + + /** + * Returns the name of the node + * + * @return string + */ + function getName() { + + list(, $name) = URLUtil::splitPath($this->path); + return $name; + + } + + /** + * Renames the node + * + * @param string $name The new name + * @return void + */ + function setName($name) { + + list($parentPath, ) = URLUtil::splitPath($this->path); + list(, $newName) = URLUtil::splitPath($name); + + $newPath = $parentPath . '/' . $newName; + rename($this->path, $newPath); + + $this->path = $newPath; + + } + + /** + * Returns the last modification time, as a unix timestamp + * + * @return int + */ + function getLastModified() { + + return filemtime($this->path); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/FSExt/Directory.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/FSExt/Directory.php new file mode 100644 index 00000000000..dd5f992dbce --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/FSExt/Directory.php @@ -0,0 +1,211 @@ +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 + * @return void + */ + 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 + */ + 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 + */ + 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[] + */ + 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 + */ + 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 + */ + 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 + */ + 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 inheritence + // with the current class. + rename($sourceNode->path, $this->path . '/' . $targetName); + + return true; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/FSExt/File.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/FSExt/File.php new file mode 100644 index 00000000000..eb5ae19fec5 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/FSExt/File.php @@ -0,0 +1,152 @@ +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 + * * 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 + */ + 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; + } + 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 + */ + function get() { + + return fopen($this->path, 'r'); + + } + + /** + * Delete the current file + * + * @return bool + */ + 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 + */ + 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 + */ + function getContentType() { + + return null; + + } + + /** + * Returns the size of the file, in bytes + * + * @return int + */ + function getSize() { + + return filesize($this->path); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/File.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/File.php new file mode 100644 index 00000000000..5161fbd5153 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/File.php @@ -0,0 +1,96 @@ +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 + */ + function getLocks($uri, $returnChildLocks) { + + $newLocks = []; + + $locks = $this->getData(); + + foreach ($locks as $lock) { + + if ($lock->uri === $uri || + //deep locks on parents + ($lock->depth != 0 && strpos($uri, $lock->uri . '/') === 0) || + + // locks on children + ($returnChildLocks && (strpos($lock->uri, $uri . '/') === 0))) { + + $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 + * @param LockInfo $lockInfo + * @return bool + */ + 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 + * @param LockInfo $lockInfo + * @return bool + */ + 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 + * + * @param array $newData + * @return void + */ + 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/htdocs/includes/sabre/sabre/dav/lib/DAV/Locks/Backend/PDO.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Locks/Backend/PDO.php new file mode 100644 index 00000000000..510f266f7e6 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Locks/Backend/PDO.php @@ -0,0 +1,180 @@ +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 + */ + 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 + * @param LockInfo $lockInfo + * @return bool + */ + 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 + * @param LockInfo $lockInfo + * @return bool + */ + function unlock($uri, LockInfo $lockInfo) { + + $stmt = $this->pdo->prepare('DELETE FROM ' . $this->tableName . ' WHERE uri = ? AND token = ?'); + $stmt->execute([$uri, $lockInfo->token]); + + return $stmt->rowCount() === 1; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Locks/LockInfo.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Locks/LockInfo.php new file mode 100644 index 00000000000..2c8cca0fee4 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Locks/LockInfo.php @@ -0,0 +1,80 @@ +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 + * + * @param Backend\BackendInterface $locksBackend + */ + function __construct(Backend\BackendInterface $locksBackend) { + + $this->locksBackend = $locksBackend; + + } + + /** + * Initializes the plugin + * + * This method is automatically called by the Server class after addPlugin. + * + * @param DAV\Server $server + * @return void + */ + 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 + */ + function getPluginName() { + + return 'locks'; + + } + + /** + * This method is called after most properties have been found + * it allows us to add in any Lock-related properties + * + * @param DAV\PropFind $propFind + * @param DAV\INode $node + * @return void + */ + 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 + */ + 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 + */ + 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 + */ + 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 + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + 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 ($existingLock->scope === LockInfo::EXCLUSIVE) { + throw new DAV\Exception\ConflictingLock($existingLock); + } + } + + $lockInfo = $this->parseLockRequest($body); + $lockInfo->depth = $this->server->getHTTPDepth(); + $lockInfo->uri = $uri; + if ($existingLock && $lockInfo->scope != LockInfo::SHARED) + 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 + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return void + */ + 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 + * @return void + */ + function afterUnbind($path) { + + $locks = $this->getLocks($path, $includeChildren = true); + foreach ($locks as $lock) { + $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 + * @param LockInfo $lockInfo + * @return bool + */ + 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 + * @param LockInfo $lockInfo + * @return bool + */ + 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 + */ + function getTimeoutHeader() { + + $header = $this->server->httpRequest->getHeader('Timeout'); + + if ($header) { + + if (stripos($header, 'second-') === 0) $header = (int)(substr($header, 7)); + elseif (stripos($header, 'infinite') === 0) $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 + * + * @param LockInfo $lockInfo + * @return string + */ + protected function generateLockResponse(LockInfo $lockInfo) { + + return $this->server->xml->write('{DAV:}prop', [ + '{DAV:}lockdiscovery' => + new DAV\Xml\Property\LockDiscovery([$lockInfo]) + ]); + } + + /** + * 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 RequestInterface $request + * @param mixed $conditions + * @return void + */ + 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 (substr($token['token'], 0, 16) !== 'opaquelocktoken:') { + 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 + */ + 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/htdocs/includes/sabre/sabre/dav/lib/DAV/MkCol.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/MkCol.php new file mode 100644 index 00000000000..042e14bca31 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/MkCol.php @@ -0,0 +1,72 @@ +resourceType = $resourceType; + parent::__construct($mutations); + + } + + /** + * Returns the resourcetype of the new collection. + * + * @return string[] + */ + 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 + */ + function hasResourceType($resourceType) { + + return count(array_diff((array)$resourceType, $this->resourceType)) === 0; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Mount/Plugin.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Mount/Plugin.php new file mode 100644 index 00000000000..dc923ad8522 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Mount/Plugin.php @@ -0,0 +1,86 @@ +server = $server; + $this->server->on('method:GET', [$this, 'httpGet'], 90); + + } + + /** + * 'beforeMethod' event handles. This event handles intercepts GET requests ending + * with ?mount + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + 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 ResponseInterface $response + * @param string $uri absolute uri + * @return void + */ + 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/htdocs/includes/sabre/sabre/dav/lib/DAV/Node.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Node.php new file mode 100644 index 00000000000..ef6eea18e06 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Node.php @@ -0,0 +1,54 @@ +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. + * + * @param DAV\Server $server + * @return void + */ + 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 + */ + 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 + */ + 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 + */ + 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 + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return void + */ + 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( + $request->getHeader('Content-Type') + ); + + if ($contentType != 'application/x-sabredav-partialupdate') { + 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. + * + * @param RequestInterface $request + * @return array|null + */ + 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 ($matches[1] === 'append') { + return [self::RANGE_APPEND]; + } elseif (strlen($matches[2]) > 0) { + return [self::RANGE_START, $matches[2], $matches[3] ?: null]; + } else { + return [self::RANGE_END, $matches[4]]; + } + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/PropFind.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/PropFind.php new file mode 100644 index 00000000000..0940a1ce29a --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/PropFind.php @@ -0,0 +1,347 @@ +path = $path; + $this->properties = $properties; + $this->depth = $depth; + $this->requestType = $requestType; + + if ($requestType === self::ALLPROPS) { + $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 + * @return void + */ + function handle($propertyName, $valueOrCallBack) { + + if ($this->itemsLeft && isset($this->result[$propertyName]) && $this->result[$propertyName][0] === 404) { + 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 + * @return void + */ + 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 ($this->requestType === self::ALLPROPS) { + $this->result[$propertyName] = [$status, $value]; + } + return; + } + if ($status !== 404 && $this->result[$propertyName][0] === 404) { + $this->itemsLeft--; + } elseif ($status === 404 && $this->result[$propertyName][0] !== 404) { + $this->itemsLeft++; + } + $this->result[$propertyName] = [$status, $value]; + + } + + /** + * Returns the current value for a property. + * + * @param string $propertyName + * @return mixed + */ + 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 + */ + function getStatus($propertyName) { + + return isset($this->result[$propertyName]) ? $this->result[$propertyName][0] : null; + + } + + /** + * Updates the path for this PROPFIND. + * + * @param string $path + * @return void + */ + function setPath($path) { + + $this->path = $path; + + } + + /** + * Returns the path this PROPFIND request is for. + * + * @return string + */ + function getPath() { + + return $this->path; + + } + + /** + * Returns the depth of this propfind request. + * + * @return int + */ + function getDepth() { + + return $this->depth; + + } + + /** + * Updates the depth of this propfind request. + * + * @param int $depth + * @return void + */ + function setDepth($depth) { + + $this->depth = $depth; + + } + + /** + * Returns all propertynames that have a 404 status, and thus don't have a + * value yet. + * + * @return array + */ + function get404Properties() { + + if ($this->itemsLeft === 0) { + return []; + } + $result = []; + foreach ($this->result as $propertyName => $stuff) { + if ($stuff[0] === 404) { + $result[] = $propertyName; + } + } + return $result; + + } + + /** + * Returns the full list of requested properties. + * + * This returns just their names, not a status or value. + * + * @return array + */ + function getRequestedProperties() { + + return $this->properties; + + } + + /** + * Returns true if this was an '{DAV:}allprops' request. + * + * @return bool + */ + function isAllProps() { + + return $this->requestType === self::ALLPROPS; + + } + + /** + * 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 + */ + 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 ($this->requestType === self::ALLPROPS) 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/htdocs/includes/sabre/sabre/dav/lib/DAV/PropPatch.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/PropPatch.php new file mode 100644 index 00000000000..6d599dacc4a --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/PropPatch.php @@ -0,0 +1,373 @@ +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 + * @param callable $callback + * @return void + */ + 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. + * + * @param callable $callback + * @return void + */ + 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 + * @return void + */ + 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 + * @return void + */ + 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[] + */ + 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 + */ + 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 + */ + 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 ($status === 202) { + // Failed dependency + $this->result[$propertyName] = 424; + } + } + + } + + return !$this->failed; + + } + + /** + * Executes a property callback with the single-property syntax. + * + * @param string $propertyName + * @param callable $callback + * @return void + */ + 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. + * + * @param array $propertyList + * @param callable $callback + * @return void + */ + 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 ($result === true) { + + // Success + foreach ($argument as $propertyName => $propertyValue) { + $this->result[$propertyName] = is_null($propertyValue) ? 204 : 200; + } + + } elseif ($result === false) { + // 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 + */ + function getResult() { + + return $this->result; + + } + + /** + * Returns the full list of mutations + * + * @return array + */ + function getMutations() { + + return $this->mutations; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/PropertyStorage/Backend/BackendInterface.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/PropertyStorage/Backend/BackendInterface.php new file mode 100644 index 00000000000..b15d7fef944 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/PropertyStorage/Backend/BackendInterface.php @@ -0,0 +1,80 @@ +isAllProps(). + * + * @param string $path + * @param PropFind $propFind + * @return void + */ + 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 + * @param PropPatch $propPatch + * @return void + */ + 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 + * @return void + */ + 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 + * @return void + */ + function move($source, $destination); + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php new file mode 100644 index 00000000000..6f3f1feaf57 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php @@ -0,0 +1,246 @@ +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 + * @param PropFind $propFind + * @return void + */ + function propFind($path, PropFind $propFind) { + + if (!$propFind->isAllProps() && count($propFind->get404Properties()) === 0) { + 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 (gettype($row['value']) === 'resource') { + $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 + * @param PropPatch $propPatch + * @return void + */ + function propPatch($path, PropPatch $propPatch) { + + $propPatch->handleRemaining(function($properties) use ($path) { + + + if ($this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME) === 'pgsql') { + + $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 + * @return void + */ + 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 + * @return void + */ + 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 && strpos($row['path'], $source . '/') !== 0) continue; + + $trailingPart = substr($row['path'], strlen($source) + 1); + $newPath = $destination; + if ($trailingPart) { + $newPath .= '/' . $trailingPart; + } + $update->execute([$newPath, $row['id']]); + + } + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/PropertyStorage/Plugin.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/PropertyStorage/Plugin.php new file mode 100644 index 00000000000..a66a14113fb --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/PropertyStorage/Plugin.php @@ -0,0 +1,185 @@ +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. + * + * @param Server $server + * @return void + */ + 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. + * + * @param PropFind $propFind + * @param INode $node + * @return void + */ + 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 + * @param PropPatch $propPatch + * @return void + */ + 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 + * @return void + */ + 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 + * @return void + */ + 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 + */ + 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 + */ + 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/htdocs/includes/sabre/sabre/dav/lib/DAV/Server.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Server.php new file mode 100644 index 00000000000..6805ec0b013 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Server.php @@ -0,0 +1,1687 @@ + '{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 + */ + static $exposeVersion = true; + + /** + * 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 + */ + function __construct($treeOrNode = null) { + + if ($treeOrNode instanceof Tree) { + $this->tree = $treeOrNode; + } elseif ($treeOrNode instanceof INode) { + $this->tree = new Tree($treeOrNode); + } elseif (is_array($treeOrNode)) { + + // If it's an array, a list of nodes was passed, and we need to + // create the root node. + foreach ($treeOrNode as $node) { + if (!($node instanceof INode)) { + throw new Exception('Invalid argument passed to constructor. If you\'re passing an array, all the values must implement Sabre\\DAV\\INode'); + } + } + + $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 = new HTTP\Sapi(); + $this->httpResponse = new HTTP\Response(); + $this->httpRequest = $this->sapi->getRequest(); + $this->addPlugin(new CorePlugin()); + + } + + /** + * Starts the DAV Server + * + * @return void + */ + function exec() { + + 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 (\Exception $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($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); + + } + + } + + /** + * Sets the base server uri + * + * @param string $uri + * @return void + */ + 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 + */ + 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 + */ + 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 = URLUtil::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 + * + * @param ServerPlugin $plugin + * @return void + */ + 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 + */ + function getPlugin($name) { + + if (isset($this->plugins[$name])) + return $this->plugins[$name]; + + return null; + + } + + /** + * Returns all plugins + * + * @return array + */ + function getPlugins() { + + return $this->plugins; + + } + + /** + * Returns the PSR-3 logger object. + * + * @return LoggerInterface + */ + 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 RequestInterface $request + * @param ResponseInterface $response + * @param bool $sendResponse Whether to send the HTTP response to the DAV client. + * @return void + */ + function invokeMethod(RequestInterface $request, ResponseInterface $response, $sendResponse = true) { + + $method = $request->getMethod(); + + if (!$this->emit('beforeMethod:' . $method, [$request, $response])) return; + if (!$this->emit('beforeMethod', [$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])) { + if ($this->emit('method', [$request, $response])) { + $exMessage = "There was no plugin in the system that was willing to handle this " . $method . " method."; + if ($method === "GET") { + $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 (!$this->emit('afterMethod', [$request, $response])) return; + + if ($response->getStatus() === null) { + 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 + */ + 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 + */ + 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 + */ + function calculateUri($uri) { + + if ($uri[0] != '/' && strpos($uri, '://')) { + + $uri = parse_url($uri, PHP_URL_PATH); + + } + + $uri = Uri\normalize(str_replace('//', '/', $uri)); + $baseUri = Uri\normalize($this->getBaseUri()); + + if (strpos($uri, $baseUri) === 0) { + + return trim(URLUtil::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 + */ + 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 ($depth == 'infinity') 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 array|null + */ + 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] !== '' ? $matches[1] : null, + $matches[2] !== '' ? $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 + */ + 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 ($this->httpRequest->getHeader('Brief') == 't') { + $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) + * + * @param RequestInterface $request + * @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 + */ + 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 (strtoupper($overwrite) == 'T') $overwrite = true; + elseif (strtoupper($overwrite) == 'F') $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) = URLUtil::splitPath($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' => !!$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 + */ + 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 + */ + function getPropertiesForChildren($path, $propertyNames) { + + $result = []; + foreach ($this->getPropertiesForPath($path, $propertyNames, 1) as $k => $row) { + + // Skipping the parent path + if ($k === 0) 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 + */ + 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\Util::toHTTPDate($properties[$property]->getTime()); + } + + } + + return $headers; + + } + + /** + * Small helper to support PROPFIND with DEPTH_INFINITY. + * + * @param PropFind $propFind + * @param array $yieldFirst + * @return \Iterator + */ + private function generatePathNodes(PropFind $propFind, array $yieldFirst = null) { + if ($yieldFirst !== null) { + yield $yieldFirst; + } + $newDepth = $propFind->getDepth(); + $path = $propFind->getPath(); + + if ($newDepth !== self::DEPTH_INFINITY) { + $newDepth--; + } + + foreach ($this->tree->getChildren($path) as $childNode) { + $subPropFind = clone $propFind; + $subPropFind->setDepth($newDepth); + if ($path !== '') { + $subPath = $path . '/' . $childNode->getName(); + } else { + $subPath = $childNode->getName(); + } + $subPropFind->setPath($subPath); + + yield [ + $subPropFind, + $childNode + ]; + + if (($newDepth === self::DEPTH_INFINITY || $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() + */ + 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 + */ + 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 && $depth != 0) $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 || $depth === self::DEPTH_INFINITY) && $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. + * + * @param array $paths + * @param array $propertyNames + * @return array + */ + 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. + * + * @param PropFind $propFind + * @param INode $node + * @return bool + */ + 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 + */ + function createFile($uri, $data, &$etag = null) { + + list($dir, $name) = URLUtil::splitPath($uri); + + if (!$this->emit('beforeBind', [$uri])) return false; + + $parent = $this->tree->getNodeForPath($dir); + 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 + */ + 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 + * @return void + */ + function createDirectory($uri) { + + $this->createCollection($uri, new MkCol(['{DAV:}collection'], [])); + + } + + /** + * Use this method to create a new collection + * + * @param string $uri The new uri + * @param MkCol $mkCol + * @return array|null + */ + function createCollection($uri, MkCol $mkCol) { + + list($parentUri, $newName) = URLUtil::splitPath($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 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 + * @param array $properties + * @return array + */ + 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. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + 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 the 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 ($request->getMethod() === 'GET') { + $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\Util::parseHTTPDate($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\Util::toHTTPDate($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\Util::parseHTTPDate($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"' + * ] + * ] + * ], + * ], + * ] + * + * @param RequestInterface $request + * @return array + */ + 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. + * + * @param INode $node + * @return array + */ + function getResourceTypeForNode(INode $node) { + + $result = []; + foreach ($this->resourceTypeMapping as $className => $resourceType) { + if ($node instanceof $className) $result[] = $resourceType; + } + return $result; + + } + + // }}} + // {{{ XML Readers & Writers + + + /** + * Generates 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 string + */ + function generateMultiStatus($fileProperties, $strip404s = false) { + + $w = $this->xml->getWriter(); + $w->openMemory(); + $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(); + + return $w->outputMemory(); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/ServerPlugin.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/ServerPlugin.php new file mode 100644 index 00000000000..b2c468ab387 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/ServerPlugin.php @@ -0,0 +1,110 @@ + $this->getPluginName(), + 'description' => null, + 'link' => null, + ]; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Sharing/ISharedNode.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Sharing/ISharedNode.php new file mode 100644 index 00000000000..034aefbdce6 --- /dev/null +++ b/htdocs/includes/sabre/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 + * @return void + */ + 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. + * + * @param PropFind $propFind + * @param INode $node + * @return void + */ + 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 + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return null|bool + */ + function httpPost(RequestInterface $request, ResponseInterface $response) { + + $path = $request->getPath(); + $contentType = $request->getHeader('Content-Type'); + + // We're only interested in the davsharing content type. + if (strpos($contentType, 'application/davsharing+xml') === false) { + 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 reqeuests the privileges + * hat are supported on a particular node. + * + * We need to add a number of privileges for scheduling purposes. + * + * @param INode $node + * @param array $supportedPrivilegeSet + */ + 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 + */ + 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 INode $node + * @param string $output + * @param string $path + * @return bool|null + */ + 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 + */ + function browserPostAction($path, $action, $postVars) { + + if ($action !== 'share') { + 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/htdocs/includes/sabre/sabre/dav/lib/DAV/SimpleCollection.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/SimpleCollection.php new file mode 100644 index 00000000000..998cfcbff5f --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/SimpleCollection.php @@ -0,0 +1,107 @@ +name = $name; + foreach ($children as $child) { + + if (!($child instanceof INode)) throw new Exception('Only instances of Sabre\DAV\INode are allowed to be passed in the children argument'); + $this->addChild($child); + + } + + } + + /** + * Adds a new childnode to this collection + * + * @param INode $child + * @return void + */ + function addChild(INode $child) { + + $this->children[$child->getName()] = $child; + + } + + /** + * Returns the name of the collection + * + * @return string + */ + 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 + */ + 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[] + */ + function getChildren() { + + return array_values($this->children); + + } + + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/SimpleFile.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/SimpleFile.php new file mode 100644 index 00000000000..bcad786f34a --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/SimpleFile.php @@ -0,0 +1,121 @@ +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 + */ + function getName() { + + return $this->name; + + } + + /** + * Returns the data + * + * This method may either return a string or a readable stream resource + * + * @return mixed + */ + function get() { + + return $this->contents; + + } + + /** + * Returns the size of the file, in bytes. + * + * @return int + */ + 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 + */ + 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 + */ + function getContentType() { + + return $this->mimeType; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/StringUtil.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/StringUtil.php new file mode 100644 index 00000000000..10eecebfd47 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/StringUtil.php @@ -0,0 +1,91 @@ + '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 + */ + function getChanges($syncToken, $syncLevel, $limit = null); + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Sync/Plugin.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Sync/Plugin.php new file mode 100644 index 00000000000..8e4b1aa6412 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Sync/Plugin.php @@ -0,0 +1,277 @@ +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 ($reportName === '{DAV:}sync-collection') { + $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 + */ + 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 + * @param SyncCollectionReport $report + * @return void + */ + 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 (substr($syncToken, 0, strlen(self::SYNCTOKEN_PREFIX)) !== 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'); + + } + + // Encoding the response + $this->sendSyncCollectionResponse( + $changeInfo['syncToken'], + $uri, + $changeInfo['added'], + $changeInfo['modified'], + $changeInfo['deleted'], + $report->properties + ); + + } + + /** + * Sends the response to a sync-collection request. + * + * @param string $syncToken + * @param string $collectionUrl + * @param array $added + * @param array $modified + * @param array $deleted + * @param array $properties + * @return void + */ + protected function sendSyncCollectionResponse($syncToken, $collectionUrl, array $added, array $modified, array $deleted, array $properties) { + + + $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); + + } + $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. + * + * @param DAV\PropFind $propFind + * @param DAV\INode $node + * @return void + */ + 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 RequestInterface $request + * @param array $conditions + * @return void + */ + 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 (substr($token['token'], 0, strlen(self::SYNCTOKEN_PREFIX)) !== 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 + */ + function getPluginInfo() { + + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds support for WebDAV Collection Sync (rfc6578)', + 'link' => 'http://sabre.io/dav/sync/', + ]; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/TemporaryFileFilterPlugin.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/TemporaryFileFilterPlugin.php new file mode 100644 index 00000000000..7b453d10536 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/TemporaryFileFilterPlugin.php @@ -0,0 +1,297 @@ +dataDir = $dataDir; + + } + + /** + * Initialize the plugin + * + * This is called automatically be the Server class after this plugin is + * added with Sabre\DAV\Server::addPlugin() + * + * @param Server $server + * @return void + */ + 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. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + 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 ICollection $parent + * @param bool $modified Should be set to true, if this event handler + * changed &$data. + * @return bool + */ + 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) = URLUtil::splitPath($path); + + 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 RequestInterface $request + * @param ResponseInterface $hR + * @param string $tempLocation + * @return bool + */ + 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 RequestInterface $request + * @param ResponseInterface $hR + * @param string $tempLocation + * @return bool + */ + 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 RequestInterface $request + * @param ResponseInterface $hR + * @param string $tempLocation + * @return bool + */ + 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 RequestInterface $request + * @param ResponseInterface $hR + * @param string $tempLocation + * @return bool + */ + 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/htdocs/includes/sabre/sabre/dav/lib/DAV/Tree.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Tree.php new file mode 100644 index 00000000000..5d2792503f9 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Tree.php @@ -0,0 +1,340 @@ +rootNode = $rootNode; + + } + + /** + * Returns the INode object for the requested path + * + * @param string $path + * @return INode + */ + 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; + } + + // Attempting to fetch its parent + list($parentName, $baseName) = URLUtil::splitPath($path); + + // If there was no parent, we must simply ask it from the root node. + if ($parentName === "") { + $node = $this->rootNode->getChild($baseName); + } else { + // Otherwise, we recursively grab the parent and ask him/her. + $parent = $this->getNodeForPath($parentName); + + if (!($parent instanceof ICollection)) + throw new Exception\NotFound('Could not find node at path: ' . $path); + + $node = $parent->getChild($baseName); + + } + + $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 + */ + function nodeExists($path) { + + try { + + // The root always exists + if ($path === '') return true; + + list($parent, $base) = URLUtil::splitPath($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 + * @return void + */ + function copy($sourcePath, $destinationPath) { + + $sourceNode = $this->getNodeForPath($sourcePath); + + // grab the dirname and basename components + list($destinationDir, $destinationName) = URLUtil::splitPath($destinationPath); + + $destinationParent = $this->getNodeForPath($destinationDir); + $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 + * @return int + */ + function move($sourcePath, $destinationPath) { + + list($sourceDir) = URLUtil::splitPath($sourcePath); + list($destinationDir, $destinationName) = URLUtil::splitPath($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 + * @return void + */ + function delete($path) { + + $node = $this->getNodeForPath($path); + $node->delete(); + + list($parent) = URLUtil::splitPath($path); + $this->markDirty($parent); + + } + + /** + * Returns a list of childnodes for a given path. + * + * @param string $path + * @return array + */ + function getChildren($path) { + + $node = $this->getNodeForPath($path); + $children = $node->getChildren(); + $basePath = trim($path, '/'); + if ($basePath !== '') $basePath .= '/'; + + foreach ($children as $child) { + + $this->cache[$basePath . $child->getName()] = $child; + + } + return $children; + + } + + /** + * 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 + * @return void + */ + 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 || strpos($nodePath, $path . '/') === 0) + 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 + */ + function getMultipleNodes($paths) { + + // Finding common parents + $parents = []; + foreach ($paths as $path) { + list($parent, $node) = URLUtil::splitPath($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 INode $source + * @param ICollection $destinationParent + * @param string $destinationName + * @return void + */ + protected function copyNode(INode $source, ICollection $destinationParent, $destinationName = null) { + + if (!$destinationName) $destinationName = $source->getName(); + + 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/htdocs/includes/sabre/sabre/dav/lib/DAV/UUIDUtil.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/UUIDUtil.php new file mode 100644 index 00000000000..177adafd3b8 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/UUIDUtil.php @@ -0,0 +1,64 @@ +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. + * + * @param Reader $reader + * @return mixed + */ + 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->nodeType === Reader::ELEMENT) { + + $clark = $reader->getClark(); + $values[$clark] = self::parseCurrentElement($reader)['value']; + + } else { + $reader->read(); + } + + } while ($reader->nodeType !== Reader::END_ELEMENT); + + $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. + * + * @param Reader $reader + * @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 ($type === 'string') { + $type .= ' (' . $deserializer . ')'; + } elseif ($type === 'object') { + $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/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Element/Response.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Element/Response.php new file mode 100644 index 00000000000..ce97ae94366 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Element/Response.php @@ -0,0 +1,253 @@ +href = $href; + $this->responseProperties = $responseProperties; + $this->httpStatus = $httpStatus; + + } + + /** + * Returns the url + * + * @return string + */ + function getHref() { + + return $this->href; + + } + + /** + * Returns the httpStatus value + * + * @return string + */ + function getHttpStatus() { + + return $this->httpStatus; + + } + + /** + * Returns the property list + * + * @return array + */ + 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. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Writer $writer) { + + if ($status = $this->getHTTPStatus()) { + $writer->writeElement('{DAV:}status', 'HTTP/1.1 ' . $status . ' ' . \Sabre\HTTP\Response::$statusCodes[$status]); + } + $writer->writeElement('{DAV:}href', $writer->contextUri . \Sabre\HTTP\encodePath($this->getHref())); + + $empty = true; + + foreach ($this->getResponseProperties() as $status => $properties) { + + // Skipping empty lists + if (!$properties || (!ctype_digit($status) && !is_int($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 + + } + if ($empty) { + /* + * The WebDAV spec _requires_ at least one DAV:propstat to appear for + * every DAV:response. 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. + * + * @param Reader $reader + * @return mixed + */ + 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 []; + } + $values = []; + $reader->read(); + do { + if ($reader->nodeType === Reader::ELEMENT) { + $clark = $reader->getClark(); + + if ($reader->isEmptyElement) { + $values[$clark] = null; + $reader->next(); + } else { + $values[$clark] = $reader->parseCurrentElement()['value']; + } + } else { + $reader->read(); + } + } while ($reader->nodeType !== Reader::END_ELEMENT); + $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/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Element/Sharee.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Element/Sharee.php new file mode 100644 index 00000000000..e187bf11cb0 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Element/Sharee.php @@ -0,0 +1,199 @@ + $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. + * + * @param Writer $writer + * @return void + */ + 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. + * + * @param Reader $reader + * @return mixed + */ + 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/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/Complex.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/Complex.php new file mode 100644 index 00000000000..258806e4a59 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/Complex.php @@ -0,0 +1,89 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + + $xml = $reader->readInnerXml(); + + if ($reader->nodeType === Reader::ELEMENT && $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/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/GetLastModified.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/GetLastModified.php new file mode 100644 index 00000000000..101a0f0c91a --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/GetLastModified.php @@ -0,0 +1,110 @@ +time = clone $time; + } else { + $this->time = new DateTime('@' . $time); + } + + // Setting timezone to UTC + $this->time->setTimezone(new DateTimeZone('UTC')); + + } + + /** + * getTime + * + * @return DateTime + */ + 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. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Writer $writer) { + + $writer->write( + HTTP\Util::toHTTPDate($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. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + + return + new self(new DateTime($reader->parseInnerTree())); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/Href.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/Href.php new file mode 100644 index 00000000000..6c4f11b87ad --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/Href.php @@ -0,0 +1,165 @@ +hrefs = $hrefs; + + } + + /** + * Returns the first Href. + * + * @return string + */ + function getHref() { + + return $this->hrefs[0]; + + } + + /** + * Returns the hrefs as an array + * + * @return array + */ + 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. + * + * @param Writer $writer + * @return void + */ + 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. + * + * @param HtmlOutputHelper $html + * @return string + */ + 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. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + + $hrefs = []; + foreach ((array)$reader->parseInnerTree() as $elem) { + if ($elem['name'] !== '{DAV:}href') + continue; + + $hrefs[] = $elem['value']; + + } + if ($hrefs) { + return new self($hrefs, false); + } + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/Invite.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/Invite.php new file mode 100644 index 00000000000..6adad365040 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/Invite.php @@ -0,0 +1,70 @@ +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. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Writer $writer) { + + foreach ($this->sharees as $sharee) { + $writer->writeElement('{DAV:}sharee', $sharee); + } + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/LocalHref.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/LocalHref.php new file mode 100644 index 00000000000..00d2fa708d1 --- /dev/null +++ b/htdocs/includes/sabre/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. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Writer $writer) { + + foreach ($this->locks as $lock) { + + $writer->startElement('{DAV:}activelock'); + + $writer->startElement('{DAV:}lockscope'); + if ($lock->scope === LockInfo::SHARED) { + $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', ($lock->depth == DAV\Server::DEPTH_INFINITY ? 'infinity' : $lock->depth)); + $writer->writeElement('{DAV:}timeout', 'Second-' . $lock->timeout); + + $writer->startElement('{DAV:}locktoken'); + $writer->writeElement('{DAV:}href', 'opaquelocktoken:' . $lock->token); + $writer->endElement(); // {DAV:}locktoken + + $writer->writeElement('{DAV:}owner', new XmlFragment($lock->owner)); + $writer->endElement(); // {DAV:}activelock + + } + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/ResourceType.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/ResourceType.php new file mode 100644 index 00000000000..ce640ff32db --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/ResourceType.php @@ -0,0 +1,128 @@ +value; + + } + + /** + * Checks if the principal contains a certain value + * + * @param string $type + * @return bool + */ + function is($type) { + + return in_array($type, $this->value); + + } + + /** + * Adds a resourcetype value to this property + * + * @param string $type + * @return void + */ + 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. + * + * @param Reader $reader + * @return mixed + */ + 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. + * + * @param HtmlOutputHelper $html + * @return string + */ + function toHtml(HtmlOutputHelper $html) { + + return implode( + ', ', + array_map([$html, 'xmlName'], $this->getValue()) + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/ShareAccess.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/ShareAccess.php new file mode 100644 index 00000000000..939062f76fd --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/ShareAccess.php @@ -0,0 +1,143 @@ +value = $shareAccess; + + } + + /** + * Returns the current value. + * + * @return int + */ + 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. + * + * @param Writer $writer + * @return void + */ + 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. + * + * @param Reader $reader + * @return mixed + */ + 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/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/SupportedLock.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/SupportedLock.php new file mode 100644 index 00000000000..677fdde4bdf --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/SupportedLock.php @@ -0,0 +1,54 @@ +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/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/SupportedMethodSet.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/SupportedMethodSet.php new file mode 100644 index 00000000000..1a3878ef71f --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/SupportedMethodSet.php @@ -0,0 +1,121 @@ +methods = $methods; + + } + + /** + * Returns the list of supported http methods. + * + * @return string[] + */ + function getValue() { + + return $this->methods; + + } + + /** + * Returns true or false if the property contains a specific method. + * + * @param string $methodName + * @return bool + */ + 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. + * + * @param Writer $writer + * @return void + */ + 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. + * + * @param HtmlOutputHelper $html + * @return string + */ + function toHtml(HtmlOutputHelper $html) { + + return implode( + ', ', + array_map([$html, 'h'], $this->getValue()) + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/SupportedReportSet.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/SupportedReportSet.php new file mode 100644 index 00000000000..96383ec96c8 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Property/SupportedReportSet.php @@ -0,0 +1,154 @@ +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 + * @return void + */ + 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[] + */ + function getValue() { + + return $this->reports; + + } + + /** + * Returns true or false if the property contains a specific report. + * + * @param string $reportName + * @return bool + */ + 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. + * + * @param Writer $writer + * @return void + */ + 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. + * + * @param HtmlOutputHelper $html + * @return string + */ + function toHtml(HtmlOutputHelper $html) { + + return implode( + ', ', + array_map([$html, 'xmlName'], $this->getValue()) + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Request/Lock.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Request/Lock.php new file mode 100644 index 00000000000..c315a9a4502 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Request/Lock.php @@ -0,0 +1,81 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + 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 ($elem['name'] === '{DAV:}exclusive') $new->scope = LockInfo::EXCLUSIVE; + } + } + return $new; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Request/MkCol.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Request/MkCol.php new file mode 100644 index 00000000000..9490bf58cf2 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Request/MkCol.php @@ -0,0 +1,82 @@ +value array with properties that are supposed to get set + * during creation of the new collection. + * + * @return array + */ + 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. + * + * @param Reader $reader + * @return mixed + */ + 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 ($elem['name'] === '{DAV:}set') { + $self->properties = array_merge($self->properties, $elem['value']['{DAV:}prop']); + } + } + + return $self; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Request/PropFind.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Request/PropFind.php new file mode 100644 index 00000000000..f1b5b6fdc6f --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Request/PropFind.php @@ -0,0 +1,83 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + 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/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Request/PropPatch.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Request/PropPatch.php new file mode 100644 index 00000000000..821b9e047d4 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Request/PropPatch.php @@ -0,0 +1,118 @@ +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. + * + * @param Reader $reader + * @return mixed + */ + 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 ($elem['name'] === '{DAV:}set') { + $self->properties = array_merge($self->properties, $elem['value']['{DAV:}prop']); + } + if ($elem['name'] === '{DAV:}remove') { + + // Ensuring there are no values. + foreach ($elem['value']['{DAV:}prop'] as $remove => $value) { + $self->properties[$remove] = null; + } + + } + } + + return $self; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Request/ShareResource.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Request/ShareResource.php new file mode 100644 index 00000000000..526a4eb6ff6 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Request/ShareResource.php @@ -0,0 +1,81 @@ +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. + * + * @param Reader $reader + * @return mixed + */ + 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 ($elem['name'] !== '{DAV:}sharee') continue; + $sharees[] = $elem['value']; + + } + + return new self($sharees); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Request/SyncCollectionReport.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Request/SyncCollectionReport.php new file mode 100644 index 00000000000..830293a0178 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Request/SyncCollectionReport.php @@ -0,0 +1,122 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + 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 ($child['name'] === '{DAV:}nresults') { + $nresults = (int)$child['value']; + } + } + $self->limit = $nresults; + } + + if (isset($elems['{DAV:}sync-level'])) { + + $value = $elems['{DAV:}sync-level']; + if ($value === 'infinity') { + $value = \Sabre\DAV\Server::DEPTH_INFINITY; + } + $self->syncLevel = $value; + + } + + return $self; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Response/MultiStatus.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Response/MultiStatus.php new file mode 100644 index 00000000000..cf5a0453b4a --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Response/MultiStatus.php @@ -0,0 +1,142 @@ +responses = $responses; + $this->syncToken = $syncToken; + + } + + /** + * Returns the response list. + * + * @return \Sabre\DAV\Xml\Element\Response[] + */ + function getResponses() { + + return $this->responses; + + } + + /** + * Returns the sync-token, if available. + * + * @return string|null + */ + 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. + * + * @param Writer $writer + * @return void + */ + 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. + * + * @param Reader $reader + * @return mixed + */ + 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 ($elem['name'] === '{DAV:}response') { + $responses[] = $elem['value']; + } + if ($elem['name'] === '{DAV:}sync-token') { + $syncToken = $elem['value']; + } + } + + return new self($responses, $syncToken); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Service.php b/htdocs/includes/sabre/sabre/dav/lib/DAV/Xml/Service.php new file mode 100644 index 00000000000..f41ed984ad9 --- /dev/null +++ b/htdocs/includes/sabre/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/htdocs/includes/sabre/sabre/dav/lib/DAVACL/ACLTrait.php b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/ACLTrait.php new file mode 100644 index 00000000000..602654a2ed3 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/ACLTrait.php @@ -0,0 +1,100 @@ + '{DAV:}all', + 'principal' => '{DAV:}owner', + 'protected' => true, + ] + ]; + + } + + /** + * Updates the ACL + * + * This method will receive a list of new ACE's as an array argument. + * + * @param array $acl + * @return void + */ + 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 + */ + function getSupportedPrivilegeSet() { + + return null; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAVACL/AbstractPrincipalCollection.php b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/AbstractPrincipalCollection.php new file mode 100644 index 00000000000..9d2026380c5 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/AbstractPrincipalCollection.php @@ -0,0 +1,181 @@ +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. + * + * @param array $principalInfo + * @return IPrincipal + */ + abstract function getChildForPrincipal(array $principalInfo); + + /** + * Returns the name of this collection. + * + * @return string + */ + function getName() { + + list(, $name) = URLUtil::splitPath($this->principalPrefix); + return $name; + + } + + /** + * Return the list of users + * + * @return array + */ + 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 + */ + 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 array $searchProperties + * @param string $test + * @return array + */ + function searchPrincipals(array $searchProperties, $test = 'allof') { + + $result = $this->principalBackend->searchPrincipals($this->principalPrefix, $searchProperties, $test); + $r = []; + + foreach ($result as $row) { + list(, $r[]) = URLUtil::splitPath($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 + */ + function findByUri($uri) { + + return $this->principalBackend->findByUri($uri, $this->principalPrefix); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Exception/AceConflict.php b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Exception/AceConflict.php new file mode 100644 index 00000000000..22450b4a681 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Exception/AceConflict.php @@ -0,0 +1,35 @@ +ownerDocument; + + $np = $doc->createElementNS('DAV:', 'd:no-ace-conflict'); + $errorNode->appendChild($np); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Exception/NeedPrivileges.php b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Exception/NeedPrivileges.php new file mode 100644 index 00000000000..5624fd22f23 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Exception/NeedPrivileges.php @@ -0,0 +1,82 @@ +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 + * + * @param DAV\Server $server + * @param \DOMElement $errorNode + * @return void + */ + 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/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Exception/NoAbstract.php b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Exception/NoAbstract.php new file mode 100644 index 00000000000..a2363b174bf --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Exception/NoAbstract.php @@ -0,0 +1,35 @@ +ownerDocument; + + $np = $doc->createElementNS('DAV:', 'd:no-abstract'); + $errorNode->appendChild($np); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Exception/NotRecognizedPrincipal.php b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Exception/NotRecognizedPrincipal.php new file mode 100644 index 00000000000..d7ae188ae7e --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Exception/NotRecognizedPrincipal.php @@ -0,0 +1,35 @@ +ownerDocument; + + $np = $doc->createElementNS('DAV:', 'd:recognized-principal'); + $errorNode->appendChild($np); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Exception/NotSupportedPrivilege.php b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Exception/NotSupportedPrivilege.php new file mode 100644 index 00000000000..73b81190dd2 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Exception/NotSupportedPrivilege.php @@ -0,0 +1,35 @@ +ownerDocument; + + $np = $doc->createElementNS('DAV:', 'd:not-supported-privilege'); + $errorNode->appendChild($np); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAVACL/FS/Collection.php b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/FS/Collection.php new file mode 100644 index 00000000000..b4fe7a1b0be --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/FS/Collection.php @@ -0,0 +1,111 @@ +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 + */ + 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 + */ + 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 + */ + function getACL() { + + return $this->acl; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAVACL/FS/File.php b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/FS/File.php new file mode 100644 index 00000000000..aaf2ae148a7 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/FS/File.php @@ -0,0 +1,80 @@ +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 + */ + 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 + */ + function getACL() { + + return $this->acl; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAVACL/FS/HomeCollection.php b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/FS/HomeCollection.php new file mode 100644 index 00000000000..201235e5a0f --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/FS/HomeCollection.php @@ -0,0 +1,128 @@ +storagePath = $storagePath; + + } + + /** + * Returns the name of the node. + * + * This is used to generate the url. + * + * @return string + */ + 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. + * + * @param array $principalInfo + * @return \Sabre\DAV\INode + */ + 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 + */ + function getACL() { + + return [ + [ + 'principal' => '{DAV:}authenticated', + 'privilege' => '{DAV:}read', + 'protected' => true, + ] + ]; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAVACL/IACL.php b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/IACL.php new file mode 100644 index 00000000000..f7a138665a5 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/IACL.php @@ -0,0 +1,74 @@ +getChild in the future. + * + * @param array $searchProperties + * @param string $test + * @return array + */ + 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 + */ + function findByUri($uri); + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Plugin.php b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Plugin.php new file mode 100644 index 00000000000..a2aa118d70b --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Plugin.php @@ -0,0 +1,1636 @@ + '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 + */ + function getFeatures() { + + return ['access-control', 'calendarserver-principal-property-search']; + + } + + /** + * Returns a list of available methods for a given url + * + * @param string $uri + * @return array + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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. + * + * @param array $acl + * @return void + */ + 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 + */ + 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 + */ + 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 + */ + function principalMatchesPrincipal($checkPrincipal, $currentPrincipal = null) { + + if (is_null($currentPrincipal)) { + $currentPrincipal = $this->getCurrentUserPrincipal(); + } + if ($currentPrincipal === $checkPrincipal) { + return true; + } + 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 + */ + 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 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 + */ + 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 + */ + function getCurrentUserPrivilegeSet($node) { + + if (is_string($node)) { + $node = $this->server->tree->getNodeForPath($node); + } + + $acl = $this->getACL($node); + + $collected = []; + + $isAuthenticated = $this->getCurrentUserPrincipal() !== null; + + 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 null|string + */ + 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 ommitted, 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. + */ + 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. + * + * @param DAV\Server $server + * @return void + */ + 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 + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return void + */ + 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 + * @return void + */ + 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 + * @return void + */ + 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 + * @param DAV\Locks\LockInfo $lock + * @TODO: not yet implemented + * @return void + */ + function beforeUnlock($uri, DAV\Locks\LockInfo $lock) { + + + } + + /** + * Triggered before properties are looked up in specific nodes. + * + * @param DAV\PropFind $propFind + * @param DAV\INode $node + * @TODO really should be broken into multiple methods, or even a class. + * @return bool + */ + 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 + * @param DAV\PropPatch $propPatch + * @return void + */ + 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 + * @return bool + */ + 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. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + 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 + * @param Xml\Request\PrincipalMatchReport $report + * @return void + */ + protected function principalMatchReport($path, Xml\Request\PrincipalMatchReport $report) { + + $depth = $this->server->getHTTPDepth(0); + if ($depth !== 0) { + throw new BadRequest('The principal-match report is only defined on Depth: 0'); + } + + $currentPrincipals = $this->getCurrentUserPrincipals(); + + $result = []; + + if ($report->type === Xml\Request\PrincipalMatchReport::SELF) { + + // Finding all principals under the request uri that match the + // current principal. + foreach ($currentPrincipals as $currentPrincipal) { + + if ($currentPrincipal === $path || strpos($currentPrincipal, $path . '/') === 0) { + $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 + * @return void + */ + 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 (count($childRequestedProperties) === 0) 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 + * @return void + */ + protected function principalSearchPropertySetReport($path, $report) { + + $httpDepth = $this->server->getHTTPDepth(0); + if ($httpDepth !== 0) { + 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 + * @param Xml\Request\PrincipalPropertySearchReport $report + * @return void + */ + protected function principalPropertySearchReport($path, Xml\Request\PrincipalPropertySearchReport $report) { + + if ($report->applyToPrincipalCollectionSet) { + $path = null; + } + if ($this->server->getHttpDepth('0') !== 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, $prefer['return'] === 'minimal')); + + } + + /** + * 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 + * @param Xml\Request\AclPrincipalPropSetReport $report + * @return void + */ + protected function aclPrincipalPropSetReport($path, Xml\Request\AclPrincipalPropSetReport $report) { + + if ($this->server->getHTTPDepth(0) !== 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 DAV\INode $node + * @param string $output + * @return bool + */ + 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 + */ + function getPluginInfo() { + + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds support for WebDAV ACL (rfc3744)', + 'link' => 'http://sabre.io/dav/acl/', + ]; + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Principal.php b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Principal.php new file mode 100644 index 00000000000..d7db9499946 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Principal.php @@ -0,0 +1,221 @@ +principalBackend = $principalBackend; + $this->principalProperties = $principalProperties; + + } + + /** + * Returns the full principal url + * + * @return string + */ + 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 + */ + 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 + */ + 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 + */ + 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. + * + * @param array $groupMembers + * @return void + */ + function setGroupMemberSet(array $groupMembers) { + + $this->principalBackend->setGroupMemberSet($this->principalProperties['uri'], $groupMembers); + + } + + /** + * Returns this principals name. + * + * @return string + */ + function getName() { + + $uri = $this->principalProperties['uri']; + list(, $name) = URLUtil::splitPath($uri); + return $name; + + } + + /** + * Returns the name of the user + * + * @return string + */ + 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 + */ + 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. + * + * @param DAV\PropPatch $propPatch + * @return void + */ + 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 + */ + function getOwner() { + + return $this->principalProperties['uri']; + + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAVACL/PrincipalBackend/AbstractBackend.php b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/PrincipalBackend/AbstractBackend.php new file mode 100644 index 00000000000..9bf9ba4453c --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/PrincipalBackend/AbstractBackend.php @@ -0,0 +1,53 @@ +searchPrincipals( + $principalPrefix, + ['{http://sabredav.org/ns}email-address' => substr($uri, 7)] + ); + + if ($result) { + return $result[0]; + } + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAVACL/PrincipalBackend/BackendInterface.php b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/PrincipalBackend/BackendInterface.php new file mode 100644 index 00000000000..40b6e33ead9 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/PrincipalBackend/BackendInterface.php @@ -0,0 +1,141 @@ + [ + 'dbField' => 'displayname', + ], + + /** + * This is the users' primary email-address. + */ + '{http://sabredav.org/ns}email-address' => [ + 'dbField' => 'email', + ], + ]; + + /** + * Sets up the backend. + * + * @param \PDO $pdo + */ + 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 actualy injected in a number of other properties. If + * you have an email address, use this property. + * + * @param string $prefixPath + * @return array + */ + 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) = URLUtil::splitPath($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 + */ + 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 + * @param DAV\PropPatch $propPatch + */ + 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 array $searchProperties + * @param string $test + * @return array + */ + function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') { + if (count($searchProperties) == 0) 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 .= (strcmp($test, "anyof") == 0 ? " 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) = URLUtil::splitPath($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 + */ + function findByUri($uri, $principalPrefix) { + $value = null; + $scheme = null; + list($scheme, $value) = explode(":", $uri, 2); + if (empty($value)) return null; + + $uri = null; + switch ($scheme){ + case "mailto": + $query = 'SELECT uri FROM ' . $this->tableName . ' WHERE lower(email)=lower(?)'; + $stmt = $this->pdo->prepare($query); + $stmt->execute([$value]); + + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + // Checking if the principal is in the prefix + list($rowPrefix) = URLUtil::splitPath($row['uri']); + if ($rowPrefix !== $principalPrefix) continue; + + $uri = $row['uri']; + break; //Stop on first match + } + break; + default: + //unsupported uri scheme + return null; + } + return $uri; + } + + /** + * Returns the list of members for a group-principal + * + * @param string $principal + * @return array + */ + 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 + */ + 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 + * @param array $members + * @return void + */ + 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 + * @param MkCol $mkCol + * @return void + */ + 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/htdocs/includes/sabre/sabre/dav/lib/DAVACL/PrincipalCollection.php b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/PrincipalCollection.php new file mode 100644 index 00000000000..ee5b88a904e --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/PrincipalCollection.php @@ -0,0 +1,98 @@ +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 + * @param MkCol $mkCol + * @throws InvalidResourceType + * @return void + */ + 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 + */ + function getACL() { + return [ + [ + 'principal' => '{DAV:}authenticated', + 'privilege' => '{DAV:}read', + 'protected' => true, + ], + ]; + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Property/Acl.php b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Property/Acl.php new file mode 100644 index 00000000000..0e1c30ccfea --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Property/Acl.php @@ -0,0 +1,277 @@ +privileges = $privileges; + $this->prefixBaseUrl = $prefixBaseUrl; + + } + + /** + * Returns the list of privileges for this property + * + * @return array + */ + 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. + * + * @param Writer $writer + * @return void + */ + 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. + * + * @param HtmlOutputHelper $html + * @return string + */ + 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. + * + * @param Reader $reader + * @return mixed + */ + 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 ($element['name'] !== '{DAV:}ace') { + 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 ($elem['name'] !== '{DAV:}privilege') { + continue; + } + + foreach ($elem['value'] as $priv) { + $privileges[] = [ + 'principal' => $principal, + 'protected' => $protected, + 'privilege' => $priv, + ]; + } + + } + + } + + return new self($privileges); + + } + + /** + * Serializes a single access control entry. + * + * @param Writer $writer + * @param array $ace + * @return void + */ + 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/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Property/AclRestrictions.php b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Property/AclRestrictions.php new file mode 100644 index 00000000000..8d5854c23c0 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Property/AclRestrictions.php @@ -0,0 +1,45 @@ +writeElement('{DAV:}grant-only'); + $writer->writeElement('{DAV:}no-invert'); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Property/CurrentUserPrivilegeSet.php b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Property/CurrentUserPrivilegeSet.php new file mode 100644 index 00000000000..74c09cee150 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Property/CurrentUserPrivilegeSet.php @@ -0,0 +1,159 @@ +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. + * + * @param Writer $writer + * @return void + */ + 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 + */ + function has($privilegeName) { + + return in_array($privilegeName, $this->privileges); + + } + + /** + * Returns the list of privileges. + * + * @return array + */ + 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. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + + $result = []; + + $tree = $reader->parseInnerTree(['{DAV:}privilege' => 'Sabre\\Xml\\Element\\Elements']); + foreach ($tree as $element) { + if ($element['name'] !== '{DAV:}privilege') { + 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. + * + * @param HtmlOutputHelper $html + * @return string + */ + function toHtml(HtmlOutputHelper $html) { + + return implode( + ', ', + array_map([$html, 'xmlName'], $this->getValue()) + ); + + } + + +} diff --git a/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Property/Principal.php b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Property/Principal.php new file mode 100644 index 00000000000..04d22165d27 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Property/Principal.php @@ -0,0 +1,196 @@ +type = $type; + if ($type === self::HREF && 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 + */ + 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. + * + * @param Writer $writer + * @return void + */ + 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. + * + * @param HtmlOutputHelper $html + * @return string + */ + 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'; + } + + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called staticly, 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. + * + * @param Reader $reader + * @return mixed + */ + 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/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php new file mode 100644 index 00000000000..b963cc8c3ba --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php @@ -0,0 +1,160 @@ +privileges = $privileges; + + } + + /** + * Returns the privilege value. + * + * @return array + */ + 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. + * + * @param Writer $writer + * @return void + */ + 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. + * + * @param HtmlOutputHelper $html + * @return string + */ + 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 Writer $writer + * @param string $privName + * @param array $privilege + * @return void + */ + 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/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Request/AclPrincipalPropSetReport.php b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Request/AclPrincipalPropSetReport.php new file mode 100644 index 00000000000..0aa2f29a559 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Request/AclPrincipalPropSetReport.php @@ -0,0 +1,67 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + 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/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Request/ExpandPropertyReport.php b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Request/ExpandPropertyReport.php new file mode 100644 index 00000000000..a9938ba5bf0 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Request/ExpandPropertyReport.php @@ -0,0 +1,103 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + 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 void + */ + private static function traverse($elems) { + + $result = []; + + foreach ($elems as $elem) { + + if ($elem['name'] !== '{DAV:}property') { + 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/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Request/PrincipalMatchReport.php b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Request/PrincipalMatchReport.php new file mode 100644 index 00000000000..1be15ab2da6 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Request/PrincipalMatchReport.php @@ -0,0 +1,107 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + 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/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Request/PrincipalPropertySearchReport.php b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Request/PrincipalPropertySearchReport.php new file mode 100644 index 00000000000..b0cf0e408ff --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Request/PrincipalPropertySearchReport.php @@ -0,0 +1,127 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + + $self = new self(); + + $foundSearchProp = false; + $self->test = 'allof'; + if ($reader->getAttribute('test') === 'anyof') { + $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/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Request/PrincipalSearchPropertySetReport.php b/htdocs/includes/sabre/sabre/dav/lib/DAVACL/Xml/Request/PrincipalSearchPropertySetReport.php new file mode 100644 index 00000000000..64d1f7f861e --- /dev/null +++ b/htdocs/includes/sabre/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. + * + * @param Reader $reader + * @return mixed + */ + 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/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/AbstractPDOTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/AbstractPDOTest.php new file mode 100644 index 00000000000..406dbe0e8e2 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/AbstractPDOTest.php @@ -0,0 +1,1431 @@ +dropTables([ + 'calendarobjects', + 'calendars', + 'calendarinstances', + 'calendarchanges', + 'calendarsubscriptions', + 'schedulingobjects', + ]); + $this->createSchema('calendars'); + + $this->pdo = $this->getDb(); + + } + + function testConstruct() { + + $backend = new PDO($this->pdo); + $this->assertTrue($backend instanceof PDO); + + } + + /** + * @depends testConstruct + */ + function testGetCalendarsForUserNoCalendars() { + + $backend = new PDO($this->pdo); + $calendars = $backend->getCalendarsForUser('principals/user2'); + $this->assertEquals([], $calendars); + + } + + /** + * @depends testConstruct + */ + function testCreateCalendarAndFetch() { + + $backend = new PDO($this->pdo); + $returnedId = $backend->createCalendar('principals/user2', 'somerandomid', [ + '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => new CalDAV\Xml\Property\SupportedCalendarComponentSet(['VEVENT']), + '{DAV:}displayname' => 'Hello!', + '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp' => new CalDAV\Xml\Property\ScheduleCalendarTransp('transparent'), + ]); + $calendars = $backend->getCalendarsForUser('principals/user2'); + + $elementCheck = [ + 'uri' => 'somerandomid', + '{DAV:}displayname' => 'Hello!', + '{urn:ietf:params:xml:ns:caldav}calendar-description' => '', + '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp' => new CalDAV\Xml\Property\ScheduleCalendarTransp('transparent'), + 'share-access' => \Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER, + ]; + + $this->assertInternalType('array', $calendars); + $this->assertEquals(1, count($calendars)); + + foreach ($elementCheck as $name => $value) { + + $this->assertArrayHasKey($name, $calendars[0]); + $this->assertEquals($value, $calendars[0][$name]); + + } + + } + + /** + * @depends testConstruct + */ + function testUpdateCalendarAndFetch() { + + $backend = new PDO($this->pdo); + + //Creating a new calendar + $newId = $backend->createCalendar('principals/user2', 'somerandomid', []); + + $propPatch = new PropPatch([ + '{DAV:}displayname' => 'myCalendar', + '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp' => new CalDAV\Xml\Property\ScheduleCalendarTransp('transparent'), + ]); + + // Updating the calendar + $backend->updateCalendar($newId, $propPatch); + $result = $propPatch->commit(); + + // Verifying the result of the update + $this->assertTrue($result); + + // Fetching all calendars from this user + $calendars = $backend->getCalendarsForUser('principals/user2'); + + // Checking if all the information is still correct + $elementCheck = [ + 'id' => $newId, + 'uri' => 'somerandomid', + '{DAV:}displayname' => 'myCalendar', + '{urn:ietf:params:xml:ns:caldav}calendar-description' => '', + '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => '', + '{http://calendarserver.org/ns/}getctag' => 'http://sabre.io/ns/sync/2', + '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp' => new CalDAV\Xml\Property\ScheduleCalendarTransp('transparent'), + ]; + + $this->assertInternalType('array', $calendars); + $this->assertEquals(1, count($calendars)); + + foreach ($elementCheck as $name => $value) { + + $this->assertArrayHasKey($name, $calendars[0]); + $this->assertEquals($value, $calendars[0][$name]); + + } + + } + + /** + * @depends testConstruct + * @expectedException \InvalidArgumentException + */ + function testUpdateCalendarBadId() { + + $backend = new PDO($this->pdo); + + //Creating a new calendar + $newId = $backend->createCalendar('principals/user2', 'somerandomid', []); + + $propPatch = new PropPatch([ + '{DAV:}displayname' => 'myCalendar', + '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp' => new CalDAV\Xml\Property\ScheduleCalendarTransp('transparent'), + ]); + + // Updating the calendar + $backend->updateCalendar('raaaa', $propPatch); + + } + + /** + * @depends testUpdateCalendarAndFetch + */ + function testUpdateCalendarUnknownProperty() { + + $backend = new PDO($this->pdo); + + //Creating a new calendar + $newId = $backend->createCalendar('principals/user2', 'somerandomid', []); + + $propPatch = new PropPatch([ + '{DAV:}displayname' => 'myCalendar', + '{DAV:}yourmom' => 'wittycomment', + ]); + + // Updating the calendar + $backend->updateCalendar($newId, $propPatch); + $propPatch->commit(); + + // Verifying the result of the update + $this->assertEquals([ + '{DAV:}yourmom' => 403, + '{DAV:}displayname' => 424, + ], $propPatch->getResult()); + + } + + /** + * @depends testCreateCalendarAndFetch + */ + function testDeleteCalendar() { + + $backend = new PDO($this->pdo); + $returnedId = $backend->createCalendar('principals/user2', 'somerandomid', [ + '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => new CalDAV\Xml\Property\SupportedCalendarComponentSet(['VEVENT']), + '{DAV:}displayname' => 'Hello!', + ]); + + $backend->deleteCalendar($returnedId); + + $calendars = $backend->getCalendarsForUser('principals/user2'); + $this->assertEquals([], $calendars); + + } + + /** + * @depends testCreateCalendarAndFetch + * @expectedException \InvalidArgumentException + */ + function testDeleteCalendarBadID() { + + $backend = new PDO($this->pdo); + $returnedId = $backend->createCalendar('principals/user2', 'somerandomid', [ + '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => new CalDAV\Xml\Property\SupportedCalendarComponentSet(['VEVENT']), + '{DAV:}displayname' => 'Hello!', + ]); + + $backend->deleteCalendar('bad-id'); + + } + + /** + * @depends testCreateCalendarAndFetch + * @expectedException \Sabre\DAV\Exception + */ + function testCreateCalendarIncorrectComponentSet() {; + + $backend = new PDO($this->pdo); + + //Creating a new calendar + $newId = $backend->createCalendar('principals/user2', 'somerandomid', [ + '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => 'blabla', + ]); + + } + + function testCreateCalendarObject() { + + $backend = new PDO($this->pdo); + $returnedId = $backend->createCalendar('principals/user2', 'somerandomid', []); + + $object = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART;VALUE=DATE:20120101\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"; + + $backend->createCalendarObject($returnedId, 'random-id', $object); + + $result = $this->pdo->query('SELECT etag, size, calendardata, firstoccurence, lastoccurence, componenttype FROM calendarobjects WHERE uri = \'random-id\''); + + $row = $result->fetch(\PDO::FETCH_ASSOC); + if (is_resource($row['calendardata'])) { + $row['calendardata'] = stream_get_contents($row['calendardata']); + } + + $this->assertEquals([ + 'etag' => md5($object), + 'size' => strlen($object), + 'calendardata' => $object, + 'firstoccurence' => strtotime('20120101'), + 'lastoccurence' => strtotime('20120101') + (3600 * 24), + 'componenttype' => 'VEVENT', + ], $row); + + } + function testGetMultipleObjects() { + + $backend = new PDO($this->pdo); + $returnedId = $backend->createCalendar('principals/user2', 'somerandomid', []); + + $object = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART;VALUE=DATE:20120101\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"; + + $backend->createCalendarObject($returnedId, 'id-1', $object); + $backend->createCalendarObject($returnedId, 'id-2', $object); + + $check = [ + [ + 'id' => 1, + 'etag' => '"' . md5($object) . '"', + 'uri' => 'id-1', + 'size' => strlen($object), + 'calendardata' => $object, + 'lastmodified' => null, + ], + [ + 'id' => 2, + 'etag' => '"' . md5($object) . '"', + 'uri' => 'id-2', + 'size' => strlen($object), + 'calendardata' => $object, + 'lastmodified' => null, + ], + ]; + + $result = $backend->getMultipleCalendarObjects($returnedId, ['id-1', 'id-2']); + + foreach ($check as $index => $props) { + + foreach ($props as $key => $expected) { + + $actual = $result[$index][$key]; + + switch ($key) { + case 'lastmodified' : + $this->assertInternalType('int', $actual); + break; + case 'calendardata' : + if (is_resource($actual)) { + $actual = stream_get_contents($actual); + } + // no break intentional + default : + $this->assertEquals($expected, $actual); + + } + + } + + } + + } + + /** + * @depends testGetMultipleObjects + * @expectedException \InvalidArgumentException + */ + function testGetMultipleObjectsBadId() { + + $backend = new PDO($this->pdo); + $backend->getMultipleCalendarObjects('bad-id', ['foo-bar']); + + } + + /** + * @expectedException Sabre\DAV\Exception\BadRequest + * @depends testCreateCalendarObject + */ + function testCreateCalendarObjectNoComponent() { + + $backend = new PDO($this->pdo); + $returnedId = $backend->createCalendar('principals/user2', 'somerandomid', []); + + $object = "BEGIN:VCALENDAR\r\nBEGIN:VTIMEZONE\r\nEND:VTIMEZONE\r\nEND:VCALENDAR\r\n"; + + $backend->createCalendarObject($returnedId, 'random-id', $object); + + } + + /** + * @depends testCreateCalendarObject + */ + function testCreateCalendarObjectDuration() { + + $backend = new PDO($this->pdo); + $returnedId = $backend->createCalendar('principals/user2', 'somerandomid', []); + + $object = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART;VALUE=DATE:20120101\r\nDURATION:P2D\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"; + + $backend->createCalendarObject($returnedId, 'random-id', $object); + + $result = $this->pdo->query('SELECT etag, size, calendardata, firstoccurence, lastoccurence, componenttype FROM calendarobjects WHERE uri = \'random-id\''); + + $row = $result->fetch(\PDO::FETCH_ASSOC); + if (is_resource($row['calendardata'])) { + $row['calendardata'] = stream_get_contents($row['calendardata']); + } + + $this->assertEquals([ + 'etag' => md5($object), + 'size' => strlen($object), + 'calendardata' => $object, + 'firstoccurence' => strtotime('20120101'), + 'lastoccurence' => strtotime('20120101') + (3600 * 48), + 'componenttype' => 'VEVENT', + ], $row); + + } + + /** + * @depends testCreateCalendarObject + * @expectedException \InvalidArgumentException + */ + function testCreateCalendarObjectBadId() { + + $backend = new PDO($this->pdo); + $returnedId = $backend->createCalendar('principals/user2', 'somerandomid', []); + + $object = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART;VALUE=DATE:20120101\r\nDURATION:P2D\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"; + + $backend->createCalendarObject('bad-id', 'random-id', $object); + + } + + + /** + * @depends testCreateCalendarObject + */ + function testCreateCalendarObjectNoDTEND() { + + $backend = new PDO($this->pdo); + $returnedId = $backend->createCalendar('principals/user2', 'somerandomid', []); + + $object = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART;VALUE=DATE-TIME:20120101T100000Z\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"; + + $backend->createCalendarObject($returnedId, 'random-id', $object); + + $result = $this->pdo->query('SELECT etag, size, calendardata, firstoccurence, lastoccurence, componenttype FROM calendarobjects WHERE uri = \'random-id\''); + $row = $result->fetch(\PDO::FETCH_ASSOC); + if (is_resource($row['calendardata'])) { + $row['calendardata'] = stream_get_contents($row['calendardata']); + } + + $this->assertEquals([ + 'etag' => md5($object), + 'size' => strlen($object), + 'calendardata' => $object, + 'firstoccurence' => strtotime('2012-01-01 10:00:00'), + 'lastoccurence' => strtotime('2012-01-01 10:00:00'), + 'componenttype' => 'VEVENT', + ], $row); + + } + + /** + * @depends testCreateCalendarObject + */ + function testCreateCalendarObjectWithDTEND() { + + $backend = new PDO($this->pdo); + $returnedId = $backend->createCalendar('principals/user2', 'somerandomid', []); + + $object = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART;VALUE=DATE-TIME:20120101T100000Z\r\nDTEND:20120101T110000Z\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"; + + $backend->createCalendarObject($returnedId, 'random-id', $object); + + $result = $this->pdo->query('SELECT etag, size, calendardata, firstoccurence, lastoccurence, componenttype FROM calendarobjects WHERE uri = \'random-id\''); + $row = $result->fetch(\PDO::FETCH_ASSOC); + if (is_resource($row['calendardata'])) { + $row['calendardata'] = stream_get_contents($row['calendardata']); + } + + $this->assertEquals([ + 'etag' => md5($object), + 'size' => strlen($object), + 'calendardata' => $object, + 'firstoccurence' => strtotime('2012-01-01 10:00:00'), + 'lastoccurence' => strtotime('2012-01-01 11:00:00'), + 'componenttype' => 'VEVENT', + ], $row); + + } + + /** + * @depends testCreateCalendarObject + */ + function testCreateCalendarObjectInfiniteRecurrence() { + + $backend = new PDO($this->pdo); + $returnedId = $backend->createCalendar('principals/user2', 'somerandomid', []); + + $object = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART;VALUE=DATE-TIME:20120101T100000Z\r\nRRULE:FREQ=DAILY\r\nUID:foo\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"; + + $backend->createCalendarObject($returnedId, 'random-id', $object); + + $result = $this->pdo->query('SELECT etag, size, calendardata, firstoccurence, lastoccurence, componenttype FROM calendarobjects WHERE uri = \'random-id\''); + $row = $result->fetch(\PDO::FETCH_ASSOC); + if (is_resource($row['calendardata'])) { + $row['calendardata'] = stream_get_contents($row['calendardata']); + } + + $this->assertEquals([ + 'etag' => md5($object), + 'size' => strlen($object), + 'calendardata' => $object, + 'firstoccurence' => strtotime('2012-01-01 10:00:00'), + 'lastoccurence' => strtotime(PDO::MAX_DATE), + 'componenttype' => 'VEVENT', + ], $row); + + } + + /** + * @depends testCreateCalendarObject + */ + function testCreateCalendarObjectEndingRecurrence() { + + $backend = new PDO($this->pdo); + $returnedId = $backend->createCalendar('principals/user2', 'somerandomid', []); + + $object = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART;VALUE=DATE-TIME:20120101T100000Z\r\nDTEND;VALUE=DATE-TIME:20120101T110000Z\r\nUID:foo\r\nRRULE:FREQ=DAILY;COUNT=1000\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"; + + $backend->createCalendarObject($returnedId, 'random-id', $object); + + $result = $this->pdo->query('SELECT etag, size, calendardata, firstoccurence, lastoccurence, componenttype FROM calendarobjects WHERE uri = \'random-id\''); + $row = $result->fetch(\PDO::FETCH_ASSOC); + if (is_resource($row['calendardata'])) { + $row['calendardata'] = stream_get_contents($row['calendardata']); + } + + $this->assertEquals([ + 'etag' => md5($object), + 'size' => strlen($object), + 'calendardata' => $object, + 'firstoccurence' => strtotime('2012-01-01 10:00:00'), + 'lastoccurence' => strtotime('2012-01-01 11:00:00') + (3600 * 24 * 999), + 'componenttype' => 'VEVENT', + ], $row); + + } + + /** + * @depends testCreateCalendarObject + */ + function testCreateCalendarObjectTask() { + + $backend = new PDO($this->pdo); + $returnedId = $backend->createCalendar('principals/user2', 'somerandomid', []); + + $object = "BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nDUE;VALUE=DATE-TIME:20120101T100000Z\r\nEND:VTODO\r\nEND:VCALENDAR\r\n"; + + $backend->createCalendarObject($returnedId, 'random-id', $object); + + $result = $this->pdo->query('SELECT etag, size, calendardata, firstoccurence, lastoccurence, componenttype FROM calendarobjects WHERE uri = \'random-id\''); + $row = $result->fetch(\PDO::FETCH_ASSOC); + if (is_resource($row['calendardata'])) { + $row['calendardata'] = stream_get_contents($row['calendardata']); + } + + $this->assertEquals([ + 'etag' => md5($object), + 'size' => strlen($object), + 'calendardata' => $object, + 'firstoccurence' => null, + 'lastoccurence' => null, + 'componenttype' => 'VTODO', + ], $row); + + } + + /** + * @depends testCreateCalendarObject + */ + function testGetCalendarObjects() { + + $backend = new PDO($this->pdo); + $returnedId = $backend->createCalendar('principals/user2', 'somerandomid', []); + + $object = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART;VALUE=DATE:20120101\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"; + $backend->createCalendarObject($returnedId, 'random-id', $object); + + $data = $backend->getCalendarObjects($returnedId); + + $this->assertEquals(1, count($data)); + $data = $data[0]; + + $this->assertEquals('random-id', $data['uri']); + $this->assertEquals(strlen($object), $data['size']); + + } + + /** + * @depends testGetCalendarObjects + * @expectedException \InvalidArgumentException + */ + function testGetCalendarObjectsBadId() { + + $backend = new PDO($this->pdo); + $backend->getCalendarObjects('bad-id'); + + } + + /** + * @depends testGetCalendarObjects + * @expectedException \InvalidArgumentException + */ + function testGetCalendarObjectBadId() { + + $backend = new PDO($this->pdo); + $backend->getCalendarObject('bad-id', 'foo-bar'); + + } + + /** + * @depends testCreateCalendarObject + */ + function testGetCalendarObjectByUID() { + + $backend = new PDO($this->pdo); + $returnedId = $backend->createCalendar('principals/user2', 'somerandomid', []); + + $object = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nUID:foo\r\nDTSTART;VALUE=DATE:20120101\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"; + $backend->createCalendarObject($returnedId, 'random-id', $object); + + $this->assertNull( + $backend->getCalendarObjectByUID('principals/user2', 'bar') + ); + $this->assertEquals( + 'somerandomid/random-id', + $backend->getCalendarObjectByUID('principals/user2', 'foo') + ); + + } + + /** + * @depends testCreateCalendarObject + */ + function testUpdateCalendarObject() { + + $backend = new PDO($this->pdo); + $returnedId = $backend->createCalendar('principals/user2', 'somerandomid', []); + + $object = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART;VALUE=DATE:20120101\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"; + $object2 = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART;VALUE=DATE:20130101\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"; + $backend->createCalendarObject($returnedId, 'random-id', $object); + $backend->updateCalendarObject($returnedId, 'random-id', $object2); + + $data = $backend->getCalendarObject($returnedId, 'random-id'); + + if (is_resource($data['calendardata'])) { + $data['calendardata'] = stream_get_contents($data['calendardata']); + } + + $this->assertEquals($object2, $data['calendardata']); + $this->assertEquals('random-id', $data['uri']); + + + } + + /** + * @depends testUpdateCalendarObject + * @expectedException \InvalidArgumentException + */ + function testUpdateCalendarObjectBadId() { + + $backend = new PDO($this->pdo); + $backend->updateCalendarObject('bad-id', 'object-id', 'objectdata'); + + } + + /** + * @depends testCreateCalendarObject + */ + function testDeleteCalendarObject() { + + $backend = new PDO($this->pdo); + $returnedId = $backend->createCalendar('principals/user2', 'somerandomid', []); + + $object = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART;VALUE=DATE:20120101\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"; + $backend->createCalendarObject($returnedId, 'random-id', $object); + $backend->deleteCalendarObject($returnedId, 'random-id'); + + $data = $backend->getCalendarObject($returnedId, 'random-id'); + $this->assertNull($data); + + } + + /** + * @depends testDeleteCalendarObject + * @expectedException \InvalidArgumentException + */ + function testDeleteCalendarObjectBadId() { + + $backend = new PDO($this->pdo); + $returnedId = $backend->createCalendar('principals/user2', 'somerandomid', []); + + $object = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART;VALUE=DATE:20120101\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"; + $backend->createCalendarObject($returnedId, 'random-id', $object); + $backend->deleteCalendarObject('bad-id', 'random-id'); + + } + + function testCalendarQueryNoResult() { + + $abstract = new PDO($this->pdo); + $filters = [ + 'name' => 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => 'VJOURNAL', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ], + ], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]; + + $this->assertEquals([ + ], $abstract->calendarQuery([1, 1], $filters)); + + } + + /** + * @expectedException \InvalidArgumentException + * @depends testCalendarQueryNoResult + */ + function testCalendarQueryBadId() { + + $abstract = new PDO($this->pdo); + $filters = [ + 'name' => 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => 'VJOURNAL', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ], + ], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]; + + $abstract->calendarQuery('bad-id', $filters); + + } + + function testCalendarQueryTodo() { + + $backend = new PDO($this->pdo); + $backend->createCalendarObject([1, 1], "todo", "BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nEND:VTODO\r\nEND:VCALENDAR\r\n"); + $backend->createCalendarObject([1, 1], "event", "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20120101\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"); + + $filters = [ + 'name' => 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => 'VTODO', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ], + ], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]; + + $this->assertEquals([ + "todo", + ], $backend->calendarQuery([1, 1], $filters)); + + } + function testCalendarQueryTodoNotMatch() { + + $backend = new PDO($this->pdo); + $backend->createCalendarObject([1, 1], "todo", "BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nEND:VTODO\r\nEND:VCALENDAR\r\n"); + $backend->createCalendarObject([1, 1], "event", "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20120101\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"); + + $filters = [ + 'name' => 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => 'VTODO', + 'comp-filters' => [], + 'prop-filters' => [ + [ + 'name' => 'summary', + 'text-match' => null, + 'time-range' => null, + 'param-filters' => [], + 'is-not-defined' => false, + ], + ], + 'is-not-defined' => false, + 'time-range' => null, + ], + ], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]; + + $this->assertEquals([ + ], $backend->calendarQuery([1, 1], $filters)); + + } + + function testCalendarQueryNoFilter() { + + $backend = new PDO($this->pdo); + $backend->createCalendarObject([1, 1], "todo", "BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nEND:VTODO\r\nEND:VCALENDAR\r\n"); + $backend->createCalendarObject([1, 1], "event", "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20120101\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"); + + $filters = [ + 'name' => 'VCALENDAR', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]; + + $result = $backend->calendarQuery([1, 1], $filters); + $this->assertTrue(in_array('todo', $result)); + $this->assertTrue(in_array('event', $result)); + + } + + function testCalendarQueryTimeRange() { + + $backend = new PDO($this->pdo); + $backend->createCalendarObject([1, 1], "todo", "BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nEND:VTODO\r\nEND:VCALENDAR\r\n"); + $backend->createCalendarObject([1, 1], "event", "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART;VALUE=DATE:20120101\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"); + $backend->createCalendarObject([1, 1], "event2", "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART;VALUE=DATE:20120103\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"); + + $filters = [ + 'name' => 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => 'VEVENT', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => [ + 'start' => new \DateTime('20120103'), + 'end' => new \DateTime('20120104'), + ], + ], + ], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]; + + $this->assertEquals([ + "event2", + ], $backend->calendarQuery([1, 1], $filters)); + + } + function testCalendarQueryTimeRangeNoEnd() { + + $backend = new PDO($this->pdo); + $backend->createCalendarObject([1, 1], "todo", "BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nEND:VTODO\r\nEND:VCALENDAR\r\n"); + $backend->createCalendarObject([1, 1], "event", "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20120101\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"); + $backend->createCalendarObject([1, 1], "event2", "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20120103\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"); + + $filters = [ + 'name' => 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => 'VEVENT', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => [ + 'start' => new \DateTime('20120102'), + 'end' => null, + ], + ], + ], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]; + + $this->assertEquals([ + "event2", + ], $backend->calendarQuery([1, 1], $filters)); + + } + + function testGetChanges() { + + $backend = new PDO($this->pdo); + $id = $backend->createCalendar( + 'principals/user1', + 'bla', + [] + ); + $result = $backend->getChangesForCalendar($id, null, 1); + + $this->assertEquals([ + 'syncToken' => 1, + 'modified' => [], + 'deleted' => [], + 'added' => [], + ], $result); + + $currentToken = $result['syncToken']; + + $dummyTodo = "BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nEND:VTODO\r\nEND:VCALENDAR\r\n"; + + $backend->createCalendarObject($id, "todo1.ics", $dummyTodo); + $backend->createCalendarObject($id, "todo2.ics", $dummyTodo); + $backend->createCalendarObject($id, "todo3.ics", $dummyTodo); + $backend->updateCalendarObject($id, "todo1.ics", $dummyTodo); + $backend->deleteCalendarObject($id, "todo2.ics"); + + $result = $backend->getChangesForCalendar($id, $currentToken, 1); + + $this->assertEquals([ + 'syncToken' => 6, + 'modified' => ["todo1.ics"], + 'deleted' => ["todo2.ics"], + 'added' => ["todo3.ics"], + ], $result); + + $result = $backend->getChangesForCalendar($id, null, 1); + + $this->assertEquals([ + 'syncToken' => 6, + 'modified' => [], + 'deleted' => [], + 'added' => ["todo1.ics", "todo3.ics"], + ], $result); + } + + /** + * @depends testGetChanges + * @expectedException \InvalidArgumentException + */ + function testGetChangesBadId() { + + $backend = new PDO($this->pdo); + $id = $backend->createCalendar( + 'principals/user1', + 'bla', + [] + ); + $backend->getChangesForCalendar('bad-id', null, 1); + + } + + function testCreateSubscriptions() { + + $props = [ + '{http://calendarserver.org/ns/}source' => new \Sabre\DAV\Xml\Property\Href('http://example.org/cal.ics', false), + '{DAV:}displayname' => 'cal', + '{http://apple.com/ns/ical/}refreshrate' => 'P1W', + '{http://apple.com/ns/ical/}calendar-color' => '#FF00FFFF', + '{http://calendarserver.org/ns/}subscribed-strip-todos' => true, + //'{http://calendarserver.org/ns/}subscribed-strip-alarms' => true, + '{http://calendarserver.org/ns/}subscribed-strip-attachments' => true, + ]; + + $backend = new PDO($this->pdo); + $backend->createSubscription('principals/user1', 'sub1', $props); + + $subs = $backend->getSubscriptionsForUser('principals/user1'); + + $expected = $props; + $expected['id'] = 1; + $expected['uri'] = 'sub1'; + $expected['principaluri'] = 'principals/user1'; + + unset($expected['{http://calendarserver.org/ns/}source']); + $expected['source'] = 'http://example.org/cal.ics'; + + $this->assertEquals(1, count($subs)); + foreach ($expected as $k => $v) { + $this->assertEquals($subs[0][$k], $expected[$k]); + } + + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + */ + function testCreateSubscriptionFail() { + + $props = [ + ]; + + $backend = new PDO($this->pdo); + $backend->createSubscription('principals/user1', 'sub1', $props); + + } + + function testUpdateSubscriptions() { + + $props = [ + '{http://calendarserver.org/ns/}source' => new \Sabre\DAV\Xml\Property\Href('http://example.org/cal.ics', false), + '{DAV:}displayname' => 'cal', + '{http://apple.com/ns/ical/}refreshrate' => 'P1W', + '{http://apple.com/ns/ical/}calendar-color' => '#FF00FFFF', + '{http://calendarserver.org/ns/}subscribed-strip-todos' => true, + //'{http://calendarserver.org/ns/}subscribed-strip-alarms' => true, + '{http://calendarserver.org/ns/}subscribed-strip-attachments' => true, + ]; + + $backend = new PDO($this->pdo); + $backend->createSubscription('principals/user1', 'sub1', $props); + + $newProps = [ + '{DAV:}displayname' => 'new displayname', + '{http://calendarserver.org/ns/}source' => new \Sabre\DAV\Xml\Property\Href('http://example.org/cal2.ics', false), + ]; + + $propPatch = new DAV\PropPatch($newProps); + $backend->updateSubscription(1, $propPatch); + $result = $propPatch->commit(); + + $this->assertTrue($result); + + $subs = $backend->getSubscriptionsForUser('principals/user1'); + + $expected = array_merge($props, $newProps); + $expected['id'] = 1; + $expected['uri'] = 'sub1'; + $expected['principaluri'] = 'principals/user1'; + + unset($expected['{http://calendarserver.org/ns/}source']); + $expected['source'] = 'http://example.org/cal2.ics'; + + $this->assertEquals(1, count($subs)); + foreach ($expected as $k => $v) { + $this->assertEquals($subs[0][$k], $expected[$k]); + } + + } + + function testUpdateSubscriptionsFail() { + + $props = [ + '{http://calendarserver.org/ns/}source' => new \Sabre\DAV\Xml\Property\Href('http://example.org/cal.ics', false), + '{DAV:}displayname' => 'cal', + '{http://apple.com/ns/ical/}refreshrate' => 'P1W', + '{http://apple.com/ns/ical/}calendar-color' => '#FF00FFFF', + '{http://calendarserver.org/ns/}subscribed-strip-todos' => true, + //'{http://calendarserver.org/ns/}subscribed-strip-alarms' => true, + '{http://calendarserver.org/ns/}subscribed-strip-attachments' => true, + ]; + + $backend = new PDO($this->pdo); + $backend->createSubscription('principals/user1', 'sub1', $props); + + $propPatch = new DAV\PropPatch([ + '{DAV:}displayname' => 'new displayname', + '{http://calendarserver.org/ns/}source' => new \Sabre\DAV\Xml\Property\Href('http://example.org/cal2.ics', false), + '{DAV:}unknown' => 'foo', + ]); + + $backend->updateSubscription(1, $propPatch); + $propPatch->commit(); + + $this->assertEquals([ + '{DAV:}unknown' => 403, + '{DAV:}displayname' => 424, + '{http://calendarserver.org/ns/}source' => 424, + ], $propPatch->getResult()); + + } + + function testDeleteSubscriptions() { + + $props = [ + '{http://calendarserver.org/ns/}source' => new \Sabre\DAV\Xml\Property\Href('http://example.org/cal.ics', false), + '{DAV:}displayname' => 'cal', + '{http://apple.com/ns/ical/}refreshrate' => 'P1W', + '{http://apple.com/ns/ical/}calendar-color' => '#FF00FFFF', + '{http://calendarserver.org/ns/}subscribed-strip-todos' => true, + //'{http://calendarserver.org/ns/}subscribed-strip-alarms' => true, + '{http://calendarserver.org/ns/}subscribed-strip-attachments' => true, + ]; + + $backend = new PDO($this->pdo); + $backend->createSubscription('principals/user1', 'sub1', $props); + + $newProps = [ + '{DAV:}displayname' => 'new displayname', + '{http://calendarserver.org/ns/}source' => new \Sabre\DAV\Xml\Property\Href('http://example.org/cal2.ics', false), + ]; + + $backend->deleteSubscription(1); + + $subs = $backend->getSubscriptionsForUser('principals/user1'); + $this->assertEquals(0, count($subs)); + } + + function testSchedulingMethods() { + + $backend = new PDO($this->pdo); + + $calData = "BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n"; + + $backend->createSchedulingObject( + 'principals/user1', + 'schedule1.ics', + $calData + ); + + $expected = [ + 'calendardata' => $calData, + 'uri' => 'schedule1.ics', + 'etag' => '"' . md5($calData) . '"', + 'size' => strlen($calData) + ]; + + $result = $backend->getSchedulingObject('principals/user1', 'schedule1.ics'); + foreach ($expected as $k => $v) { + $this->assertArrayHasKey($k, $result); + if (is_resource($result[$k])) { + $result[$k] = stream_get_contents($result[$k]); + } + $this->assertEquals($v, $result[$k]); + } + + $results = $backend->getSchedulingObjects('principals/user1'); + + $this->assertEquals(1, count($results)); + $result = $results[0]; + foreach ($expected as $k => $v) { + if (is_resource($result[$k])) { + $result[$k] = stream_get_contents($result[$k]); + } + $this->assertEquals($v, $result[$k]); + } + + $backend->deleteSchedulingObject('principals/user1', 'schedule1.ics'); + $result = $backend->getSchedulingObject('principals/user1', 'schedule1.ics'); + + $this->assertNull($result); + + } + + function testGetInvites() { + + $backend = new PDO($this->pdo); + + // creating a new calendar + $backend->createCalendar('principals/user1', 'somerandomid', []); + $calendar = $backend->getCalendarsForUser('principals/user1')[0]; + + $result = $backend->getInvites($calendar['id']); + $expected = [ + new Sharee([ + 'href' => 'principals/user1', + 'principal' => 'principals/user1', + 'access' => \Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER, + 'inviteStatus' => \Sabre\DAV\Sharing\Plugin::INVITE_ACCEPTED, + ]) + ]; + + $this->assertEquals($expected, $result); + + } + + /** + * @depends testGetInvites + * @expectedException \InvalidArgumentException + */ + function testGetInvitesBadId() { + + $backend = new PDO($this->pdo); + + // creating a new calendar + $backend->createCalendar('principals/user1', 'somerandomid', []); + $calendar = $backend->getCalendarsForUser('principals/user1')[0]; + + $backend->getInvites('bad-id'); + + } + + /** + * @depends testCreateCalendarAndFetch + */ + function testUpdateInvites() { + + $backend = new PDO($this->pdo); + + // creating a new calendar + $backend->createCalendar('principals/user1', 'somerandomid', []); + $calendar = $backend->getCalendarsForUser('principals/user1')[0]; + + $ownerSharee = new Sharee([ + 'href' => 'principals/user1', + 'principal' => 'principals/user1', + 'access' => \Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER, + 'inviteStatus' => \Sabre\DAV\Sharing\Plugin::INVITE_ACCEPTED, + ]); + + // Add a new invite + $backend->updateInvites( + $calendar['id'], + [ + new Sharee([ + 'href' => 'mailto:user@example.org', + 'principal' => 'principals/user2', + 'access' => \Sabre\DAV\Sharing\Plugin::ACCESS_READ, + 'inviteStatus' => \Sabre\DAV\Sharing\Plugin::INVITE_ACCEPTED, + 'properties' => ['{DAV:}displayname' => 'User 2'], + ]) + ] + ); + + $result = $backend->getInvites($calendar['id']); + $expected = [ + $ownerSharee, + new Sharee([ + 'href' => 'mailto:user@example.org', + 'principal' => 'principals/user2', + 'access' => \Sabre\DAV\Sharing\Plugin::ACCESS_READ, + 'inviteStatus' => \Sabre\DAV\Sharing\Plugin::INVITE_ACCEPTED, + 'properties' => [ + '{DAV:}displayname' => 'User 2', + ], + ]) + ]; + $this->assertEquals($expected, $result); + + // Checking calendar_instances too + $expectedCalendar = [ + 'id' => [1,2], + 'principaluri' => 'principals/user2', + '{http://calendarserver.org/ns/}getctag' => 'http://sabre.io/ns/sync/1', + '{http://sabredav.org/ns}sync-token' => '1', + 'share-access' => \Sabre\DAV\Sharing\Plugin::ACCESS_READ, + 'read-only' => true, + 'share-resource-uri' => '/ns/share/1', + ]; + $calendars = $backend->getCalendarsForUser('principals/user2'); + + foreach ($expectedCalendar as $k => $v) { + $this->assertEquals( + $v, + $calendars[0][$k], + "Key " . $k . " in calendars array did not have the expected value." + ); + } + + + // Updating an invite + $backend->updateInvites( + $calendar['id'], + [ + new Sharee([ + 'href' => 'mailto:user@example.org', + 'principal' => 'principals/user2', + 'access' => \Sabre\DAV\Sharing\Plugin::ACCESS_READWRITE, + 'inviteStatus' => \Sabre\DAV\Sharing\Plugin::INVITE_ACCEPTED, + ]) + ] + ); + + $result = $backend->getInvites($calendar['id']); + $expected = [ + $ownerSharee, + new Sharee([ + 'href' => 'mailto:user@example.org', + 'principal' => 'principals/user2', + 'access' => \Sabre\DAV\Sharing\Plugin::ACCESS_READWRITE, + 'inviteStatus' => \Sabre\DAV\Sharing\Plugin::INVITE_ACCEPTED, + 'properties' => [ + '{DAV:}displayname' => 'User 2', + ], + ]) + ]; + $this->assertEquals($expected, $result); + + // Removing an invite + $backend->updateInvites( + $calendar['id'], + [ + new Sharee([ + 'href' => 'mailto:user@example.org', + 'access' => \Sabre\DAV\Sharing\Plugin::ACCESS_NOACCESS, + ]) + ] + ); + + $result = $backend->getInvites($calendar['id']); + $expected = [ + $ownerSharee + ]; + $this->assertEquals($expected, $result); + + // Preventing the owner share from being removed + $backend->updateInvites( + $calendar['id'], + [ + new Sharee([ + 'href' => 'principals/user2', + 'access' => \Sabre\DAV\Sharing\Plugin::ACCESS_NOACCESS, + ]) + ] + ); + + $result = $backend->getInvites($calendar['id']); + $expected = [ + new Sharee([ + 'href' => 'principals/user1', + 'principal' => 'principals/user1', + 'access' => \Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER, + 'inviteStatus' => \Sabre\DAV\Sharing\Plugin::INVITE_ACCEPTED, + ]), + ]; + $this->assertEquals($expected, $result); + + } + + /** + * @depends testUpdateInvites + * @expectedException \InvalidArgumentException + */ + function testUpdateInvitesBadId() { + + $backend = new PDO($this->pdo); + // Add a new invite + $backend->updateInvites( + 'bad-id', + [] + ); + + } + + /** + * @depends testUpdateInvites + */ + function testUpdateInvitesNoPrincipal() { + + $backend = new PDO($this->pdo); + + // creating a new calendar + $backend->createCalendar('principals/user1', 'somerandomid', []); + $calendar = $backend->getCalendarsForUser('principals/user1')[0]; + + $ownerSharee = new Sharee([ + 'href' => 'principals/user1', + 'principal' => 'principals/user1', + 'access' => \Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER, + 'inviteStatus' => \Sabre\DAV\Sharing\Plugin::INVITE_ACCEPTED, + ]); + + // Add a new invite + $backend->updateInvites( + $calendar['id'], + [ + new Sharee([ + 'href' => 'mailto:user@example.org', + 'principal' => null, + 'access' => \Sabre\DAV\Sharing\Plugin::ACCESS_READ, + 'inviteStatus' => \Sabre\DAV\Sharing\Plugin::INVITE_ACCEPTED, + 'properties' => ['{DAV:}displayname' => 'User 2'], + ]) + ] + ); + + $result = $backend->getInvites($calendar['id']); + $expected = [ + $ownerSharee, + new Sharee([ + 'href' => 'mailto:user@example.org', + 'principal' => null, + 'access' => \Sabre\DAV\Sharing\Plugin::ACCESS_READ, + 'inviteStatus' => \Sabre\DAV\Sharing\Plugin::INVITE_INVALID, + 'properties' => [ + '{DAV:}displayname' => 'User 2', + ], + ]) + ]; + $this->assertEquals($expected, $result, null, 0.0, 10, true); // Last argument is $canonicalize = true, which allows us to compare, ignoring the order, because it's different between MySQL and Sqlite. + + } + + /** + * @depends testUpdateInvites + */ + function testDeleteSharedCalendar() { + + $backend = new PDO($this->pdo); + + // creating a new calendar + $backend->createCalendar('principals/user1', 'somerandomid', []); + $calendar = $backend->getCalendarsForUser('principals/user1')[0]; + + $ownerSharee = new Sharee([ + 'href' => 'principals/user1', + 'principal' => 'principals/user1', + 'access' => \Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER, + 'inviteStatus' => \Sabre\DAV\Sharing\Plugin::INVITE_ACCEPTED, + ]); + + // Add a new invite + $backend->updateInvites( + $calendar['id'], + [ + new Sharee([ + 'href' => 'mailto:user@example.org', + 'principal' => 'principals/user2', + 'access' => \Sabre\DAV\Sharing\Plugin::ACCESS_READ, + 'inviteStatus' => \Sabre\DAV\Sharing\Plugin::INVITE_ACCEPTED, + 'properties' => ['{DAV:}displayname' => 'User 2'], + ]) + ] + ); + + $expectedCalendar = [ + 'id' => [1,2], + 'principaluri' => 'principals/user2', + '{http://calendarserver.org/ns/}getctag' => 'http://sabre.io/ns/sync/1', + '{http://sabredav.org/ns}sync-token' => '1', + 'share-access' => \Sabre\DAV\Sharing\Plugin::ACCESS_READ, + 'read-only' => true, + 'share-resource-uri' => '/ns/share/1', + ]; + $calendars = $backend->getCalendarsForUser('principals/user2'); + + foreach ($expectedCalendar as $k => $v) { + $this->assertEquals( + $v, + $calendars[0][$k], + "Key " . $k . " in calendars array did not have the expected value." + ); + } + + // Removing the shared calendar. + $backend->deleteCalendar($calendars[0]['id']); + + $this->assertEquals( + [], + $backend->getCalendarsForUser('principals/user2') + ); + + $result = $backend->getInvites($calendar['id']); + $expected = [ + new Sharee([ + 'href' => 'principals/user1', + 'principal' => 'principals/user1', + 'access' => \Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER, + 'inviteStatus' => \Sabre\DAV\Sharing\Plugin::INVITE_ACCEPTED, + ]), + ]; + $this->assertEquals($expected, $result); + + } + + /** + * @expectedException \Sabre\DAV\Exception\NotImplemented + */ + function testSetPublishStatus() { + + $backend = new PDO($this->pdo); + $backend->setPublishStatus([1, 1], true); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/AbstractTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/AbstractTest.php new file mode 100644 index 00000000000..7f642efc9ae --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/AbstractTest.php @@ -0,0 +1,178 @@ + 'anything']); + + $abstract->updateCalendar('randomid', $propPatch); + $result = $propPatch->commit(); + + $this->assertFalse($result); + + } + + function testCalendarQuery() { + + $abstract = new AbstractMock(); + $filters = [ + 'name' => 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => 'VEVENT', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ], + ], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]; + + $this->assertEquals([ + 'event1.ics', + ], $abstract->calendarQuery(1, $filters)); + + } + + function testGetCalendarObjectByUID() { + + $abstract = new AbstractMock(); + $this->assertNull( + $abstract->getCalendarObjectByUID('principal1', 'zim') + ); + $this->assertEquals( + 'cal1/event1.ics', + $abstract->getCalendarObjectByUID('principal1', 'foo') + ); + $this->assertNull( + $abstract->getCalendarObjectByUID('principal3', 'foo') + ); + $this->assertNull( + $abstract->getCalendarObjectByUID('principal1', 'shared') + ); + + } + + function testGetMultipleCalendarObjects() { + + $abstract = new AbstractMock(); + $result = $abstract->getMultipleCalendarObjects(1, [ + 'event1.ics', + 'task1.ics', + ]); + + $expected = [ + [ + 'id' => 1, + 'calendarid' => 1, + 'uri' => 'event1.ics', + 'calendardata' => "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nUID:foo\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", + ], + [ + 'id' => 2, + 'calendarid' => 1, + 'uri' => 'task1.ics', + 'calendardata' => "BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nEND:VTODO\r\nEND:VCALENDAR\r\n", + ], + ]; + + $this->assertEquals($expected, $result); + + + } + +} + +class AbstractMock extends AbstractBackend { + + function getCalendarsForUser($principalUri) { + + return [ + [ + 'id' => 1, + 'principaluri' => 'principal1', + 'uri' => 'cal1', + ], + [ + 'id' => 2, + 'principaluri' => 'principal1', + '{http://sabredav.org/ns}owner-principal' => 'principal2', + 'uri' => 'cal1', + ], + ]; + + } + function createCalendar($principalUri, $calendarUri, array $properties) { } + function deleteCalendar($calendarId) { } + function getCalendarObjects($calendarId) { + + switch ($calendarId) { + case 1: + return [ + [ + 'id' => 1, + 'calendarid' => 1, + 'uri' => 'event1.ics', + ], + [ + 'id' => 2, + 'calendarid' => 1, + 'uri' => 'task1.ics', + ], + ]; + case 2: + return [ + [ + 'id' => 3, + 'calendarid' => 2, + 'uri' => 'shared-event.ics', + ] + ]; + } + + } + + function getCalendarObject($calendarId, $objectUri) { + + switch ($objectUri) { + + case 'event1.ics' : + return [ + 'id' => 1, + 'calendarid' => 1, + 'uri' => 'event1.ics', + 'calendardata' => "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nUID:foo\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", + ]; + case 'task1.ics' : + return [ + 'id' => 2, + 'calendarid' => 1, + 'uri' => 'task1.ics', + 'calendardata' => "BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nEND:VTODO\r\nEND:VCALENDAR\r\n", + ]; + case 'shared-event.ics' : + return [ + 'id' => 3, + 'calendarid' => 2, + 'uri' => 'event1.ics', + 'calendardata' => "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nUID:shared\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", + ]; + + } + + } + function createCalendarObject($calendarId, $objectUri, $calendarData) { } + function updateCalendarObject($calendarId, $objectUri, $calendarData) { } + function deleteCalendarObject($calendarId, $objectUri) { } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/Mock.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/Mock.php new file mode 100644 index 00000000000..cc665cd8fa3 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/Mock.php @@ -0,0 +1,258 @@ +calendars = $calendars; + $this->calendarData = $calendarData; + + } + + /** + * 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, which the basename of the uri with which the calendar is + * accessed. + * * 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'. + * + * @param string $principalUri + * @return array + */ + function getCalendarsForUser($principalUri) { + + $r = []; + foreach ($this->calendars as $row) { + if ($row['principaluri'] == $principalUri) { + $r[] = $row; + } + } + + return $r; + + } + + /** + * 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. + * + * This function must return a server-wide unique id that can be used + * later to reference the calendar. + * + * @param string $principalUri + * @param string $calendarUri + * @param array $properties + * @return string|int + */ + function createCalendar($principalUri, $calendarUri, array $properties) { + + $id = DAV\UUIDUtil::getUUID(); + $this->calendars[] = array_merge([ + 'id' => $id, + 'principaluri' => $principalUri, + 'uri' => $calendarUri, + '{' . CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => new CalDAV\Xml\Property\SupportedCalendarComponentSet(['VEVENT', 'VTODO']), + ], $properties); + + return $id; + + } + + /** + * 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 + * @param \Sabre\DAV\PropPatch $propPatch + * @return void + */ + function updateCalendar($calendarId, \Sabre\DAV\PropPatch $propPatch) { + + $propPatch->handleRemaining(function($props) use ($calendarId) { + + foreach ($this->calendars as $k => $calendar) { + + if ($calendar['id'] === $calendarId) { + foreach ($props as $propName => $propValue) { + if (is_null($propValue)) { + unset($this->calendars[$k][$propName]); + } else { + $this->calendars[$k][$propName] = $propValue; + } + } + return true; + + } + + } + + }); + + } + + /** + * Delete a calendar and all it's objects + * + * @param string $calendarId + * @return void + */ + function deleteCalendar($calendarId) { + + foreach ($this->calendars as $k => $calendar) { + if ($calendar['id'] === $calendarId) { + unset($this->calendars[$k]); + } + } + + } + + /** + * Returns all calendar objects within a calendar object. + * + * Every item contains an array with the following keys: + * * id - unique identifier which will be used for subsequent updates + * * calendardata - The iCalendar-compatible calendar data + * * uri - a unique key which will be used to construct the uri. This can be any arbitrary string. + * * lastmodified - a timestamp of the last modification time + * * etag - An arbitrary string, surrounded by double-quotes. (e.g.: + * ' "abcdef"') + * * calendarid - The calendarid as it was passed to this function. + * + * 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. + * + * @param string $calendarId + * @return array + */ + function getCalendarObjects($calendarId) { + + if (!isset($this->calendarData[$calendarId])) + return []; + + $objects = $this->calendarData[$calendarId]; + + foreach ($objects as $uri => &$object) { + $object['calendarid'] = $calendarId; + $object['uri'] = $uri; + $object['lastmodified'] = null; + } + return $objects; + + } + + /** + * 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 + */ + function getCalendarObject($calendarId, $objectUri) { + + if (!isset($this->calendarData[$calendarId][$objectUri])) { + return null; + } + $object = $this->calendarData[$calendarId][$objectUri]; + $object['calendarid'] = $calendarId; + $object['uri'] = $objectUri; + $object['lastmodified'] = null; + return $object; + + } + + /** + * Creates a new calendar object. + * + * @param string $calendarId + * @param string $objectUri + * @param string $calendarData + * @return void + */ + function createCalendarObject($calendarId, $objectUri, $calendarData) { + + $this->calendarData[$calendarId][$objectUri] = [ + 'calendardata' => $calendarData, + 'calendarid' => $calendarId, + 'uri' => $objectUri, + ]; + return '"' . md5($calendarData) . '"'; + + } + + /** + * Updates an existing calendarobject, based on it's uri. + * + * @param string $calendarId + * @param string $objectUri + * @param string $calendarData + * @return void + */ + function updateCalendarObject($calendarId, $objectUri, $calendarData) { + + $this->calendarData[$calendarId][$objectUri] = [ + 'calendardata' => $calendarData, + 'calendarid' => $calendarId, + 'uri' => $objectUri, + ]; + return '"' . md5($calendarData) . '"'; + + } + + /** + * Deletes an existing calendar object. + * + * @param string $calendarId + * @param string $objectUri + * @return void + */ + function deleteCalendarObject($calendarId, $objectUri) { + + unset($this->calendarData[$calendarId][$objectUri]); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/MockScheduling.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/MockScheduling.php new file mode 100644 index 00000000000..3ac22f474f6 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/MockScheduling.php @@ -0,0 +1,91 @@ +schedulingObjects[$principalUri][$objectUri])) { + return $this->schedulingObjects[$principalUri][$objectUri]; + } + + } + + /** + * 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 + */ + function getSchedulingObjects($principalUri) { + + if (isset($this->schedulingObjects[$principalUri])) { + return array_values($this->schedulingObjects[$principalUri]); + } + return []; + + } + + /** + * Deletes a scheduling object + * + * @param string $principalUri + * @param string $objectUri + * @return void + */ + function deleteSchedulingObject($principalUri, $objectUri) { + + if (isset($this->schedulingObjects[$principalUri][$objectUri])) { + unset($this->schedulingObjects[$principalUri][$objectUri]); + } + + } + + /** + * Creates a new scheduling object. This should land in a users' inbox. + * + * @param string $principalUri + * @param string $objectUri + * @param string $objectData; + * @return void + */ + function createSchedulingObject($principalUri, $objectUri, $objectData) { + + if (!isset($this->schedulingObjects[$principalUri])) { + $this->schedulingObjects[$principalUri] = []; + } + $this->schedulingObjects[$principalUri][$objectUri] = [ + 'uri' => $objectUri, + 'calendardata' => $objectData, + 'lastmodified' => null, + 'etag' => '"' . md5($objectData) . '"', + 'size' => strlen($objectData) + ]; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/MockSharing.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/MockSharing.php new file mode 100644 index 00000000000..eaf52e32f4a --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/MockSharing.php @@ -0,0 +1,204 @@ +notifications = $notifications; + + } + + /** + * 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, which the basename of the uri with which the calendar is + * accessed. + * * 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'. + * + * @param string $principalUri + * @return array + */ + function getCalendarsForUser($principalUri) { + + $calendars = parent::getCalendarsForUser($principalUri); + foreach ($calendars as $k => $calendar) { + + if (isset($calendar['share-access'])) { + continue; + } + if (!empty($this->shares[$calendar['id']])) { + $calendar['share-access'] = DAV\Sharing\Plugin::ACCESS_SHAREDOWNER; + } else { + $calendar['share-access'] = DAV\Sharing\Plugin::ACCESS_NOTSHARED; + } + $calendars[$k] = $calendar; + + } + return $calendars; + + } + + /** + * Returns a list of notifications for a given principal url. + * + * The returned array should only consist of implementations of + * Sabre\CalDAV\Notifications\INotificationType. + * + * @param string $principalUri + * @return array + */ + function getNotificationsForPrincipal($principalUri) { + + if (isset($this->notifications[$principalUri])) { + return $this->notifications[$principalUri]; + } + return []; + + } + + /** + * This deletes a specific notifcation. + * + * This may be called by a client once it deems a notification handled. + * + * @param string $principalUri + * @param NotificationInterface $notification + * @return void + */ + function deleteNotification($principalUri, NotificationInterface $notification) { + + foreach ($this->notifications[$principalUri] as $key => $value) { + if ($notification === $value) { + unset($this->notifications[$principalUri][$key]); + } + } + + } + + /** + * Updates the list of shares. + * + * @param mixed $calendarId + * @param \Sabre\DAV\Xml\Element\Sharee[] $sharees + * @return void + */ + function updateInvites($calendarId, array $sharees) { + + if (!isset($this->shares[$calendarId])) { + $this->shares[$calendarId] = []; + } + + foreach ($sharees as $sharee) { + + $existingKey = null; + foreach ($this->shares[$calendarId] as $k => $existingSharee) { + if ($sharee->href === $existingSharee->href) { + $existingKey = $k; + } + } + // Just making sure we're not affecting an existing copy. + $sharee = clone $sharee; + $sharee->inviteStatus = DAV\Sharing\Plugin::INVITE_NORESPONSE; + + if ($sharee->access === DAV\Sharing\Plugin::ACCESS_NOACCESS) { + // It's a removal + unset($this->shares[$calendarId][$existingKey]); + } elseif ($existingKey) { + // It's an update + $this->shares[$calendarId][$existingKey] = $sharee; + } else { + // It's an addition + $this->shares[$calendarId][] = $sharee; + } + } + + // Re-numbering keys + $this->shares[$calendarId] = array_values($this->shares[$calendarId]); + + } + + /** + * Returns the list of people whom this 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[] + */ + function getInvites($calendarId) { + + if (!isset($this->shares[$calendarId])) { + return []; + } + + return $this->shares[$calendarId]; + + } + + /** + * This method is called when a user replied to a request to share. + * + * @param string href The sharee who is replying (often a mailto: address) + * @param int status One of the \Sabre\DAV\Sharing\Plugin::INVITE_* 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 void + */ + function shareReply($href, $status, $calendarUri, $inReplyTo, $summary = null) { + + // This operation basically doesn't do anything yet + if ($status === DAV\Sharing\Plugin::INVITE_ACCEPTED) { + return 'calendars/blabla/calendar'; + } + + } + + /** + * Publishes a calendar + * + * @param mixed $calendarId + * @param bool $value + * @return void + */ + function setPublishStatus($calendarId, $value) { + + foreach ($this->calendars as $k => $cal) { + if ($cal['id'] === $calendarId) { + if (!$value) { + unset($cal['{http://calendarserver.org/ns/}publish-url']); + } else { + $cal['{http://calendarserver.org/ns/}publish-url'] = 'http://example.org/public/ ' . $calendarId . '.ics'; + } + return; + } + } + + throw new DAV\Exception('Calendar with id "' . $calendarId . '" not found'); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/MockSubscriptionSupport.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/MockSubscriptionSupport.php new file mode 100644 index 00000000000..adf9c8a17d6 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/MockSubscriptionSupport.php @@ -0,0 +1,156 @@ +subs[$principalUri])) { + return $this->subs[$principalUri]; + } + return []; + + } + + /** + * 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 + * @param array $properties + * @return mixed + */ + function createSubscription($principalUri, $uri, array $properties) { + + $properties['uri'] = $uri; + $properties['principaluri'] = $principalUri; + $properties['source'] = $properties['{http://calendarserver.org/ns/}source']->getHref(); + + if (!isset($this->subs[$principalUri])) { + $this->subs[$principalUri] = []; + } + + $id = [$principalUri, count($this->subs[$principalUri]) + 1]; + + $properties['id'] = $id; + + $this->subs[$principalUri][] = array_merge($properties, [ + 'id' => $id, + ]); + + return $id; + + } + + /** + * 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 + * @param \Sabre\DAV\PropPatch $propPatch + * @return void + */ + function updateSubscription($subscriptionId, DAV\PropPatch $propPatch) { + + $found = null; + foreach ($this->subs[$subscriptionId[0]] as &$sub) { + + if ($sub['id'][1] === $subscriptionId[1]) { + $found = & $sub; + break; + } + + } + + if (!$found) return; + + $propPatch->handleRemaining(function($mutations) use (&$found) { + foreach ($mutations as $k => $v) { + $found[$k] = $v; + } + return true; + }); + + } + + /** + * Deletes a subscription + * + * @param mixed $subscriptionId + * @return void + */ + function deleteSubscription($subscriptionId) { + + foreach ($this->subs[$subscriptionId[0]] as $index => $sub) { + + if ($sub['id'][1] === $subscriptionId[1]) { + unset($this->subs[$subscriptionId[0]][$index]); + return true; + } + + } + + return false; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/PDOMySQLTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/PDOMySQLTest.php new file mode 100644 index 00000000000..e068ff1e78f --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Backend/PDOMySQLTest.php @@ -0,0 +1,9 @@ +markTestSkipped('SQLite driver is not available'); + + if (file_exists(SABRE_TEMPDIR . '/testdb.sqlite')) + unlink(SABRE_TEMPDIR . '/testdb.sqlite'); + + $pdo = new \PDO('sqlite:' . SABRE_TEMPDIR . '/testdb.sqlite'); + $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + + $pdo->exec(<<exec(<<pdo = $pdo; + + } + + function testConstruct() { + + $backend = new SimplePDO($this->pdo); + $this->assertTrue($backend instanceof SimplePDO); + + } + + /** + * @depends testConstruct + */ + function testGetCalendarsForUserNoCalendars() { + + $backend = new SimplePDO($this->pdo); + $calendars = $backend->getCalendarsForUser('principals/user2'); + $this->assertEquals([], $calendars); + + } + + /** + * @depends testConstruct + */ + function testCreateCalendarAndFetch() { + + $backend = new SimplePDO($this->pdo); + $returnedId = $backend->createCalendar('principals/user2', 'somerandomid', [ + '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => new CalDAV\Xml\Property\SupportedCalendarComponentSet(['VEVENT']), + '{DAV:}displayname' => 'Hello!', + '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp' => new CalDAV\Xml\Property\ScheduleCalendarTransp('transparent'), + ]); + $calendars = $backend->getCalendarsForUser('principals/user2'); + + $elementCheck = [ + 'uri' => 'somerandomid', + ]; + + $this->assertInternalType('array', $calendars); + $this->assertEquals(1, count($calendars)); + + foreach ($elementCheck as $name => $value) { + + $this->assertArrayHasKey($name, $calendars[0]); + $this->assertEquals($value, $calendars[0][$name]); + + } + + } + + /** + * @depends testConstruct + */ + function testUpdateCalendarAndFetch() { + + $backend = new SimplePDO($this->pdo); + + //Creating a new calendar + $newId = $backend->createCalendar('principals/user2', 'somerandomid', []); + + $propPatch = new PropPatch([ + '{DAV:}displayname' => 'myCalendar', + '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp' => new CalDAV\Xml\Property\ScheduleCalendarTransp('transparent'), + ]); + + // Updating the calendar + $backend->updateCalendar($newId, $propPatch); + $result = $propPatch->commit(); + + // Verifying the result of the update + $this->assertFalse($result); + + } + + /** + * @depends testCreateCalendarAndFetch + */ + function testDeleteCalendar() { + + $backend = new SimplePDO($this->pdo); + $returnedId = $backend->createCalendar('principals/user2', 'somerandomid', [ + '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => new CalDAV\Xml\Property\SupportedCalendarComponentSet(['VEVENT']), + '{DAV:}displayname' => 'Hello!', + ]); + + $backend->deleteCalendar($returnedId); + + $calendars = $backend->getCalendarsForUser('principals/user2'); + $this->assertEquals([], $calendars); + + } + + function testCreateCalendarObject() { + + $backend = new SimplePDO($this->pdo); + $returnedId = $backend->createCalendar('principals/user2', 'somerandomid', []); + + $object = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART;VALUE=DATE:20120101\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"; + + $backend->createCalendarObject($returnedId, 'random-id', $object); + + $result = $this->pdo->query('SELECT calendardata FROM simple_calendarobjects WHERE uri = "random-id"'); + $this->assertEquals([ + 'calendardata' => $object, + ], $result->fetch(\PDO::FETCH_ASSOC)); + + } + function testGetMultipleObjects() { + + $backend = new SimplePDO($this->pdo); + $returnedId = $backend->createCalendar('principals/user2', 'somerandomid', []); + + $object = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART;VALUE=DATE:20120101\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"; + + $backend->createCalendarObject($returnedId, 'id-1', $object); + $backend->createCalendarObject($returnedId, 'id-2', $object); + + $check = [ + [ + 'id' => 1, + 'etag' => '"' . md5($object) . '"', + 'uri' => 'id-1', + 'size' => strlen($object), + 'calendardata' => $object, + ], + [ + 'id' => 2, + 'etag' => '"' . md5($object) . '"', + 'uri' => 'id-2', + 'size' => strlen($object), + 'calendardata' => $object, + ], + ]; + + $result = $backend->getMultipleCalendarObjects($returnedId, ['id-1', 'id-2']); + + foreach ($check as $index => $props) { + + foreach ($props as $key => $value) { + + if ($key !== 'lastmodified') { + $this->assertEquals($value, $result[$index][$key]); + } else { + $this->assertTrue(isset($result[$index][$key])); + } + + } + + } + + } + + /** + * @depends testCreateCalendarObject + */ + function testGetCalendarObjects() { + + $backend = new SimplePDO($this->pdo); + $returnedId = $backend->createCalendar('principals/user2', 'somerandomid', []); + + $object = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART;VALUE=DATE:20120101\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"; + $backend->createCalendarObject($returnedId, 'random-id', $object); + + $data = $backend->getCalendarObjects($returnedId); + + $this->assertEquals(1, count($data)); + $data = $data[0]; + + $this->assertEquals('random-id', $data['uri']); + $this->assertEquals(strlen($object), $data['size']); + + } + + /** + * @depends testCreateCalendarObject + */ + function testGetCalendarObjectByUID() { + + $backend = new SimplePDO($this->pdo); + $returnedId = $backend->createCalendar('principals/user2', 'somerandomid', []); + + $object = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nUID:foo\r\nDTSTART;VALUE=DATE:20120101\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"; + $backend->createCalendarObject($returnedId, 'random-id', $object); + + $this->assertNull( + $backend->getCalendarObjectByUID('principals/user2', 'bar') + ); + $this->assertEquals( + 'somerandomid/random-id', + $backend->getCalendarObjectByUID('principals/user2', 'foo') + ); + + } + + /** + * @depends testCreateCalendarObject + */ + function testUpdateCalendarObject() { + + $backend = new SimplePDO($this->pdo); + $returnedId = $backend->createCalendar('principals/user2', 'somerandomid', []); + + $object = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART;VALUE=DATE:20120101\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"; + $object2 = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART;VALUE=DATE:20130101\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"; + $backend->createCalendarObject($returnedId, 'random-id', $object); + $backend->updateCalendarObject($returnedId, 'random-id', $object2); + + $data = $backend->getCalendarObject($returnedId, 'random-id'); + + $this->assertEquals($object2, $data['calendardata']); + $this->assertEquals('random-id', $data['uri']); + + + } + + + /** + * @depends testCreateCalendarObject + */ + function testDeleteCalendarObject() { + + $backend = new SimplePDO($this->pdo); + $returnedId = $backend->createCalendar('principals/user2', 'somerandomid', []); + + $object = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART;VALUE=DATE:20120101\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"; + $backend->createCalendarObject($returnedId, 'random-id', $object); + $backend->deleteCalendarObject($returnedId, 'random-id'); + + $data = $backend->getCalendarObject($returnedId, 'random-id'); + $this->assertNull($data); + + } + + + function testCalendarQueryNoResult() { + + $abstract = new SimplePDO($this->pdo); + $filters = [ + 'name' => 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => 'VJOURNAL', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ], + ], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]; + + $this->assertEquals([ + ], $abstract->calendarQuery(1, $filters)); + + } + + function testCalendarQueryTodo() { + + $backend = new SimplePDO($this->pdo); + $backend->createCalendarObject(1, "todo", "BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nEND:VTODO\r\nEND:VCALENDAR\r\n"); + $backend->createCalendarObject(1, "event", "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20120101\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"); + + $filters = [ + 'name' => 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => 'VTODO', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ], + ], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]; + + $this->assertEquals([ + "todo", + ], $backend->calendarQuery(1, $filters)); + + } + function testCalendarQueryTodoNotMatch() { + + $backend = new SimplePDO($this->pdo); + $backend->createCalendarObject(1, "todo", "BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nEND:VTODO\r\nEND:VCALENDAR\r\n"); + $backend->createCalendarObject(1, "event", "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20120101\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"); + + $filters = [ + 'name' => 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => 'VTODO', + 'comp-filters' => [], + 'prop-filters' => [ + [ + 'name' => 'summary', + 'text-match' => null, + 'time-range' => null, + 'param-filters' => [], + 'is-not-defined' => false, + ], + ], + 'is-not-defined' => false, + 'time-range' => null, + ], + ], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]; + + $this->assertEquals([ + ], $backend->calendarQuery(1, $filters)); + + } + + function testCalendarQueryNoFilter() { + + $backend = new SimplePDO($this->pdo); + $backend->createCalendarObject(1, "todo", "BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nEND:VTODO\r\nEND:VCALENDAR\r\n"); + $backend->createCalendarObject(1, "event", "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20120101\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"); + + $filters = [ + 'name' => 'VCALENDAR', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]; + + $result = $backend->calendarQuery(1, $filters); + $this->assertTrue(in_array('todo', $result)); + $this->assertTrue(in_array('event', $result)); + + } + + function testCalendarQueryTimeRange() { + + $backend = new SimplePDO($this->pdo); + $backend->createCalendarObject(1, "todo", "BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nEND:VTODO\r\nEND:VCALENDAR\r\n"); + $backend->createCalendarObject(1, "event", "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART;VALUE=DATE:20120101\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"); + $backend->createCalendarObject(1, "event2", "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART;VALUE=DATE:20120103\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"); + + $filters = [ + 'name' => 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => 'VEVENT', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => [ + 'start' => new \DateTime('20120103'), + 'end' => new \DateTime('20120104'), + ], + ], + ], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]; + + $this->assertEquals([ + "event2", + ], $backend->calendarQuery(1, $filters)); + + } + function testCalendarQueryTimeRangeNoEnd() { + + $backend = new SimplePDO($this->pdo); + $backend->createCalendarObject(1, "todo", "BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nEND:VTODO\r\nEND:VCALENDAR\r\n"); + $backend->createCalendarObject(1, "event", "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20120101\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"); + $backend->createCalendarObject(1, "event2", "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20120103\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"); + + $filters = [ + 'name' => 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => 'VEVENT', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => [ + 'start' => new \DateTime('20120102'), + 'end' => null, + ], + ], + ], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]; + + $this->assertEquals([ + "event2", + ], $backend->calendarQuery(1, $filters)); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarHomeNotificationsTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarHomeNotificationsTest.php new file mode 100644 index 00000000000..36302cc3556 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarHomeNotificationsTest.php @@ -0,0 +1,49 @@ + 'principals/user']); + + $this->assertEquals( + [], + $calendarHome->getChildren() + ); + + } + + /** + * @expectedException \Sabre\DAV\Exception\NotFound + */ + function testGetChildNoSupport() { + + $backend = new Backend\Mock(); + $calendarHome = new CalendarHome($backend, ['uri' => 'principals/user']); + $calendarHome->getChild('notifications'); + + } + + function testGetChildren() { + + $backend = new Backend\MockSharing(); + $calendarHome = new CalendarHome($backend, ['uri' => 'principals/user']); + + $result = $calendarHome->getChildren(); + $this->assertEquals('notifications', $result[0]->getName()); + + } + + function testGetChild() { + + $backend = new Backend\MockSharing(); + $calendarHome = new CalendarHome($backend, ['uri' => 'principals/user']); + $result = $calendarHome->getChild('notifications'); + $this->assertEquals('notifications', $result->getName()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarHomeSharedCalendarsTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarHomeSharedCalendarsTest.php new file mode 100644 index 00000000000..9079fdecd80 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarHomeSharedCalendarsTest.php @@ -0,0 +1,80 @@ + 1, + 'principaluri' => 'principals/user1', + ], + [ + 'id' => 2, + '{http://calendarserver.org/ns/}shared-url' => 'calendars/owner/cal1', + '{http://sabredav.org/ns}owner-principal' => 'principal/owner', + '{http://sabredav.org/ns}read-only' => false, + 'principaluri' => 'principals/user1', + ], + ]; + + $this->backend = new Backend\MockSharing( + $calendars, + [], + [] + ); + + return new CalendarHome($this->backend, [ + 'uri' => 'principals/user1' + ]); + + } + + function testSimple() { + + $instance = $this->getInstance(); + $this->assertEquals('user1', $instance->getName()); + + } + + function testGetChildren() { + + $instance = $this->getInstance(); + $children = $instance->getChildren(); + $this->assertEquals(3, count($children)); + + // Testing if we got all the objects back. + $sharedCalendars = 0; + $hasOutbox = false; + $hasNotifications = false; + + foreach ($children as $child) { + + if ($child instanceof ISharedCalendar) { + $sharedCalendars++; + } + if ($child instanceof Notifications\ICollection) { + $hasNotifications = true; + } + + } + $this->assertEquals(2, $sharedCalendars); + $this->assertTrue($hasNotifications); + + } + + function testShareReply() { + + $instance = $this->getInstance(); + $result = $instance->shareReply('uri', DAV\Sharing\Plugin::INVITE_DECLINED, 'curi', '1'); + $this->assertNull($result); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarHomeSubscriptionsTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarHomeSubscriptionsTest.php new file mode 100644 index 00000000000..4a479c81602 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarHomeSubscriptionsTest.php @@ -0,0 +1,85 @@ + 'baz', + '{http://calendarserver.org/ns/}source' => new \Sabre\DAV\Xml\Property\Href('http://example.org/test.ics'), + ]; + $principal = [ + 'uri' => 'principals/user1' + ]; + $this->backend = new Backend\MockSubscriptionSupport([], []); + $this->backend->createSubscription('principals/user1', 'uri', $props); + + return new CalendarHome($this->backend, $principal); + + } + + function testSimple() { + + $instance = $this->getInstance(); + $this->assertEquals('user1', $instance->getName()); + + } + + function testGetChildren() { + + $instance = $this->getInstance(); + $children = $instance->getChildren(); + $this->assertEquals(1, count($children)); + foreach ($children as $child) { + if ($child instanceof Subscriptions\Subscription) { + return; + } + } + $this->fail('There were no subscription nodes in the calendar home'); + + } + + function testCreateSubscription() { + + $instance = $this->getInstance(); + $rt = ['{DAV:}collection', '{http://calendarserver.org/ns/}subscribed']; + + $props = [ + '{DAV:}displayname' => 'baz', + '{http://calendarserver.org/ns/}source' => new \Sabre\DAV\Xml\Property\Href('http://example.org/test2.ics'), + ]; + $instance->createExtendedCollection('sub2', new MkCol($rt, $props)); + + $children = $instance->getChildren(); + $this->assertEquals(2, count($children)); + + } + + /** + * @expectedException \Sabre\DAV\Exception\InvalidResourceType + */ + function testNoSubscriptionSupport() { + + $principal = [ + 'uri' => 'principals/user1' + ]; + $backend = new Backend\Mock([], []); + $uC = new CalendarHome($backend, $principal); + + $rt = ['{DAV:}collection', '{http://calendarserver.org/ns/}subscribed']; + + $props = [ + '{DAV:}displayname' => 'baz', + '{http://calendarserver.org/ns/}source' => new \Sabre\DAV\Xml\Property\Href('http://example.org/test2.ics'), + ]; + $uC->createExtendedCollection('sub2', new MkCol($rt, $props)); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarHomeTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarHomeTest.php new file mode 100644 index 00000000000..ff52ea6ad20 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarHomeTest.php @@ -0,0 +1,215 @@ +backend = TestUtil::getBackend(); + $this->usercalendars = new CalendarHome($this->backend, [ + 'uri' => 'principals/user1' + ]); + + } + + function testSimple() { + + $this->assertEquals('user1', $this->usercalendars->getName()); + + } + + /** + * @expectedException Sabre\DAV\Exception\NotFound + * @depends testSimple + */ + function testGetChildNotFound() { + + $this->usercalendars->getChild('randomname'); + + } + + function testChildExists() { + + $this->assertFalse($this->usercalendars->childExists('foo')); + $this->assertTrue($this->usercalendars->childExists('UUID-123467')); + + } + + function testGetOwner() { + + $this->assertEquals('principals/user1', $this->usercalendars->getOwner()); + + } + + function testGetGroup() { + + $this->assertNull($this->usercalendars->getGroup()); + + } + + function testGetACL() { + + $expected = [ + [ + 'privilege' => '{DAV:}read', + 'principal' => 'principals/user1', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write', + 'principal' => 'principals/user1', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'principals/user1/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write', + 'principal' => 'principals/user1/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'principals/user1/calendar-proxy-read', + 'protected' => true, + ], + ]; + $this->assertEquals($expected, $this->usercalendars->getACL()); + + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + */ + function testSetACL() { + + $this->usercalendars->setACL([]); + + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + * @depends testSimple + */ + function testSetName() { + + $this->usercalendars->setName('bla'); + + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + * @depends testSimple + */ + function testDelete() { + + $this->usercalendars->delete(); + + } + + /** + * @depends testSimple + */ + function testGetLastModified() { + + $this->assertNull($this->usercalendars->getLastModified()); + + } + + /** + * @expectedException \Sabre\DAV\Exception\MethodNotAllowed + * @depends testSimple + */ + function testCreateFile() { + + $this->usercalendars->createFile('bla'); + + } + + + /** + * @expectedException Sabre\DAV\Exception\MethodNotAllowed + * @depends testSimple + */ + function testCreateDirectory() { + + $this->usercalendars->createDirectory('bla'); + + } + + /** + * @depends testSimple + */ + function testCreateExtendedCollection() { + + $mkCol = new MkCol( + ['{DAV:}collection', '{urn:ietf:params:xml:ns:caldav}calendar'], + [] + ); + $result = $this->usercalendars->createExtendedCollection('newcalendar', $mkCol); + $this->assertNull($result); + $cals = $this->backend->getCalendarsForUser('principals/user1'); + $this->assertEquals(3, count($cals)); + + } + + /** + * @expectedException Sabre\DAV\Exception\InvalidResourceType + * @depends testSimple + */ + function testCreateExtendedCollectionBadResourceType() { + + $mkCol = new MkCol( + ['{DAV:}collection', '{DAV:}blabla'], + [] + ); + $this->usercalendars->createExtendedCollection('newcalendar', $mkCol); + + } + + /** + * @expectedException Sabre\DAV\Exception\InvalidResourceType + * @depends testSimple + */ + function testCreateExtendedCollectionNotACalendar() { + + $mkCol = new MkCol( + ['{DAV:}collection'], + [] + ); + $this->usercalendars->createExtendedCollection('newcalendar', $mkCol); + + } + + function testGetSupportedPrivilegesSet() { + + $this->assertNull($this->usercalendars->getSupportedPrivilegeSet()); + + } + + /** + * @expectedException Sabre\DAV\Exception\NotImplemented + */ + function testShareReplyFail() { + + $this->usercalendars->shareReply('uri', DAV\Sharing\Plugin::INVITE_DECLINED, 'curi', '1'); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarObjectTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarObjectTest.php new file mode 100644 index 00000000000..c92cde66133 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarObjectTest.php @@ -0,0 +1,383 @@ +backend = TestUtil::getBackend(); + + $calendars = $this->backend->getCalendarsForUser('principals/user1'); + $this->assertEquals(2, count($calendars)); + $this->calendar = new Calendar($this->backend, $calendars[0]); + + } + + function teardown() { + + unset($this->calendar); + unset($this->backend); + + } + + function testSetup() { + + $children = $this->calendar->getChildren(); + $this->assertTrue($children[0] instanceof CalendarObject); + + $this->assertInternalType('string', $children[0]->getName()); + $this->assertInternalType('string', $children[0]->get()); + $this->assertInternalType('string', $children[0]->getETag()); + $this->assertEquals('text/calendar; charset=utf-8', $children[0]->getContentType()); + + } + + /** + * @expectedException InvalidArgumentException + */ + function testInvalidArg1() { + + $obj = new CalendarObject( + new Backend\Mock([], []), + [], + [] + ); + + } + + /** + * @expectedException InvalidArgumentException + */ + function testInvalidArg2() { + + $obj = new CalendarObject( + new Backend\Mock([], []), + [], + ['calendarid' => '1'] + ); + + } + + /** + * @depends testSetup + */ + function testPut() { + + $children = $this->calendar->getChildren(); + $this->assertTrue($children[0] instanceof CalendarObject); + $newData = TestUtil::getTestCalendarData(); + + $children[0]->put($newData); + $this->assertEquals($newData, $children[0]->get()); + + } + + /** + * @depends testSetup + */ + function testPutStream() { + + $children = $this->calendar->getChildren(); + $this->assertTrue($children[0] instanceof CalendarObject); + $newData = TestUtil::getTestCalendarData(); + + $stream = fopen('php://temp', 'r+'); + fwrite($stream, $newData); + rewind($stream); + $children[0]->put($stream); + $this->assertEquals($newData, $children[0]->get()); + + } + + + /** + * @depends testSetup + */ + function testDelete() { + + $children = $this->calendar->getChildren(); + $this->assertTrue($children[0] instanceof CalendarObject); + + $obj = $children[0]; + $obj->delete(); + + $children2 = $this->calendar->getChildren(); + $this->assertEquals(count($children) - 1, count($children2)); + + } + + /** + * @depends testSetup + */ + function testGetLastModified() { + + $children = $this->calendar->getChildren(); + $this->assertTrue($children[0] instanceof CalendarObject); + + $obj = $children[0]; + + $lastMod = $obj->getLastModified(); + $this->assertTrue(is_int($lastMod) || ctype_digit($lastMod) || is_null($lastMod)); + + } + + /** + * @depends testSetup + */ + function testGetSize() { + + $children = $this->calendar->getChildren(); + $this->assertTrue($children[0] instanceof CalendarObject); + + $obj = $children[0]; + + $size = $obj->getSize(); + $this->assertInternalType('int', $size); + + } + + function testGetOwner() { + + $children = $this->calendar->getChildren(); + $this->assertTrue($children[0] instanceof CalendarObject); + + $obj = $children[0]; + $this->assertEquals('principals/user1', $obj->getOwner()); + + } + + function testGetGroup() { + + $children = $this->calendar->getChildren(); + $this->assertTrue($children[0] instanceof CalendarObject); + + $obj = $children[0]; + $this->assertNull($obj->getGroup()); + + } + + function testGetACL() { + + $expected = [ + [ + 'privilege' => '{DAV:}read', + 'principal' => 'principals/user1', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'principals/user1/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'principals/user1/calendar-proxy-read', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write', + 'principal' => 'principals/user1', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write', + 'principal' => 'principals/user1/calendar-proxy-write', + 'protected' => true, + ], + ]; + + $children = $this->calendar->getChildren(); + $this->assertTrue($children[0] instanceof CalendarObject); + + $obj = $children[0]; + $this->assertEquals($expected, $obj->getACL()); + + } + + function testDefaultACL() { + + $backend = new Backend\Mock([], []); + $calendarObject = new CalendarObject($backend, ['principaluri' => 'principals/user1'], ['calendarid' => 1, 'uri' => 'foo']); + $expected = [ + [ + 'privilege' => '{DAV:}all', + 'principal' => 'principals/user1', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}all', + 'principal' => 'principals/user1/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'principals/user1/calendar-proxy-read', + 'protected' => true, + ], + ]; + $this->assertEquals($expected, $calendarObject->getACL()); + + + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + */ + function testSetACL() { + + $children = $this->calendar->getChildren(); + $this->assertTrue($children[0] instanceof CalendarObject); + + $obj = $children[0]; + $obj->setACL([]); + + } + + function testGet() { + + $children = $this->calendar->getChildren(); + $this->assertTrue($children[0] instanceof CalendarObject); + + $obj = $children[0]; + + $expected = "BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//iCal 4.0.1//EN +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Asia/Seoul +BEGIN:DAYLIGHT +TZOFFSETFROM:+0900 +RRULE:FREQ=YEARLY;UNTIL=19880507T150000Z;BYMONTH=5;BYDAY=2SU +DTSTART:19870510T000000 +TZNAME:GMT+09:00 +TZOFFSETTO:+1000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+1000 +DTSTART:19881009T000000 +TZNAME:GMT+09:00 +TZOFFSETTO:+0900 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20100225T154229Z +UID:39A6B5ED-DD51-4AFE-A683-C35EE3749627 +TRANSP:TRANSPARENT +SUMMARY:Something here +DTSTAMP:20100228T130202Z +DTSTART;TZID=Asia/Seoul:20100223T060000 +DTEND;TZID=Asia/Seoul:20100223T070000 +ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com +SEQUENCE:2 +END:VEVENT +END:VCALENDAR"; + + + + $this->assertEquals($expected, $obj->get()); + + } + + function testGetRefetch() { + + $backend = new Backend\Mock([], [ + 1 => [ + 'foo' => [ + 'calendardata' => 'foo', + 'uri' => 'foo' + ], + ] + ]); + $obj = new CalendarObject($backend, ['id' => 1], ['uri' => 'foo']); + + $this->assertEquals('foo', $obj->get()); + + } + + function testGetEtag1() { + + $objectInfo = [ + 'calendardata' => 'foo', + 'uri' => 'foo', + 'etag' => 'bar', + 'calendarid' => 1 + ]; + + $backend = new Backend\Mock([], []); + $obj = new CalendarObject($backend, [], $objectInfo); + + $this->assertEquals('bar', $obj->getETag()); + + } + + function testGetEtag2() { + + $objectInfo = [ + 'calendardata' => 'foo', + 'uri' => 'foo', + 'calendarid' => 1 + ]; + + $backend = new Backend\Mock([], []); + $obj = new CalendarObject($backend, [], $objectInfo); + + $this->assertEquals('"' . md5('foo') . '"', $obj->getETag()); + + } + + function testGetSupportedPrivilegesSet() { + + $objectInfo = [ + 'calendardata' => 'foo', + 'uri' => 'foo', + 'calendarid' => 1 + ]; + + $backend = new Backend\Mock([], []); + $obj = new CalendarObject($backend, [], $objectInfo); + $this->assertNull($obj->getSupportedPrivilegeSet()); + + } + + function testGetSize1() { + + $objectInfo = [ + 'calendardata' => 'foo', + 'uri' => 'foo', + 'calendarid' => 1 + ]; + + $backend = new Backend\Mock([], []); + $obj = new CalendarObject($backend, [], $objectInfo); + $this->assertEquals(3, $obj->getSize()); + + } + + function testGetSize2() { + + $objectInfo = [ + 'uri' => 'foo', + 'calendarid' => 1, + 'size' => 4, + ]; + + $backend = new Backend\Mock([], []); + $obj = new CalendarObject($backend, [], $objectInfo); + $this->assertEquals(4, $obj->getSize()); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarQueryVAlarmTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarQueryVAlarmTest.php new file mode 100644 index 00000000000..ca06d8ffa7b --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarQueryVAlarmTest.php @@ -0,0 +1,122 @@ +createComponent('VEVENT'); + $vevent->RRULE = 'FREQ=MONTHLY'; + $vevent->DTSTART = '20120101T120000Z'; + $vevent->UID = 'bla'; + + $valarm = $vcalendar->createComponent('VALARM'); + $valarm->TRIGGER = '-P15D'; + $vevent->add($valarm); + + + $vcalendar->add($vevent); + + $filter = [ + 'name' => 'VCALENDAR', + 'is-not-defined' => false, + 'time-range' => null, + 'prop-filters' => [], + 'comp-filters' => [ + [ + 'name' => 'VEVENT', + 'is-not-defined' => false, + 'time-range' => null, + 'prop-filters' => [], + 'comp-filters' => [ + [ + 'name' => 'VALARM', + 'is-not-defined' => false, + 'prop-filters' => [], + 'comp-filters' => [], + 'time-range' => [ + 'start' => new \DateTime('2012-05-10'), + 'end' => new \DateTime('2012-05-20'), + ], + ], + ], + ], + ], + ]; + + $validator = new CalendarQueryValidator(); + $this->assertTrue($validator->validate($vcalendar, $filter)); + + $vcalendar = new VObject\Component\VCalendar(); + + // A limited recurrence rule, should return false + $vevent = $vcalendar->createComponent('VEVENT'); + $vevent->RRULE = 'FREQ=MONTHLY;COUNT=1'; + $vevent->DTSTART = '20120101T120000Z'; + $vevent->UID = 'bla'; + + $valarm = $vcalendar->createComponent('VALARM'); + $valarm->TRIGGER = '-P15D'; + $vevent->add($valarm); + + $vcalendar->add($vevent); + + $this->assertFalse($validator->validate($vcalendar, $filter)); + } + + function testAlarmWayBefore() { + + $vcalendar = new VObject\Component\VCalendar(); + + $vevent = $vcalendar->createComponent('VEVENT'); + $vevent->DTSTART = '20120101T120000Z'; + $vevent->UID = 'bla'; + + $valarm = $vcalendar->createComponent('VALARM'); + $valarm->TRIGGER = '-P2W1D'; + $vevent->add($valarm); + + $vcalendar->add($vevent); + + $filter = [ + 'name' => 'VCALENDAR', + 'is-not-defined' => false, + 'time-range' => null, + 'prop-filters' => [], + 'comp-filters' => [ + [ + 'name' => 'VEVENT', + 'is-not-defined' => false, + 'time-range' => null, + 'prop-filters' => [], + 'comp-filters' => [ + [ + 'name' => 'VALARM', + 'is-not-defined' => false, + 'prop-filters' => [], + 'comp-filters' => [], + 'time-range' => [ + 'start' => new \DateTime('2011-12-10'), + 'end' => new \DateTime('2011-12-20'), + ], + ], + ], + ], + ], + ]; + + $validator = new CalendarQueryValidator(); + $this->assertTrue($validator->validate($vcalendar, $filter)); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarQueryValidatorTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarQueryValidatorTest.php new file mode 100644 index 00000000000..f3305163bdf --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarQueryValidatorTest.php @@ -0,0 +1,829 @@ +assertFalse($validator->validate($vcal, ['name' => 'VFOO'])); + + } + + /** + * @param string $icalObject + * @param array $filters + * @param int $outcome + * @dataProvider provider + */ + function testValid($icalObject, $filters, $outcome) { + + $validator = new CalendarQueryValidator(); + + // Wrapping filter in a VCALENDAR component filter, as this is always + // there anyway. + $filters = [ + 'name' => 'VCALENDAR', + 'comp-filters' => [$filters], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]; + + $vObject = VObject\Reader::read($icalObject); + + switch ($outcome) { + case 0 : + $this->assertFalse($validator->validate($vObject, $filters)); + break; + case 1 : + $this->assertTrue($validator->validate($vObject, $filters)); + break; + case -1 : + try { + $validator->validate($vObject, $filters); + $this->fail('This test was supposed to fail'); + } catch (\Exception $e) { + // We need to test something to be valid for phpunit strict + // mode. + $this->assertTrue(true); + } catch (\Throwable $e) { + // PHP7 + $this->assertTrue(true); + } + break; + + } + + } + + function provider() { + + $blob1 = << 'VEVENT', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]; + $filter2 = $filter1; + $filter2['name'] = 'VTODO'; + + $filter3 = $filter1; + $filter3['is-not-defined'] = true; + + $filter4 = $filter1; + $filter4['name'] = 'VTODO'; + $filter4['is-not-defined'] = true; + + $filter5 = $filter1; + $filter5['comp-filters'] = [ + [ + 'name' => 'VALARM', + 'is-not-defined' => false, + 'comp-filters' => [], + 'prop-filters' => [], + 'time-range' => null, + ], + ]; + $filter6 = $filter1; + $filter6['prop-filters'] = [ + [ + 'name' => 'SUMMARY', + 'is-not-defined' => false, + 'param-filters' => [], + 'time-range' => null, + 'text-match' => null, + ], + ]; + $filter7 = $filter6; + $filter7['prop-filters'][0]['name'] = 'DESCRIPTION'; + + $filter8 = $filter6; + $filter8['prop-filters'][0]['is-not-defined'] = true; + + $filter9 = $filter7; + $filter9['prop-filters'][0]['is-not-defined'] = true; + + $filter10 = $filter5; + $filter10['prop-filters'] = $filter6['prop-filters']; + + // Param filters + $filter11 = $filter1; + $filter11['prop-filters'] = [ + [ + 'name' => 'DTSTART', + 'is-not-defined' => false, + 'param-filters' => [ + [ + 'name' => 'VALUE', + 'is-not-defined' => false, + 'text-match' => null, + ], + ], + 'time-range' => null, + 'text-match' => null, + ], + ]; + + $filter12 = $filter11; + $filter12['prop-filters'][0]['param-filters'][0]['name'] = 'TZID'; + + $filter13 = $filter11; + $filter13['prop-filters'][0]['param-filters'][0]['is-not-defined'] = true; + + $filter14 = $filter12; + $filter14['prop-filters'][0]['param-filters'][0]['is-not-defined'] = true; + + // Param text filter + $filter15 = $filter11; + $filter15['prop-filters'][0]['param-filters'][0]['text-match'] = [ + 'collation' => 'i;ascii-casemap', + 'value' => 'dAtE', + 'negate-condition' => false, + ]; + $filter16 = $filter15; + $filter16['prop-filters'][0]['param-filters'][0]['text-match']['collation'] = 'i;octet'; + + $filter17 = $filter15; + $filter17['prop-filters'][0]['param-filters'][0]['text-match']['negate-condition'] = true; + + $filter18 = $filter15; + $filter18['prop-filters'][0]['param-filters'][0]['text-match']['negate-condition'] = true; + $filter18['prop-filters'][0]['param-filters'][0]['text-match']['collation'] = 'i;octet'; + + // prop + text + $filter19 = $filter5; + $filter19['comp-filters'][0]['prop-filters'] = [ + [ + 'name' => 'action', + 'is-not-defined' => false, + 'time-range' => null, + 'param-filters' => [], + 'text-match' => [ + 'collation' => 'i;ascii-casemap', + 'value' => 'display', + 'negate-condition' => false, + ], + ], + ]; + + // Time range + $filter20 = [ + 'name' => 'VEVENT', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => [ + 'start' => new \DateTime('2011-01-01 10:00:00', new \DateTimeZone('GMT')), + 'end' => new \DateTime('2011-01-01 13:00:00', new \DateTimeZone('GMT')), + ], + ]; + // Time range, no end date + $filter21 = $filter20; + $filter21['time-range']['end'] = null; + + // Time range, no start date + $filter22 = $filter20; + $filter22['time-range']['start'] = null; + + // Time range, other dates + $filter23 = $filter20; + $filter23['time-range'] = [ + 'start' => new \DateTime('2011-02-01 10:00:00', new \DateTimeZone('GMT')), + 'end' => new \DateTime('2011-02-01 13:00:00', new \DateTimeZone('GMT')), + ]; + // Time range + $filter24 = [ + 'name' => 'VTODO', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => [ + 'start' => new \DateTime('2011-01-01 12:45:00', new \DateTimeZone('GMT')), + 'end' => new \DateTime('2011-01-01 13:15:00', new \DateTimeZone('GMT')), + ], + ]; + // Time range, other dates (1 month in the future) + $filter25 = $filter24; + $filter25['time-range'] = [ + 'start' => new \DateTime('2011-02-01 10:00:00', new \DateTimeZone('GMT')), + 'end' => new \DateTime('2011-02-01 13:00:00', new \DateTimeZone('GMT')), + ]; + $filter26 = $filter24; + $filter26['time-range'] = [ + 'start' => new \DateTime('2011-01-01 11:45:00', new \DateTimeZone('GMT')), + 'end' => new \DateTime('2011-01-01 12:15:00', new \DateTimeZone('GMT')), + ]; + + // Time range for VJOURNAL + $filter27 = [ + 'name' => 'VJOURNAL', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => [ + 'start' => new \DateTime('2011-01-01 12:45:00', new \DateTimeZone('GMT')), + 'end' => new \DateTime('2011-01-01 13:15:00', new \DateTimeZone('GMT')), + ], + ]; + $filter28 = $filter27; + $filter28['time-range'] = [ + 'start' => new \DateTime('2011-01-01 11:45:00', new \DateTimeZone('GMT')), + 'end' => new \DateTime('2011-01-01 12:15:00', new \DateTimeZone('GMT')), + ]; + // Time range for VFREEBUSY + $filter29 = [ + 'name' => 'VFREEBUSY', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => [ + 'start' => new \DateTime('2011-01-01 12:45:00', new \DateTimeZone('GMT')), + 'end' => new \DateTime('2011-01-01 13:15:00', new \DateTimeZone('GMT')), + ], + ]; + // Time range filter on property + $filter30 = [ + 'name' => 'VEVENT', + 'comp-filters' => [], + 'prop-filters' => [ + [ + 'name' => 'DTSTART', + 'is-not-defined' => false, + 'param-filters' => [], + 'time-range' => [ + 'start' => new \DateTime('2011-01-01 10:00:00', new \DateTimeZone('GMT')), + 'end' => new \DateTime('2011-01-01 13:00:00', new \DateTimeZone('GMT')), + ], + 'text-match' => null, + ], + ], + 'is-not-defined' => false, + 'time-range' => null, + ]; + + // Time range for alarm + $filter31 = [ + 'name' => 'VEVENT', + 'prop-filters' => [], + 'comp-filters' => [ + [ + 'name' => 'VALARM', + 'is-not-defined' => false, + 'comp-filters' => [], + 'prop-filters' => [], + 'time-range' => [ + 'start' => new \DateTime('2011-01-01 10:45:00', new \DateTimeZone('GMT')), + 'end' => new \DateTime('2011-01-01 11:15:00', new \DateTimeZone('GMT')), + ], + 'text-match' => null, + ], + ], + 'is-not-defined' => false, + 'time-range' => null, + ]; + $filter32 = $filter31; + $filter32['comp-filters'][0]['time-range'] = [ + 'start' => new \DateTime('2011-01-01 11:45:00', new \DateTimeZone('GMT')), + 'end' => new \DateTime('2011-01-01 12:15:00', new \DateTimeZone('GMT')), + ]; + + $filter33 = $filter31; + $filter33['name'] = 'VTODO'; + $filter34 = $filter32; + $filter34['name'] = 'VTODO'; + $filter35 = $filter31; + $filter35['name'] = 'VJOURNAL'; + $filter36 = $filter32; + $filter36['name'] = 'VJOURNAL'; + + // Time range filter on non-datetime property + $filter37 = [ + 'name' => 'VEVENT', + 'comp-filters' => [], + 'prop-filters' => [ + [ + 'name' => 'SUMMARY', + 'is-not-defined' => false, + 'param-filters' => [], + 'time-range' => [ + 'start' => new \DateTime('2011-01-01 10:00:00', new \DateTimeZone('GMT')), + 'end' => new \DateTime('2011-01-01 13:00:00', new \DateTimeZone('GMT')), + ], + 'text-match' => null, + ], + ], + 'is-not-defined' => false, + 'time-range' => null, + ]; + + $filter38 = [ + 'name' => 'VEVENT', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => [ + 'start' => new \DateTime('2012-07-01 00:00:00', new \DateTimeZone('UTC')), + 'end' => new \DateTime('2012-08-01 00:00:00', new \DateTimeZone('UTC')), + ] + ]; + $filter39 = [ + 'name' => 'VEVENT', + 'comp-filters' => [ + [ + 'name' => 'VALARM', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => [ + 'start' => new \DateTime('2012-09-01 00:00:00', new \DateTimeZone('UTC')), + 'end' => new \DateTime('2012-10-01 00:00:00', new \DateTimeZone('UTC')), + ] + ], + ], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]; + + return [ + + // Component check + + [$blob1, $filter1, 1], + [$blob1, $filter2, 0], + [$blob1, $filter3, 0], + [$blob1, $filter4, 1], + + // Subcomponent check (4) + [$blob1, $filter5, 0], + [$blob2, $filter5, 1], + + // Property checki (6) + [$blob1, $filter6, 1], + [$blob1, $filter7, 0], + [$blob1, $filter8, 0], + [$blob1, $filter9, 1], + + // Subcomponent + property (10) + [$blob2, $filter10, 1], + + // Param filter (11) + [$blob3, $filter11, 1], + [$blob3, $filter12, 0], + [$blob3, $filter13, 0], + [$blob3, $filter14, 1], + + // Param + text (15) + [$blob3, $filter15, 1], + [$blob3, $filter16, 0], + [$blob3, $filter17, 0], + [$blob3, $filter18, 1], + + // Prop + text (19) + [$blob2, $filter19, 1], + + // Incorrect object (vcard) (20) + [$blob4, $filter1, -1], + + // Time-range for event (21) + [$blob5, $filter20, 1], + [$blob6, $filter20, 1], + [$blob7, $filter20, 1], + [$blob8, $filter20, 1], + + [$blob5, $filter21, 1], + [$blob5, $filter22, 1], + + [$blob5, $filter23, 0], + [$blob6, $filter23, 0], + [$blob7, $filter23, 0], + [$blob8, $filter23, 0], + + // Time-range for todo (31) + [$blob9, $filter24, 1], + [$blob9, $filter25, 0], + [$blob9, $filter26, 1], + [$blob10, $filter24, 1], + [$blob10, $filter25, 0], + [$blob10, $filter26, 1], + + [$blob11, $filter24, 0], + [$blob11, $filter25, 0], + [$blob11, $filter26, 1], + + [$blob12, $filter24, 1], + [$blob12, $filter25, 0], + [$blob12, $filter26, 0], + + [$blob13, $filter24, 1], + [$blob13, $filter25, 0], + [$blob13, $filter26, 1], + + [$blob14, $filter24, 1], + [$blob14, $filter25, 0], + [$blob14, $filter26, 0], + + [$blob15, $filter24, 1], + [$blob15, $filter25, 1], + [$blob15, $filter26, 1], + + [$blob16, $filter24, 1], + [$blob16, $filter25, 1], + [$blob16, $filter26, 1], + + // Time-range for journals (55) + [$blob17, $filter27, 0], + [$blob17, $filter28, 0], + [$blob18, $filter27, 0], + [$blob18, $filter28, 1], + [$blob19, $filter27, 1], + [$blob19, $filter28, 1], + + // Time-range for free-busy (61) + [$blob20, $filter29, -1], + + // Time-range on property (62) + [$blob5, $filter30, 1], + [$blob3, $filter37, -1], + [$blob3, $filter30, 0], + + // Time-range on alarm in vevent (65) + [$blob21, $filter31, 1], + [$blob21, $filter32, 0], + [$blob22, $filter31, 1], + [$blob22, $filter32, 0], + [$blob23, $filter31, 1], + [$blob23, $filter32, 0], + [$blob24, $filter31, 1], + [$blob24, $filter32, 0], + [$blob25, $filter31, 1], + [$blob25, $filter32, 0], + [$blob26, $filter31, 1], + [$blob26, $filter32, 0], + + // Time-range on alarm for vtodo (77) + [$blob27, $filter33, 1], + [$blob27, $filter34, 0], + + // Time-range on alarm for vjournal (79) + [$blob28, $filter35, -1], + [$blob28, $filter36, -1], + + // Time-range on alarm with duration (81) + [$blob29, $filter31, 1], + [$blob29, $filter32, 0], + [$blob30, $filter31, 0], + [$blob30, $filter32, 0], + + // Time-range with RRULE (85) + [$blob31, $filter20, 1], + [$blob32, $filter20, 0], + + // Bug reported on mailing list, related to all-day events (87) + //array($blob33, $filter38, 1), + + // Event in timerange, but filtered alarm is in the far future (88). + [$blob34, $filter39, 0], + ]; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarTest.php new file mode 100644 index 00000000000..df85b6ded0d --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/CalendarTest.php @@ -0,0 +1,256 @@ +backend = TestUtil::getBackend(); + + $this->calendars = $this->backend->getCalendarsForUser('principals/user1'); + $this->assertEquals(2, count($this->calendars)); + $this->calendar = new Calendar($this->backend, $this->calendars[0]); + + + } + + function teardown() { + + unset($this->backend); + + } + + function testSimple() { + + $this->assertEquals($this->calendars[0]['uri'], $this->calendar->getName()); + + } + + /** + * @depends testSimple + */ + function testUpdateProperties() { + + $propPatch = new PropPatch([ + '{DAV:}displayname' => 'NewName', + ]); + + $result = $this->calendar->propPatch($propPatch); + $result = $propPatch->commit(); + + $this->assertEquals(true, $result); + + $calendars2 = $this->backend->getCalendarsForUser('principals/user1'); + $this->assertEquals('NewName', $calendars2[0]['{DAV:}displayname']); + + } + + /** + * @depends testSimple + */ + function testGetProperties() { + + $question = [ + '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set', + ]; + + $result = $this->calendar->getProperties($question); + + foreach ($question as $q) $this->assertArrayHasKey($q, $result); + + $this->assertEquals(['VEVENT', 'VTODO'], $result['{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set']->getValue()); + + } + + /** + * @expectedException Sabre\DAV\Exception\NotFound + * @depends testSimple + */ + function testGetChildNotFound() { + + $this->calendar->getChild('randomname'); + + } + + /** + * @depends testSimple + */ + function testGetChildren() { + + $children = $this->calendar->getChildren(); + $this->assertEquals(1, count($children)); + + $this->assertTrue($children[0] instanceof CalendarObject); + + } + + /** + * @depends testGetChildren + */ + function testChildExists() { + + $this->assertFalse($this->calendar->childExists('foo')); + + $children = $this->calendar->getChildren(); + $this->assertTrue($this->calendar->childExists($children[0]->getName())); + } + + + + /** + * @expectedException Sabre\DAV\Exception\MethodNotAllowed + */ + function testCreateDirectory() { + + $this->calendar->createDirectory('hello'); + + } + + /** + * @expectedException Sabre\DAV\Exception\MethodNotAllowed + */ + function testSetName() { + + $this->calendar->setName('hello'); + + } + + function testGetLastModified() { + + $this->assertNull($this->calendar->getLastModified()); + + } + + function testCreateFile() { + + $file = fopen('php://memory', 'r+'); + fwrite($file, TestUtil::getTestCalendarData()); + rewind($file); + + $this->calendar->createFile('hello', $file); + + $file = $this->calendar->getChild('hello'); + $this->assertTrue($file instanceof CalendarObject); + + } + + function testCreateFileNoSupportedComponents() { + + $file = fopen('php://memory', 'r+'); + fwrite($file, TestUtil::getTestCalendarData()); + rewind($file); + + $calendar = new Calendar($this->backend, $this->calendars[1]); + $calendar->createFile('hello', $file); + + $file = $calendar->getChild('hello'); + $this->assertTrue($file instanceof CalendarObject); + + } + + function testDelete() { + + $this->calendar->delete(); + + $calendars = $this->backend->getCalendarsForUser('principals/user1'); + $this->assertEquals(1, count($calendars)); + } + + function testGetOwner() { + + $this->assertEquals('principals/user1', $this->calendar->getOwner()); + + } + + function testGetGroup() { + + $this->assertNull($this->calendar->getGroup()); + + } + + function testGetACL() { + + $expected = [ + [ + 'privilege' => '{DAV:}read', + 'principal' => 'principals/user1', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'principals/user1/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'principals/user1/calendar-proxy-read', + 'protected' => true, + ], + [ + 'privilege' => '{' . Plugin::NS_CALDAV . '}read-free-busy', + 'principal' => '{DAV:}authenticated', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write', + 'principal' => 'principals/user1', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write', + 'principal' => 'principals/user1/calendar-proxy-write', + 'protected' => true, + ], + ]; + $this->assertEquals($expected, $this->calendar->getACL()); + + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + */ + function testSetACL() { + + $this->calendar->setACL([]); + + } + + function testGetSyncToken() { + + $this->assertNull($this->calendar->getSyncToken()); + + } + + function testGetSyncTokenNoSyncSupport() { + + $calendar = new Calendar(new Backend\Mock([], []), []); + $this->assertNull($calendar->getSyncToken()); + + } + + function testGetChanges() { + + $this->assertNull($this->calendar->getChanges(1, 1)); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/ExpandEventsDTSTARTandDTENDTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/ExpandEventsDTSTARTandDTENDTest.php new file mode 100644 index 00000000000..9a3d47828fb --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/ExpandEventsDTSTARTandDTENDTest.php @@ -0,0 +1,113 @@ + 1, + 'name' => 'Calendar', + 'principaluri' => 'principals/user1', + 'uri' => 'calendar1', + ] + ]; + + protected $caldavCalendarObjects = [ + 1 => [ + 'event.ics' => [ + 'calendardata' => 'BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +DTEND;TZID=Europe/Berlin:20120207T191500 +RRULE:FREQ=DAILY;INTERVAL=1;COUNT=3 +SUMMARY:RecurringEvents 3 times +DTSTART;TZID=Europe/Berlin:20120207T181500 +END:VEVENT +BEGIN:VEVENT +CREATED:20120207T111900Z +UID:foobar +DTEND;TZID=Europe/Berlin:20120208T191500 +SUMMARY:RecurringEvents 3 times OVERWRITTEN +DTSTART;TZID=Europe/Berlin:20120208T181500 +RECURRENCE-ID;TZID=Europe/Berlin:20120208T181500 +END:VEVENT +END:VCALENDAR +', + ], + ], + ]; + + function testExpand() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'REPORT', + 'HTTP_CONTENT_TYPE' => 'application/xml', + 'REQUEST_URI' => '/calendars/user1/calendar1', + 'HTTP_DEPTH' => '1', + ]); + + $request->setBody(' + + + + + + + + + + + + + + +'); + + $response = $this->request($request); + + // Everts super awesome xml parser. + $body = substr( + $response->body, + $start = strpos($response->body, 'BEGIN:VCALENDAR'), + strpos($response->body, 'END:VCALENDAR') - $start + 13 + ); + $body = str_replace(' ', '', $body); + + try { + $vObject = VObject\Reader::read($body); + } catch (VObject\ParseException $e) { + $this->fail('Could not parse object. Error:' . $e->getMessage() . ' full object: ' . $response->getBodyAsString()); + } + + // check if DTSTARTs and DTENDs are correct + foreach ($vObject->VEVENT as $vevent) { + /** @var $vevent Sabre\VObject\Component\VEvent */ + foreach ($vevent->children() as $child) { + /** @var $child Sabre\VObject\Property */ + if ($child->name == 'DTSTART') { + // DTSTART has to be one of three valid values + $this->assertContains($child->getValue(), ['20120207T171500Z', '20120208T171500Z', '20120209T171500Z'], 'DTSTART is not a valid value: ' . $child->getValue()); + } elseif ($child->name == 'DTEND') { + // DTEND has to be one of three valid values + $this->assertContains($child->getValue(), ['20120207T181500Z', '20120208T181500Z', '20120209T181500Z'], 'DTEND is not a valid value: ' . $child->getValue()); + } + } + } + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/ExpandEventsDTSTARTandDTENDbyDayTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/ExpandEventsDTSTARTandDTENDbyDayTest.php new file mode 100644 index 00000000000..efc49673f3f --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/ExpandEventsDTSTARTandDTENDbyDayTest.php @@ -0,0 +1,102 @@ + 1, + 'name' => 'Calendar', + 'principaluri' => 'principals/user1', + 'uri' => 'calendar1', + ] + ]; + + protected $caldavCalendarObjects = [ + 1 => [ + 'event.ics' => [ + 'calendardata' => 'BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +DTEND;TZID=Europe/Berlin:20120207T191500 +RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=TU,TH +SUMMARY:RecurringEvents on tuesday and thursday +DTSTART;TZID=Europe/Berlin:20120207T181500 +END:VEVENT +END:VCALENDAR +', + ], + ], + ]; + + function testExpandRecurringByDayEvent() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'REPORT', + 'HTTP_CONTENT_TYPE' => 'application/xml', + 'REQUEST_URI' => '/calendars/user1/calendar1', + 'HTTP_DEPTH' => '1', + ]); + + $request->setBody(' + + + + + + + + + + + + + + +'); + + $response = $this->request($request); + + // Everts super awesome xml parser. + $body = substr( + $response->body, + $start = strpos($response->body, 'BEGIN:VCALENDAR'), + strpos($response->body, 'END:VCALENDAR') - $start + 13 + ); + $body = str_replace(' ', '', $body); + + $vObject = VObject\Reader::read($body); + + $this->assertEquals(2, count($vObject->VEVENT)); + + // check if DTSTARTs and DTENDs are correct + foreach ($vObject->VEVENT as $vevent) { + /** @var $vevent Sabre\VObject\Component\VEvent */ + foreach ($vevent->children() as $child) { + /** @var $child Sabre\VObject\Property */ + if ($child->name == 'DTSTART') { + // DTSTART has to be one of two valid values + $this->assertContains($child->getValue(), ['20120214T171500Z', '20120216T171500Z'], 'DTSTART is not a valid value: ' . $child->getValue()); + } elseif ($child->name == 'DTEND') { + // DTEND has to be one of two valid values + $this->assertContains($child->getValue(), ['20120214T181500Z', '20120216T181500Z'], 'DTEND is not a valid value: ' . $child->getValue()); + } + } + } + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/ExpandEventsDoubleEventsTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/ExpandEventsDoubleEventsTest.php new file mode 100644 index 00000000000..3a22e03d4a5 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/ExpandEventsDoubleEventsTest.php @@ -0,0 +1,103 @@ + 1, + 'name' => 'Calendar', + 'principaluri' => 'principals/user1', + 'uri' => 'calendar1', + ] + ]; + + protected $caldavCalendarObjects = [ + 1 => [ + 'event.ics' => [ + 'calendardata' => 'BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +DTEND;TZID=Europe/Berlin:20120207T191500 +RRULE:FREQ=DAILY;INTERVAL=1;COUNT=3 +SUMMARY:RecurringEvents 3 times +DTSTART;TZID=Europe/Berlin:20120207T181500 +END:VEVENT +BEGIN:VEVENT +CREATED:20120207T111900Z +UID:foobar +DTEND;TZID=Europe/Berlin:20120208T191500 +SUMMARY:RecurringEvents 3 times OVERWRITTEN +DTSTART;TZID=Europe/Berlin:20120208T181500 +RECURRENCE-ID;TZID=Europe/Berlin:20120208T181500 +END:VEVENT +END:VCALENDAR +', + ], + ], + ]; + + function testExpand() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'REPORT', + 'HTTP_CONTENT_TYPE' => 'application/xml', + 'REQUEST_URI' => '/calendars/user1/calendar1', + 'HTTP_DEPTH' => '1', + ]); + + $request->setBody(' + + + + + + + + + + + + + + +'); + + $response = $this->request($request); + + // Everts super awesome xml parser. + $body = substr( + $response->body, + $start = strpos($response->body, 'BEGIN:VCALENDAR'), + strpos($response->body, 'END:VCALENDAR') - $start + 13 + ); + $body = str_replace(' ', '', $body); + + $vObject = VObject\Reader::read($body); + + // We only expect 3 events + $this->assertEquals(3, count($vObject->VEVENT), 'We got 6 events instead of 3. Output: ' . $body); + + // TZID should be gone + $this->assertFalse(isset($vObject->VEVENT->DTSTART['TZID'])); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/ExpandEventsFloatingTimeTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/ExpandEventsFloatingTimeTest.php new file mode 100644 index 00000000000..fba47d79ba1 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/ExpandEventsFloatingTimeTest.php @@ -0,0 +1,207 @@ + 1, + 'name' => 'Calendar', + 'principaluri' => 'principals/user1', + 'uri' => 'calendar1', + '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => 'BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +DTSTART:19810329T020000 +TZNAME:GMT+2 +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +DTSTART:19961027T030000 +TZNAME:GMT+1 +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE +END:VCALENDAR', + ] + ]; + + protected $caldavCalendarObjects = [ + 1 => [ + 'event.ics' => [ + 'calendardata' => 'BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +BEGIN:VEVENT +CREATED:20140701T143658Z +UID:dba46fe8-1631-4d98-a575-97963c364dfe +DTEND:20141108T073000 +TRANSP:OPAQUE +SUMMARY:Floating Time event, starting 05:30am Europe/Berlin +DTSTART:20141108T053000 +DTSTAMP:20140701T143706Z +SEQUENCE:1 +END:VEVENT +END:VCALENDAR +', + ], + ], + ]; + + function testExpandCalendarQuery() { + + $request = new HTTP\Request('REPORT', '/calendars/user1/calendar1', [ + 'Depth' => 1, + 'Content-Type' => 'application/xml', + ]); + + $request->setBody(' + + + + + + + + + + + + + + +'); + + $response = $this->request($request); + + // Everts super awesome xml parser. + $body = substr( + $response->body, + $start = strpos($response->body, 'BEGIN:VCALENDAR'), + strpos($response->body, 'END:VCALENDAR') - $start + 13 + ); + $body = str_replace(' ', '', $body); + + $vObject = VObject\Reader::read($body); + + // check if DTSTARTs and DTENDs are correct + foreach ($vObject->VEVENT as $vevent) { + /** @var $vevent Sabre\VObject\Component\VEvent */ + foreach ($vevent->children() as $child) { + /** @var $child Sabre\VObject\Property */ + if ($child->name == 'DTSTART') { + // DTSTART should be the UTC equivalent of given floating time + $this->assertEquals('20141108T043000Z', $child->getValue()); + } elseif ($child->name == 'DTEND') { + // DTEND should be the UTC equivalent of given floating time + $this->assertEquals('20141108T063000Z', $child->getValue()); + } + } + } + } + + function testExpandMultiGet() { + + $request = new HTTP\Request('REPORT', '/calendars/user1/calendar1', [ + 'Depth' => 1, + 'Content-Type' => 'application/xml', + ]); + + $request->setBody(' + + + + + + + + /calendars/user1/calendar1/event.ics +'); + + $response = $this->request($request); + + $this->assertEquals(207, $response->getStatus()); + + // Everts super awesome xml parser. + $body = substr( + $response->body, + $start = strpos($response->body, 'BEGIN:VCALENDAR'), + strpos($response->body, 'END:VCALENDAR') - $start + 13 + ); + $body = str_replace(' ', '', $body); + + $vObject = VObject\Reader::read($body); + + // check if DTSTARTs and DTENDs are correct + foreach ($vObject->VEVENT as $vevent) { + /** @var $vevent Sabre\VObject\Component\VEvent */ + foreach ($vevent->children() as $child) { + /** @var $child Sabre\VObject\Property */ + if ($child->name == 'DTSTART') { + // DTSTART should be the UTC equivalent of given floating time + $this->assertEquals($child->getValue(), '20141108T043000Z'); + } elseif ($child->name == 'DTEND') { + // DTEND should be the UTC equivalent of given floating time + $this->assertEquals($child->getValue(), '20141108T063000Z'); + } + } + } + } + + function testExpandExport() { + + $request = new HTTP\Request('GET', '/calendars/user1/calendar1?export&start=1&end=2000000000&expand=1', [ + 'Depth' => 1, + 'Content-Type' => 'application/xml', + ]); + + $response = $this->request($request); + + $this->assertEquals(200, $response->getStatus()); + + // Everts super awesome xml parser. + $body = substr( + $response->body, + $start = strpos($response->body, 'BEGIN:VCALENDAR'), + strpos($response->body, 'END:VCALENDAR') - $start + 13 + ); + $body = str_replace(' ', '', $body); + + $vObject = VObject\Reader::read($body); + + // check if DTSTARTs and DTENDs are correct + foreach ($vObject->VEVENT as $vevent) { + /** @var $vevent Sabre\VObject\Component\VEvent */ + foreach ($vevent->children() as $child) { + /** @var $child Sabre\VObject\Property */ + if ($child->name == 'DTSTART') { + // DTSTART should be the UTC equivalent of given floating time + $this->assertEquals('20141108T043000Z', $child->getValue()); + } elseif ($child->name == 'DTEND') { + // DTEND should be the UTC equivalent of given floating time + $this->assertEquals('20141108T063000Z', $child->getValue()); + } + } + } + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/FreeBusyReportTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/FreeBusyReportTest.php new file mode 100644 index 00000000000..7604c7f4c1b --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/FreeBusyReportTest.php @@ -0,0 +1,174 @@ + [ + 'obj1' => [ + 'calendarid' => 1, + 'uri' => 'event1.ics', + 'calendardata' => $obj1, + ], + 'obj2' => [ + 'calendarid' => 1, + 'uri' => 'event2.ics', + 'calendardata' => $obj2 + ], + 'obj3' => [ + 'calendarid' => 1, + 'uri' => 'event3.ics', + 'calendardata' => $obj3 + ] + ], + ]; + + + $caldavBackend = new Backend\Mock([], $calendarData); + + $calendar = new Calendar($caldavBackend, [ + 'id' => 1, + 'uri' => 'calendar', + 'principaluri' => 'principals/user1', + '{' . Plugin::NS_CALDAV . '}calendar-timezone' => "BEGIN:VCALENDAR\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nEND:VTIMEZONE\r\nEND:VCALENDAR", + ]); + + $this->server = new DAV\Server([$calendar]); + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_URI' => '/calendar', + ]); + $this->server->httpRequest = $request; + $this->server->httpResponse = new HTTP\ResponseMock(); + + $this->plugin = new Plugin(); + $this->server->addPlugin($this->plugin); + + } + + function testFreeBusyReport() { + + $reportXML = << + + + +XML; + + $report = $this->server->xml->parse($reportXML, null, $rootElem); + $this->plugin->report($rootElem, $report, null); + + $this->assertEquals(200, $this->server->httpResponse->status); + $this->assertEquals('text/calendar', $this->server->httpResponse->getHeader('Content-Type')); + $this->assertTrue(strpos($this->server->httpResponse->body, 'BEGIN:VFREEBUSY') !== false); + $this->assertTrue(strpos($this->server->httpResponse->body, '20111005T120000Z/20111005T130000Z') !== false); + $this->assertTrue(strpos($this->server->httpResponse->body, '20111006T100000Z/20111006T110000Z') !== false); + + } + + /** + * @expectedException Sabre\DAV\Exception\BadRequest + */ + function testFreeBusyReportNoTimeRange() { + + $reportXML = << + + +XML; + + $report = $this->server->xml->parse($reportXML, null, $rootElem); + + } + + /** + * @expectedException Sabre\DAV\Exception\NotImplemented + */ + function testFreeBusyReportWrongNode() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_URI' => '/', + ]); + $this->server->httpRequest = $request; + + $reportXML = << + + + +XML; + + $report = $this->server->xml->parse($reportXML, null, $rootElem); + $this->plugin->report($rootElem, $report, null); + + } + + /** + * @expectedException Sabre\DAV\Exception + */ + function testFreeBusyReportNoACLPlugin() { + + $this->server = new DAV\Server(); + $this->plugin = new Plugin(); + $this->server->addPlugin($this->plugin); + + $reportXML = << + + + +XML; + + $report = $this->server->xml->parse($reportXML, null, $rootElem); + $this->plugin->report($rootElem, $report, null); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/GetEventsByTimerangeTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/GetEventsByTimerangeTest.php new file mode 100644 index 00000000000..5fd8d29a11b --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/GetEventsByTimerangeTest.php @@ -0,0 +1,82 @@ + 1, + 'name' => 'Calendar', + 'principaluri' => 'principals/user1', + 'uri' => 'calendar1', + ] + ]; + + protected $caldavCalendarObjects = [ + 1 => [ + 'event.ics' => [ + 'calendardata' => 'BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +CREATED:20120313T142342Z +UID:171EBEFC-C951-499D-B234-7BA7D677B45D +DTEND;TZID=Europe/Berlin:20120227T010000 +TRANSP:OPAQUE +SUMMARY:Monday 0h +DTSTART;TZID=Europe/Berlin:20120227T000000 +DTSTAMP:20120313T142416Z +SEQUENCE:4 +END:VEVENT +END:VCALENDAR +', + ], + ], + ]; + + function testQueryTimerange() { + + $request = new HTTP\Request( + 'REPORT', + '/calendars/user1/calendar1', + [ + 'Content-Type' => 'application/xml', + 'Depth' => '1', + ] + ); + + $request->setBody(' + + + + + + + + + + + + + + +'); + + $response = $this->request($request); + + $this->assertTrue(strpos($response->body, 'BEGIN:VCALENDAR') !== false); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/ICSExportPluginTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/ICSExportPluginTest.php new file mode 100644 index 00000000000..75412577e9b --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/ICSExportPluginTest.php @@ -0,0 +1,386 @@ +icsExportPlugin = new ICSExportPlugin(); + $this->server->addPlugin( + $this->icsExportPlugin + ); + + $id = $this->caldavBackend->createCalendar( + 'principals/admin', + 'UUID-123467', + [ + '{DAV:}displayname' => 'Hello!', + '{http://apple.com/ns/ical/}calendar-color' => '#AA0000FF', + ] + ); + + $this->caldavBackend->createCalendarObject( + $id, + 'event-1', + <<caldavBackend->createCalendarObject( + $id, + 'todo-1', + <<assertEquals( + $this->icsExportPlugin, + $this->server->getPlugin('ics-export') + ); + $this->assertEquals($this->icsExportPlugin, $this->server->getPlugin('ics-export')); + $this->assertEquals('ics-export', $this->icsExportPlugin->getPluginInfo()['name']); + + } + + function testBeforeMethod() { + + $request = new HTTP\Request( + 'GET', + '/calendars/admin/UUID-123467?export' + ); + + $response = $this->request($request); + + $this->assertEquals(200, $response->getStatus()); + $this->assertEquals('text/calendar', $response->getHeader('Content-Type')); + + $obj = VObject\Reader::read($response->body); + + $this->assertEquals(8, count($obj->children())); + $this->assertEquals(1, count($obj->VERSION)); + $this->assertEquals(1, count($obj->CALSCALE)); + $this->assertEquals(1, count($obj->PRODID)); + $this->assertTrue(strpos((string)$obj->PRODID, DAV\Version::VERSION) !== false); + $this->assertEquals(1, count($obj->VTIMEZONE)); + $this->assertEquals(1, count($obj->VEVENT)); + $this->assertEquals("Hello!", $obj->{"X-WR-CALNAME"}); + $this->assertEquals("#AA0000FF", $obj->{"X-APPLE-CALENDAR-COLOR"}); + + } + function testBeforeMethodNoVersion() { + + $request = new HTTP\Request( + 'GET', + '/calendars/admin/UUID-123467?export' + ); + DAV\Server::$exposeVersion = false; + $response = $this->request($request); + DAV\Server::$exposeVersion = true; + + $this->assertEquals(200, $response->getStatus()); + $this->assertEquals('text/calendar', $response->getHeader('Content-Type')); + + $obj = VObject\Reader::read($response->body); + + $this->assertEquals(8, count($obj->children())); + $this->assertEquals(1, count($obj->VERSION)); + $this->assertEquals(1, count($obj->CALSCALE)); + $this->assertEquals(1, count($obj->PRODID)); + $this->assertFalse(strpos((string)$obj->PRODID, DAV\Version::VERSION) !== false); + $this->assertEquals(1, count($obj->VTIMEZONE)); + $this->assertEquals(1, count($obj->VEVENT)); + + } + + function testBeforeMethodNoExport() { + + $request = new HTTP\Request( + 'GET', + '/calendars/admin/UUID-123467' + ); + $response = new HTTP\Response(); + $this->assertNull($this->icsExportPlugin->httpGet($request, $response)); + + } + + function testACLIntegrationBlocked() { + + $aclPlugin = new DAVACL\Plugin(); + $aclPlugin->allowUnauthenticatedAccess = false; + $this->server->addPlugin( + $aclPlugin + ); + + $request = new HTTP\Request( + 'GET', + '/calendars/admin/UUID-123467?export' + ); + + $this->request($request, 403); + + } + + function testACLIntegrationNotBlocked() { + + $aclPlugin = new DAVACL\Plugin(); + $aclPlugin->allowUnauthenticatedAccess = false; + $this->server->addPlugin( + $aclPlugin + ); + $this->server->addPlugin( + new Plugin() + ); + + $this->autoLogin('admin'); + + $request = new HTTP\Request( + 'GET', + '/calendars/admin/UUID-123467?export' + ); + + $response = $this->request($request, 200); + $this->assertEquals('text/calendar', $response->getHeader('Content-Type')); + + $obj = VObject\Reader::read($response->body); + + $this->assertEquals(8, count($obj->children())); + $this->assertEquals(1, count($obj->VERSION)); + $this->assertEquals(1, count($obj->CALSCALE)); + $this->assertEquals(1, count($obj->PRODID)); + $this->assertTrue(strpos((string)$obj->PRODID, DAV\Version::VERSION) !== false); + $this->assertEquals(1, count($obj->VTIMEZONE)); + $this->assertEquals(1, count($obj->VEVENT)); + + } + + function testBadStartParam() { + + $request = new HTTP\Request( + 'GET', + '/calendars/admin/UUID-123467?export&start=foo' + ); + $this->request($request, 400); + + } + + function testBadEndParam() { + + $request = new HTTP\Request( + 'GET', + '/calendars/admin/UUID-123467?export&end=foo' + ); + $this->request($request, 400); + + } + + function testFilterStartEnd() { + + $request = new HTTP\Request( + 'GET', + '/calendars/admin/UUID-123467?export&start=1&end=2' + ); + $response = $this->request($request, 200); + + $obj = VObject\Reader::read($response->getBody()); + + $this->assertEquals(0, count($obj->VTIMEZONE)); + $this->assertEquals(0, count($obj->VEVENT)); + + } + + function testExpandNoStart() { + + $request = new HTTP\Request( + 'GET', + '/calendars/admin/UUID-123467?export&expand=1&end=2' + ); + $this->request($request, 400); + + } + + function testExpand() { + + $request = new HTTP\Request( + 'GET', + '/calendars/admin/UUID-123467?export&start=1&end=2000000000&expand=1' + ); + $response = $this->request($request, 200); + + $obj = VObject\Reader::read($response->getBody()); + + $this->assertEquals(0, count($obj->VTIMEZONE)); + $this->assertEquals(1, count($obj->VEVENT)); + + } + + function testJCal() { + + $request = new HTTP\Request( + 'GET', + '/calendars/admin/UUID-123467?export', + ['Accept' => 'application/calendar+json'] + ); + + $response = $this->request($request, 200); + $this->assertEquals('application/calendar+json', $response->getHeader('Content-Type')); + + } + + function testJCalInUrl() { + + $request = new HTTP\Request( + 'GET', + '/calendars/admin/UUID-123467?export&accept=jcal' + ); + + $response = $this->request($request, 200); + $this->assertEquals('application/calendar+json', $response->getHeader('Content-Type')); + + } + + function testNegotiateDefault() { + + $request = new HTTP\Request( + 'GET', + '/calendars/admin/UUID-123467?export', + ['Accept' => 'text/plain'] + ); + + $response = $this->request($request, 200); + $this->assertEquals('text/calendar', $response->getHeader('Content-Type')); + + } + + function testFilterComponentVEVENT() { + + $request = new HTTP\Request( + 'GET', + '/calendars/admin/UUID-123467?export&componentType=VEVENT' + ); + + $response = $this->request($request, 200); + + $obj = VObject\Reader::read($response->body); + $this->assertEquals(1, count($obj->VTIMEZONE)); + $this->assertEquals(1, count($obj->VEVENT)); + $this->assertEquals(0, count($obj->VTODO)); + + } + + function testFilterComponentVTODO() { + + $request = new HTTP\Request( + 'GET', + '/calendars/admin/UUID-123467?export&componentType=VTODO' + ); + + $response = $this->request($request, 200); + + $obj = VObject\Reader::read($response->body); + + $this->assertEquals(0, count($obj->VTIMEZONE)); + $this->assertEquals(0, count($obj->VEVENT)); + $this->assertEquals(1, count($obj->VTODO)); + + } + + function testFilterComponentBadComponent() { + + $request = new HTTP\Request( + 'GET', + '/calendars/admin/UUID-123467?export&componentType=VVOODOO' + ); + + $response = $this->request($request, 400); + + } + + function testContentDisposition() { + + $request = new HTTP\Request( + 'GET', + '/calendars/admin/UUID-123467?export' + ); + + $response = $this->request($request, 200); + $this->assertEquals('text/calendar', $response->getHeader('Content-Type')); + $this->assertEquals( + 'attachment; filename="UUID-123467-' . date('Y-m-d') . '.ics"', + $response->getHeader('Content-Disposition') + ); + + } + + function testContentDispositionJson() { + + $request = new HTTP\Request( + 'GET', + '/calendars/admin/UUID-123467?export', + ['Accept' => 'application/calendar+json'] + ); + + $response = $this->request($request, 200); + $this->assertEquals('application/calendar+json', $response->getHeader('Content-Type')); + $this->assertEquals( + 'attachment; filename="UUID-123467-' . date('Y-m-d') . '.json"', + $response->getHeader('Content-Disposition') + ); + + } + + function testContentDispositionBadChars() { + + $this->caldavBackend->createCalendar( + 'principals/admin', + 'UUID-b_ad"(ch)ars', + [ + '{DAV:}displayname' => 'Test bad characters', + '{http://apple.com/ns/ical/}calendar-color' => '#AA0000FF', + ] + ); + + $request = new HTTP\Request( + 'GET', + '/calendars/admin/UUID-b_ad"(ch)ars?export', + ['Accept' => 'application/calendar+json'] + ); + + $response = $this->request($request, 200); + $this->assertEquals('application/calendar+json', $response->getHeader('Content-Type')); + $this->assertEquals( + 'attachment; filename="UUID-b_adchars-' . date('Y-m-d') . '.json"', + $response->getHeader('Content-Disposition') + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Issue166Test.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Issue166Test.php new file mode 100644 index 00000000000..a1a9b7c0443 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Issue166Test.php @@ -0,0 +1,63 @@ + 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => 'VEVENT', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => [ + 'start' => new \DateTime('2011-12-01'), + 'end' => new \DateTime('2012-02-01'), + ], + ], + ], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]; + $input = VObject\Reader::read($input); + $this->assertTrue($validator->validate($input, $filters)); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Issue172Test.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Issue172Test.php new file mode 100644 index 00000000000..e2b85c2bcdd --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Issue172Test.php @@ -0,0 +1,135 @@ + 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => 'VEVENT', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => [ + 'start' => new \DateTime('2012-01-18 21:00:00 GMT-08:00'), + 'end' => new \DateTime('2012-01-18 21:00:00 GMT-08:00'), + ], + ], + ], + 'prop-filters' => [], + ]; + $input = VObject\Reader::read($input); + $this->assertTrue($validator->validate($input, $filters)); + } + + // Pacific Standard Time, translates to America/Los_Angeles (GMT-8 in January) + function testOutlookTimezoneName() { + $input = << 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => 'VEVENT', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => [ + 'start' => new \DateTime('2012-01-13 10:30:00 GMT-08:00'), + 'end' => new \DateTime('2012-01-13 10:30:00 GMT-08:00'), + ], + ], + ], + 'prop-filters' => [], + ]; + $input = VObject\Reader::read($input); + $this->assertTrue($validator->validate($input, $filters)); + } + + // X-LIC-LOCATION, translates to America/Los_Angeles (GMT-8 in January) + function testLibICalLocationName() { + $input = << 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => 'VEVENT', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => [ + 'start' => new \DateTime('2012-01-13 10:30:00 GMT-08:00'), + 'end' => new \DateTime('2012-01-13 10:30:00 GMT-08:00'), + ], + ], + ], + 'prop-filters' => [], + ]; + $input = VObject\Reader::read($input); + $this->assertTrue($validator->validate($input, $filters)); + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Issue203Test.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Issue203Test.php new file mode 100644 index 00000000000..369e9a70c12 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Issue203Test.php @@ -0,0 +1,137 @@ + 1, + 'name' => 'Calendar', + 'principaluri' => 'principals/user1', + 'uri' => 'calendar1', + ] + ]; + + protected $caldavCalendarObjects = [ + 1 => [ + 'event.ics' => [ + 'calendardata' => 'BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:20120330T155305CEST-6585fBUVgV +DTSTAMP:20120330T135305Z +DTSTART;TZID=Europe/Berlin:20120326T155200 +DTEND;TZID=Europe/Berlin:20120326T165200 +RRULE:FREQ=DAILY;COUNT=2;INTERVAL=1 +SUMMARY:original summary +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +UID:20120330T155305CEST-6585fBUVgV +DTSTAMP:20120330T135352Z +DESCRIPTION: +DTSTART;TZID=Europe/Berlin:20120328T155200 +DTEND;TZID=Europe/Berlin:20120328T165200 +RECURRENCE-ID;TZID=Europe/Berlin:20120327T155200 +SEQUENCE:1 +SUMMARY:overwritten summary +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR +', + ], + ], + ]; + + function testIssue203() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'REPORT', + 'HTTP_CONTENT_TYPE' => 'application/xml', + 'REQUEST_URI' => '/calendars/user1/calendar1', + 'HTTP_DEPTH' => '1', + ]); + + $request->setBody(' + + + + + + + + + + + + + + +'); + + $response = $this->request($request); + + // Everts super awesome xml parser. + $body = substr( + $response->body, + $start = strpos($response->body, 'BEGIN:VCALENDAR'), + strpos($response->body, 'END:VCALENDAR') - $start + 13 + ); + $body = str_replace(' ', '', $body); + + $vObject = VObject\Reader::read($body); + + $this->assertEquals(2, count($vObject->VEVENT)); + + + $expectedEvents = [ + [ + 'DTSTART' => '20120326T135200Z', + 'DTEND' => '20120326T145200Z', + 'SUMMARY' => 'original summary', + ], + [ + 'DTSTART' => '20120328T135200Z', + 'DTEND' => '20120328T145200Z', + 'SUMMARY' => 'overwritten summary', + 'RECURRENCE-ID' => '20120327T135200Z', + ] + ]; + + // try to match agains $expectedEvents array + foreach ($expectedEvents as $expectedEvent) { + + $matching = false; + + foreach ($vObject->VEVENT as $vevent) { + /** @var $vevent Sabre\VObject\Component\VEvent */ + foreach ($vevent->children() as $child) { + /** @var $child Sabre\VObject\Property */ + if (isset($expectedEvent[$child->name])) { + if ($expectedEvent[$child->name] != $child->getValue()) { + continue 2; + } + } + } + + $matching = true; + break; + } + + $this->assertTrue($matching, 'Did not find the following event in the response: ' . var_export($expectedEvent, true)); + } + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Issue205Test.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Issue205Test.php new file mode 100644 index 00000000000..ce40a90b035 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Issue205Test.php @@ -0,0 +1,98 @@ + 1, + 'name' => 'Calendar', + 'principaluri' => 'principals/user1', + 'uri' => 'calendar1', + ] + ]; + + protected $caldavCalendarObjects = [ + 1 => [ + 'event.ics' => [ + 'calendardata' => 'BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:20120330T155305CEST-6585fBUVgV +DTSTAMP:20120330T135305Z +DTSTART;TZID=Europe/Berlin:20120326T155200 +DTEND;TZID=Europe/Berlin:20120326T165200 +SUMMARY:original summary +TRANSP:OPAQUE +BEGIN:VALARM +ACTION:AUDIO +ATTACH;VALUE=URI:Basso +TRIGGER:PT0S +END:VALARM +END:VEVENT +END:VCALENDAR +', + ], + ], + ]; + + function testIssue205() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'REPORT', + 'HTTP_CONTENT_TYPE' => 'application/xml', + 'REQUEST_URI' => '/calendars/user1/calendar1', + 'HTTP_DEPTH' => '1', + ]); + + $request->setBody(' + + + + + + + + + + + + + + + + +'); + + $response = $this->request($request); + + $this->assertFalse(strpos($response->body, 'Exception'), 'Exception occurred: ' . $response->body); + $this->assertFalse(strpos($response->body, 'Unknown or bad format'), 'DateTime unknown format Exception: ' . $response->body); + + // Everts super awesome xml parser. + $body = substr( + $response->body, + $start = strpos($response->body, 'BEGIN:VCALENDAR'), + strpos($response->body, 'END:VCALENDAR') - $start + 13 + ); + $body = str_replace(' ', '', $body); + + $vObject = VObject\Reader::read($body); + + $this->assertEquals(1, count($vObject->VEVENT)); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Issue211Test.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Issue211Test.php new file mode 100644 index 00000000000..950629fd813 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Issue211Test.php @@ -0,0 +1,89 @@ + 1, + 'name' => 'Calendar', + 'principaluri' => 'principals/user1', + 'uri' => 'calendar1', + ] + ]; + + protected $caldavCalendarObjects = [ + 1 => [ + 'event.ics' => [ + 'calendardata' => 'BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:20120418T172519CEST-3510gh1hVw +DTSTAMP:20120418T152519Z +DTSTART;VALUE=DATE:20120330 +DTEND;VALUE=DATE:20120531 +EXDATE;TZID=Europe/Berlin:20120330T000000 +RRULE:FREQ=YEARLY;INTERVAL=1 +SEQUENCE:1 +SUMMARY:Birthday +TRANSP:TRANSPARENT +BEGIN:VALARM +ACTION:EMAIL +ATTENDEE:MAILTO:xxx@domain.de +DESCRIPTION:Dies ist eine Kalender Erinnerung +SUMMARY:Kalender Alarm Erinnerung +TRIGGER;VALUE=DATE-TIME:20120329T060000Z +END:VALARM +END:VEVENT +END:VCALENDAR +', + ], + ], + ]; + + function testIssue211() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'REPORT', + 'HTTP_CONTENT_TYPE' => 'application/xml', + 'REQUEST_URI' => '/calendars/user1/calendar1', + 'HTTP_DEPTH' => '1', + ]); + + $request->setBody(' + + + + + + + + + + + + + + +'); + + $response = $this->request($request); + + // if this assert is reached, the endless loop is gone + // There should be no matching events + $this->assertFalse(strpos('BEGIN:VEVENT', $response->body)); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Issue220Test.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Issue220Test.php new file mode 100644 index 00000000000..c3c0b5b48a8 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Issue220Test.php @@ -0,0 +1,99 @@ + 1, + 'name' => 'Calendar', + 'principaluri' => 'principals/user1', + 'uri' => 'calendar1', + ] + ]; + + protected $caldavCalendarObjects = [ + 1 => [ + 'event.ics' => [ + 'calendardata' => 'BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +DTSTART;TZID=Europe/Berlin:20120601T180000 +SUMMARY:Brot backen +RRULE:FREQ=DAILY;INTERVAL=1;WKST=MO +TRANSP:OPAQUE +DURATION:PT20M +LAST-MODIFIED:20120601T064634Z +CREATED:20120601T064634Z +DTSTAMP:20120601T064634Z +UID:b64f14c5-dccc-4eda-947f-bdb1f763fbcd +BEGIN:VALARM +TRIGGER;VALUE=DURATION:-PT5M +ACTION:DISPLAY +DESCRIPTION:Default Event Notification +X-WR-ALARMUID:cd952c1b-b3d6-41fb-b0a6-ec3a1a5bdd58 +END:VALARM +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=Europe/Berlin:20120606T180000 +SUMMARY:Brot backen +TRANSP:OPAQUE +STATUS:CANCELLED +DTEND;TZID=Europe/Berlin:20120606T182000 +LAST-MODIFIED:20120605T094310Z +SEQUENCE:1 +RECURRENCE-ID:20120606T160000Z +UID:b64f14c5-dccc-4eda-947f-bdb1f763fbcd +END:VEVENT +END:VCALENDAR +', + ], + ], + ]; + + function testIssue220() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'REPORT', + 'HTTP_CONTENT_TYPE' => 'application/xml', + 'REQUEST_URI' => '/calendars/user1/calendar1', + 'HTTP_DEPTH' => '1', + ]); + + $request->setBody(' + + + + + + + + + + + + + + +'); + + $response = $this->request($request); + + $this->assertFalse(strpos($response->body, 'PHPUnit_Framework_Error_Warning'), 'Error Warning occurred: ' . $response->body); + $this->assertFalse(strpos($response->body, 'Invalid argument supplied for foreach()'), 'Invalid argument supplied for foreach(): ' . $response->body); + + $this->assertEquals(207, $response->status); + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Issue228Test.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Issue228Test.php new file mode 100644 index 00000000000..d0783701de9 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Issue228Test.php @@ -0,0 +1,79 @@ + 1, + 'name' => 'Calendar', + 'principaluri' => 'principals/user1', + 'uri' => 'calendar1', + ] + ]; + + protected $caldavCalendarObjects = [ + 1 => [ + 'event.ics' => [ + 'calendardata' => 'BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:20120730T113415CEST-6804EGphkd@xxxxxx.de +DTSTAMP:20120730T093415Z +DTSTART;VALUE=DATE:20120729 +DTEND;VALUE=DATE:20120730 +SUMMARY:sunday event +TRANSP:TRANSPARENT +END:VEVENT +END:VCALENDAR +', + ], + ], + ]; + + function testIssue228() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'REPORT', + 'HTTP_CONTENT_TYPE' => 'application/xml', + 'REQUEST_URI' => '/calendars/user1/calendar1', + 'HTTP_DEPTH' => '1', + ]); + + $request->setBody(' + + + + + + + + + + + + + + +'); + + $response = $this->request($request); + + // We must check if absolutely nothing was returned from this query. + $this->assertFalse(strpos($response->body, 'BEGIN:VCALENDAR')); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/JCalTransformTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/JCalTransformTest.php new file mode 100644 index 00000000000..f1eed177597 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/JCalTransformTest.php @@ -0,0 +1,262 @@ + 1, + 'principaluri' => 'principals/user1', + 'uri' => 'foo', + ] + ]; + protected $caldavCalendarObjects = [ + 1 => [ + 'bar.ics' => [ + 'uri' => 'bar.ics', + 'calendarid' => 1, + 'calendardata' => "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", + 'lastmodified' => null + ] + ], + ]; + + function testGet() { + + $headers = [ + 'Accept' => 'application/calendar+json', + ]; + $request = new Request('GET', '/calendars/user1/foo/bar.ics', $headers); + + $response = $this->request($request); + + $body = $response->getBodyAsString(); + $this->assertEquals(200, $response->getStatus(), "Incorrect status code: " . $body); + + $response = json_decode($body, true); + if (json_last_error() !== JSON_ERROR_NONE) { + $this->fail('Json decoding error: ' . json_last_error_msg()); + } + $this->assertEquals( + [ + 'vcalendar', + [], + [ + [ + 'vevent', + [], + [], + ], + ], + ], + $response + ); + + } + + function testMultiGet() { + + $xml = << + + + + + /calendars/user1/foo/bar.ics + +XML; + + $headers = []; + $request = new Request('REPORT', '/calendars/user1/foo', $headers, $xml); + + $response = $this->request($request); + + $this->assertEquals(207, $response->getStatus(), 'Full rsponse: ' . $response->getBodyAsString()); + + $multiStatus = $this->server->xml->parse( + $response->getBodyAsString() + ); + + $responses = $multiStatus->getResponses(); + $this->assertEquals(1, count($responses)); + + $response = $responses[0]->getResponseProperties()[200]["{urn:ietf:params:xml:ns:caldav}calendar-data"]; + + $jresponse = json_decode($response, true); + if (json_last_error()) { + $this->fail('Json decoding error: ' . json_last_error_msg() . '. Full response: ' . $response); + } + $this->assertEquals( + [ + 'vcalendar', + [], + [ + [ + 'vevent', + [], + [], + ], + ], + ], + $jresponse + ); + + } + + function testCalendarQueryDepth1() { + + $xml = << + + + + + + + + +XML; + + $headers = [ + 'Depth' => '1', + ]; + $request = new Request('REPORT', '/calendars/user1/foo', $headers, $xml); + + $response = $this->request($request); + + $this->assertEquals(207, $response->getStatus(), "Invalid response code. Full body: " . $response->getBodyAsString()); + + $multiStatus = $this->server->xml->parse( + $response->getBodyAsString() + ); + + $responses = $multiStatus->getResponses(); + + $this->assertEquals(1, count($responses)); + + $response = $responses[0]->getResponseProperties()[200]["{urn:ietf:params:xml:ns:caldav}calendar-data"]; + $response = json_decode($response, true); + if (json_last_error()) { + $this->fail('Json decoding error: ' . json_last_error_msg()); + } + $this->assertEquals( + [ + 'vcalendar', + [], + [ + [ + 'vevent', + [], + [], + ], + ], + ], + $response + ); + + } + + function testCalendarQueryDepth0() { + + $xml = << + + + + + + + + +XML; + + $headers = [ + 'Depth' => '0', + ]; + $request = new Request('REPORT', '/calendars/user1/foo/bar.ics', $headers, $xml); + + $response = $this->request($request); + + $this->assertEquals(207, $response->getStatus(), "Invalid response code. Full body: " . $response->getBodyAsString()); + + $multiStatus = $this->server->xml->parse( + $response->getBodyAsString() + ); + + $responses = $multiStatus->getResponses(); + + $this->assertEquals(1, count($responses)); + + $response = $responses[0]->getResponseProperties()[200]["{urn:ietf:params:xml:ns:caldav}calendar-data"]; + $response = json_decode($response, true); + if (json_last_error()) { + $this->fail('Json decoding error: ' . json_last_error_msg()); + } + $this->assertEquals( + [ + 'vcalendar', + [], + [ + [ + 'vevent', + [], + [], + ], + ], + ], + $response + ); + + } + + function testValidateICalendar() { + + $input = [ + 'vcalendar', + [], + [ + [ + 'vevent', + [ + ['uid', (object)[], 'text', 'foo'], + ['dtstart', (object)[], 'date', '2016-04-06'], + ], + [], + ], + ], + ]; + $input = json_encode($input); + $this->caldavPlugin->beforeWriteContent( + 'calendars/user1/foo/bar.ics', + $this->server->tree->getNodeForPath('calendars/user1/foo/bar.ics'), + $input, + $modified + ); + + + $expected = <<assertVObjectEqualsVObject( + $expected, + $input + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Notifications/CollectionTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Notifications/CollectionTest.php new file mode 100644 index 00000000000..6585f85c379 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Notifications/CollectionTest.php @@ -0,0 +1,85 @@ +principalUri = 'principals/user1'; + + $this->notification = new CalDAV\Xml\Notification\SystemStatus(1, '"1"'); + + $this->caldavBackend = new CalDAV\Backend\MockSharing([], [], [ + 'principals/user1' => [ + $this->notification + ] + ]); + + return new Collection($this->caldavBackend, $this->principalUri); + + } + + function testGetChildren() { + + $col = $this->getInstance(); + $this->assertEquals('notifications', $col->getName()); + + $this->assertEquals([ + new Node($this->caldavBackend, $this->principalUri, $this->notification) + ], $col->getChildren()); + + } + + function testGetOwner() { + + $col = $this->getInstance(); + $this->assertEquals('principals/user1', $col->getOwner()); + + } + + function testGetGroup() { + + $col = $this->getInstance(); + $this->assertNull($col->getGroup()); + + } + + function testGetACL() { + + $col = $this->getInstance(); + $expected = [ + [ + 'privilege' => '{DAV:}all', + 'principal' => '{DAV:}owner', + 'protected' => true, + ], + ]; + + $this->assertEquals($expected, $col->getACL()); + + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + */ + function testSetACL() { + + $col = $this->getInstance(); + $col->setACL([]); + + } + + function testGetSupportedPrivilegeSet() { + + $col = $this->getInstance(); + $this->assertNull($col->getSupportedPrivilegeSet()); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Notifications/NodeTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Notifications/NodeTest.php new file mode 100644 index 00000000000..6c6e02da81a --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Notifications/NodeTest.php @@ -0,0 +1,96 @@ +systemStatus = new CalDAV\Xml\Notification\SystemStatus(1, '"1"'); + + $this->caldavBackend = new CalDAV\Backend\MockSharing([], [], [ + 'principals/user1' => [ + $this->systemStatus + ] + ]); + + $node = new Node($this->caldavBackend, 'principals/user1', $this->systemStatus); + return $node; + + } + + function testGetId() { + + $node = $this->getInstance(); + $this->assertEquals($this->systemStatus->getId() . '.xml', $node->getName()); + + } + + function testGetEtag() { + + $node = $this->getInstance(); + $this->assertEquals('"1"', $node->getETag()); + + } + + function testGetNotificationType() { + + $node = $this->getInstance(); + $this->assertEquals($this->systemStatus, $node->getNotificationType()); + + } + + function testDelete() { + + $node = $this->getInstance(); + $node->delete(); + $this->assertEquals([], $this->caldavBackend->getNotificationsForPrincipal('principals/user1')); + + } + + function testGetGroup() { + + $node = $this->getInstance(); + $this->assertNull($node->getGroup()); + + } + + function testGetACL() { + + $node = $this->getInstance(); + $expected = [ + [ + 'privilege' => '{DAV:}all', + 'principal' => '{DAV:}owner', + 'protected' => true, + ], + ]; + + $this->assertEquals($expected, $node->getACL()); + + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + */ + function testSetACL() { + + $node = $this->getInstance(); + $node->setACL([]); + + } + + function testGetSupportedPrivilegeSet() { + + $node = $this->getInstance(); + $this->assertNull($node->getSupportedPrivilegeSet()); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Notifications/PluginTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Notifications/PluginTest.php new file mode 100644 index 00000000000..73f256c98e7 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Notifications/PluginTest.php @@ -0,0 +1,168 @@ +caldavBackend = new CalDAV\Backend\MockSharing(); + $principalBackend = new DAVACL\PrincipalBackend\Mock(); + $calendars = new CalDAV\CalendarRoot($principalBackend, $this->caldavBackend); + $principals = new CalDAV\Principal\Collection($principalBackend); + + $root = new DAV\SimpleCollection('root'); + $root->addChild($calendars); + $root->addChild($principals); + + $this->server = new DAV\Server($root); + $this->server->sapi = new HTTP\SapiMock(); + $this->server->debugExceptions = true; + $this->server->setBaseUri('/'); + $this->plugin = new Plugin(); + $this->server->addPlugin($this->plugin); + + + // Adding ACL plugin + $aclPlugin = new DAVACL\Plugin(); + $aclPlugin->allowUnauthenticatedAccess = false; + $this->server->addPlugin($aclPlugin); + + // CalDAV is also required. + $this->server->addPlugin(new CalDAV\Plugin()); + // Adding Auth plugin, and ensuring that we are logged in. + $authBackend = new DAV\Auth\Backend\Mock(); + $authPlugin = new DAV\Auth\Plugin($authBackend); + $this->server->addPlugin($authPlugin); + + // This forces a login + $authPlugin->beforeMethod(new HTTP\Request(), new HTTP\Response()); + + $this->response = new HTTP\ResponseMock(); + $this->server->httpResponse = $this->response; + + } + + function testSimple() { + + $this->assertEquals([], $this->plugin->getFeatures()); + $this->assertEquals('notifications', $this->plugin->getPluginName()); + $this->assertEquals( + 'notifications', + $this->plugin->getPluginInfo()['name'] + ); + + } + + function testPrincipalProperties() { + + $httpRequest = new Request('GET', '/', ['Host' => 'sabredav.org']); + $this->server->httpRequest = $httpRequest; + + $props = $this->server->getPropertiesForPath('principals/admin', [ + '{' . Plugin::NS_CALENDARSERVER . '}notification-URL', + ]); + + $this->assertArrayHasKey(0, $props); + $this->assertArrayHasKey(200, $props[0]); + + $this->assertArrayHasKey('{' . Plugin::NS_CALENDARSERVER . '}notification-URL', $props[0][200]); + $prop = $props[0][200]['{' . Plugin::NS_CALENDARSERVER . '}notification-URL']; + $this->assertTrue($prop instanceof DAV\Xml\Property\Href); + $this->assertEquals('calendars/admin/notifications/', $prop->getHref()); + + } + + function testNotificationProperties() { + + $notification = new Node( + $this->caldavBackend, + 'principals/user1', + new SystemStatus('foo', '"1"') + ); + $propFind = new DAV\PropFind('calendars/user1/notifications', [ + '{' . Plugin::NS_CALENDARSERVER . '}notificationtype', + ]); + + $this->plugin->propFind($propFind, $notification); + + $this->assertEquals( + $notification->getNotificationType(), + $propFind->get('{' . Plugin::NS_CALENDARSERVER . '}notificationtype') + ); + + } + + function testNotificationGet() { + + $notification = new Node( + $this->caldavBackend, + 'principals/user1', + new SystemStatus('foo', '"1"') + ); + + $server = new DAV\Server([$notification]); + $caldav = new Plugin(); + + $server->httpRequest = new Request('GET', '/foo.xml'); + $httpResponse = new HTTP\ResponseMock(); + $server->httpResponse = $httpResponse; + + $server->addPlugin($caldav); + + $caldav->httpGet($server->httpRequest, $server->httpResponse); + + $this->assertEquals(200, $httpResponse->status); + $this->assertEquals([ + 'Content-Type' => ['application/xml'], + 'ETag' => ['"1"'], + ], $httpResponse->getHeaders()); + + $expected = +' + + + +'; + + $this->assertXmlStringEqualsXmlString($expected, $httpResponse->getBodyAsString()); + + } + + function testGETPassthrough() { + + $server = new DAV\Server(); + $caldav = new Plugin(); + + $httpResponse = new HTTP\ResponseMock(); + $server->httpResponse = $httpResponse; + + $server->addPlugin($caldav); + + $this->assertNull($caldav->httpGet(new HTTP\Request('GET', '/foozz'), $server->httpResponse)); + + } + + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/PluginTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/PluginTest.php new file mode 100644 index 00000000000..859f6aa0c4e --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/PluginTest.php @@ -0,0 +1,1086 @@ +caldavBackend = new Backend\Mock([ + [ + 'id' => 1, + 'uri' => 'UUID-123467', + 'principaluri' => 'principals/user1', + '{DAV:}displayname' => 'user1 calendar', + $caldavNS . 'calendar-description' => 'Calendar description', + '{http://apple.com/ns/ical/}calendar-order' => '1', + '{http://apple.com/ns/ical/}calendar-color' => '#FF0000', + $caldavNS . 'supported-calendar-component-set' => new Xml\Property\SupportedCalendarComponentSet(['VEVENT', 'VTODO']), + ], + [ + 'id' => 2, + 'uri' => 'UUID-123468', + 'principaluri' => 'principals/user1', + '{DAV:}displayname' => 'user1 calendar2', + $caldavNS . 'calendar-description' => 'Calendar description', + '{http://apple.com/ns/ical/}calendar-order' => '1', + '{http://apple.com/ns/ical/}calendar-color' => '#FF0000', + $caldavNS . 'supported-calendar-component-set' => new Xml\Property\SupportedCalendarComponentSet(['VEVENT', 'VTODO']), + ] + ], [ + 1 => [ + 'UUID-2345' => [ + 'calendardata' => TestUtil::getTestCalendarData(), + ] + ] + ]); + $principalBackend = new DAVACL\PrincipalBackend\Mock(); + $principalBackend->setGroupMemberSet('principals/admin/calendar-proxy-read', ['principals/user1']); + $principalBackend->setGroupMemberSet('principals/admin/calendar-proxy-write', ['principals/user1']); + $principalBackend->addPrincipal([ + 'uri' => 'principals/admin/calendar-proxy-read', + ]); + $principalBackend->addPrincipal([ + 'uri' => 'principals/admin/calendar-proxy-write', + ]); + + $calendars = new CalendarRoot($principalBackend, $this->caldavBackend); + $principals = new Principal\Collection($principalBackend); + + $root = new DAV\SimpleCollection('root'); + $root->addChild($calendars); + $root->addChild($principals); + + $this->server = new DAV\Server($root); + $this->server->sapi = new HTTP\SapiMock(); + $this->server->debugExceptions = true; + $this->server->setBaseUri('/'); + $this->plugin = new Plugin(); + $this->server->addPlugin($this->plugin); + + // Adding ACL plugin + $aclPlugin = new DAVACL\Plugin(); + $aclPlugin->allowUnauthenticatedAccess = false; + $this->server->addPlugin($aclPlugin); + + // Adding Auth plugin, and ensuring that we are logged in. + $authBackend = new DAV\Auth\Backend\Mock(); + $authBackend->setPrincipal('principals/user1'); + $authPlugin = new DAV\Auth\Plugin($authBackend); + $authPlugin->beforeMethod(new \Sabre\HTTP\Request(), new \Sabre\HTTP\Response()); + $this->server->addPlugin($authPlugin); + + // This forces a login + $authPlugin->beforeMethod(new HTTP\Request(), new HTTP\Response()); + + $this->response = new HTTP\ResponseMock(); + $this->server->httpResponse = $this->response; + + } + + function testSimple() { + + $this->assertEquals(['MKCALENDAR'], $this->plugin->getHTTPMethods('calendars/user1/randomnewcalendar')); + $this->assertEquals(['calendar-access', 'calendar-proxy'], $this->plugin->getFeatures()); + $this->assertEquals( + 'caldav', + $this->plugin->getPluginInfo()['name'] + ); + + } + + function testUnknownMethodPassThrough() { + + $request = new HTTP\Request('MKBREAKFAST', '/'); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(501, $this->response->status, 'Incorrect status returned. Full response body:' . $this->response->body); + + } + + function testReportPassThrough() { + + $request = new HTTP\Request('REPORT', '/', ['Content-Type' => 'application/xml']); + $request->setBody(''); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(415, $this->response->status); + + } + + function testMkCalendarBadLocation() { + + $request = new HTTP\Request('MKCALENDAR', '/blabla'); + + $body = ' + + + + Lisa\'s Events + Calendar restricted to events. + + + + + + + '; + + $request->setBody($body); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(403, $this->response->status); + + } + + function testMkCalendarNoParentNode() { + + $request = new HTTP\Request('MKCALENDAR', '/doesntexist/calendar'); + + $body = ' + + + + Lisa\'s Events + Calendar restricted to events. + + + + + + + '; + + $request->setBody($body); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(409, $this->response->status); + + } + + function testMkCalendarExistingCalendar() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'MKCALENDAR', + 'REQUEST_URI' => '/calendars/user1/UUID-123467', + ]); + + $body = ' + + + + Lisa\'s Events + Calendar restricted to events. + + + + + + + '; + + $request->setBody($body); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(405, $this->response->status); + + } + + function testMkCalendarSucceed() { + + $request = new HTTP\Request('MKCALENDAR', '/calendars/user1/NEWCALENDAR'); + + $timezone = 'BEGIN:VCALENDAR +PRODID:-//Example Corp.//CalDAV Client//EN +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:US-Eastern +LAST-MODIFIED:19870101T000000Z +BEGIN:STANDARD +DTSTART:19671029T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +TZNAME:Eastern Standard Time (US & Canada) +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:19870405T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +TZNAME:Eastern Daylight Time (US & Canada) +END:DAYLIGHT +END:VTIMEZONE +END:VCALENDAR'; + + $body = ' + + + + Lisa\'s Events + Calendar restricted to events. + + + + + + + '; + + $request->setBody($body); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(201, $this->response->status, 'Invalid response code received. Full response body: ' . $this->response->body); + + $calendars = $this->caldavBackend->getCalendarsForUser('principals/user1'); + $this->assertEquals(3, count($calendars)); + + $newCalendar = null; + foreach ($calendars as $calendar) { + if ($calendar['uri'] === 'NEWCALENDAR') { + $newCalendar = $calendar; + break; + } + } + + $this->assertInternalType('array', $newCalendar); + + $keys = [ + 'uri' => 'NEWCALENDAR', + 'id' => null, + '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'Calendar restricted to events.', + '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => $timezone, + '{DAV:}displayname' => 'Lisa\'s Events', + '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => null, + ]; + + foreach ($keys as $key => $value) { + + $this->assertArrayHasKey($key, $newCalendar); + + if (is_null($value)) continue; + $this->assertEquals($value, $newCalendar[$key]); + + } + $sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'; + $this->assertTrue($newCalendar[$sccs] instanceof Xml\Property\SupportedCalendarComponentSet); + $this->assertEquals(['VEVENT'], $newCalendar[$sccs]->getValue()); + + } + + function testMkCalendarEmptyBodySucceed() { + + $request = new HTTP\Request('MKCALENDAR', '/calendars/user1/NEWCALENDAR'); + + $request->setBody(''); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(201, $this->response->status, 'Invalid response code received. Full response body: ' . $this->response->body); + + $calendars = $this->caldavBackend->getCalendarsForUser('principals/user1'); + $this->assertEquals(3, count($calendars)); + + $newCalendar = null; + foreach ($calendars as $calendar) { + if ($calendar['uri'] === 'NEWCALENDAR') { + $newCalendar = $calendar; + break; + } + } + + $this->assertInternalType('array', $newCalendar); + + $keys = [ + 'uri' => 'NEWCALENDAR', + 'id' => null, + '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => null, + ]; + + foreach ($keys as $key => $value) { + + $this->assertArrayHasKey($key, $newCalendar); + + if (is_null($value)) continue; + $this->assertEquals($value, $newCalendar[$key]); + + } + $sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'; + $this->assertTrue($newCalendar[$sccs] instanceof Xml\Property\SupportedCalendarComponentSet); + $this->assertEquals(['VEVENT', 'VTODO'], $newCalendar[$sccs]->getValue()); + + } + + function testMkCalendarBadXml() { + + $request = new HTTP\Request('MKCALENDAR', '/blabla'); + $body = 'This is not xml'; + + $request->setBody($body); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(400, $this->response->status); + + } + + function testPrincipalProperties() { + + $httpRequest = new HTTP\Request('FOO', '/blabla', ['Host' => 'sabredav.org']); + $this->server->httpRequest = $httpRequest; + + $props = $this->server->getPropertiesForPath('/principals/user1', [ + '{' . Plugin::NS_CALDAV . '}calendar-home-set', + '{' . Plugin::NS_CALENDARSERVER . '}calendar-proxy-read-for', + '{' . Plugin::NS_CALENDARSERVER . '}calendar-proxy-write-for', + '{' . Plugin::NS_CALENDARSERVER . '}notification-URL', + '{' . Plugin::NS_CALENDARSERVER . '}email-address-set', + ]); + + $this->assertArrayHasKey(0, $props); + $this->assertArrayHasKey(200, $props[0]); + + + $this->assertArrayHasKey('{urn:ietf:params:xml:ns:caldav}calendar-home-set', $props[0][200]); + $prop = $props[0][200]['{urn:ietf:params:xml:ns:caldav}calendar-home-set']; + $this->assertInstanceOf('Sabre\\DAV\\Xml\\Property\\Href', $prop); + $this->assertEquals('calendars/user1/', $prop->getHref()); + + $this->assertArrayHasKey('{http://calendarserver.org/ns/}calendar-proxy-read-for', $props[0][200]); + $prop = $props[0][200]['{http://calendarserver.org/ns/}calendar-proxy-read-for']; + $this->assertInstanceOf('Sabre\\DAV\\Xml\\Property\\Href', $prop); + $this->assertEquals(['principals/admin/'], $prop->getHrefs()); + + $this->assertArrayHasKey('{http://calendarserver.org/ns/}calendar-proxy-write-for', $props[0][200]); + $prop = $props[0][200]['{http://calendarserver.org/ns/}calendar-proxy-write-for']; + $this->assertInstanceOf('Sabre\\DAV\\Xml\\Property\\Href', $prop); + $this->assertEquals(['principals/admin/'], $prop->getHrefs()); + + $this->assertArrayHasKey('{' . Plugin::NS_CALENDARSERVER . '}email-address-set', $props[0][200]); + $prop = $props[0][200]['{' . Plugin::NS_CALENDARSERVER . '}email-address-set']; + $this->assertInstanceOf('Sabre\\CalDAV\\Xml\\Property\\EmailAddressSet', $prop); + $this->assertEquals(['user1.sabredav@sabredav.org'], $prop->getValue()); + + } + + function testSupportedReportSetPropertyNonCalendar() { + + $props = $this->server->getPropertiesForPath('/calendars/user1', [ + '{DAV:}supported-report-set', + ]); + + $this->assertArrayHasKey(0, $props); + $this->assertArrayHasKey(200, $props[0]); + $this->assertArrayHasKey('{DAV:}supported-report-set', $props[0][200]); + + $prop = $props[0][200]['{DAV:}supported-report-set']; + + $this->assertInstanceOf('\\Sabre\\DAV\\Xml\\Property\\SupportedReportSet', $prop); + $value = [ + '{DAV:}expand-property', + '{DAV:}principal-match', + '{DAV:}principal-property-search', + '{DAV:}principal-search-property-set', + ]; + $this->assertEquals($value, $prop->getValue()); + + } + + /** + * @depends testSupportedReportSetPropertyNonCalendar + */ + function testSupportedReportSetProperty() { + + $props = $this->server->getPropertiesForPath('/calendars/user1/UUID-123467', [ + '{DAV:}supported-report-set', + ]); + + $this->assertArrayHasKey(0, $props); + $this->assertArrayHasKey(200, $props[0]); + $this->assertArrayHasKey('{DAV:}supported-report-set', $props[0][200]); + + $prop = $props[0][200]['{DAV:}supported-report-set']; + + $this->assertInstanceOf('\\Sabre\\DAV\\Xml\\Property\\SupportedReportSet', $prop); + $value = [ + '{urn:ietf:params:xml:ns:caldav}calendar-multiget', + '{urn:ietf:params:xml:ns:caldav}calendar-query', + '{urn:ietf:params:xml:ns:caldav}free-busy-query', + '{DAV:}expand-property', + '{DAV:}principal-match', + '{DAV:}principal-property-search', + '{DAV:}principal-search-property-set' + ]; + $this->assertEquals($value, $prop->getValue()); + + } + + function testSupportedReportSetUserCalendars() { + + $this->server->addPlugin(new \Sabre\DAV\Sync\Plugin()); + + $props = $this->server->getPropertiesForPath('/calendars/user1', [ + '{DAV:}supported-report-set', + ]); + + $this->assertArrayHasKey(0, $props); + $this->assertArrayHasKey(200, $props[0]); + $this->assertArrayHasKey('{DAV:}supported-report-set', $props[0][200]); + + $prop = $props[0][200]['{DAV:}supported-report-set']; + + $this->assertInstanceOf('\\Sabre\\DAV\\Xml\\Property\\SupportedReportSet', $prop); + $value = [ + '{DAV:}sync-collection', + '{DAV:}expand-property', + '{DAV:}principal-match', + '{DAV:}principal-property-search', + '{DAV:}principal-search-property-set', + ]; + $this->assertEquals($value, $prop->getValue()); + + } + + /** + * @depends testSupportedReportSetProperty + */ + function testCalendarMultiGetReport() { + + $body = + '' . + '' . + '' . + ' ' . + ' ' . + '' . + '/calendars/user1/UUID-123467/UUID-2345' . + ''; + + $request = new HTTP\Request('REPORT', '/calendars/user1', ['Depth' => '1']); + $request->setBody($body); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(207, $this->response->status, 'Invalid HTTP status received. Full response body'); + + $expectedIcal = TestUtil::getTestCalendarData(); + + $expected = << + + + /calendars/user1/UUID-123467/UUID-2345 + + + $expectedIcal + "e207e33c10e5fb9c12cfb35b5d9116e1" + + HTTP/1.1 200 OK + + + +XML; + + $this->assertXmlStringEqualsXmlString($expected, $this->response->getBodyAsString()); + + } + + /** + * @depends testCalendarMultiGetReport + */ + function testCalendarMultiGetReportExpand() { + + $body = + '' . + '' . + '' . + ' ' . + ' ' . + ' ' . + ' ' . + '' . + '/calendars/user1/UUID-123467/UUID-2345' . + ''; + + $request = new HTTP\Request('REPORT', '/calendars/user1', ['Depth' => '1']); + $request->setBody($body); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(207, $this->response->status, 'Invalid HTTP status received. Full response body: ' . $this->response->body); + + $expectedIcal = TestUtil::getTestCalendarData(); + $expectedIcal = \Sabre\VObject\Reader::read($expectedIcal); + $expectedIcal = $expectedIcal->expand( + new DateTime('2011-01-01 00:00:00', new DateTimeZone('UTC')), + new DateTime('2011-12-31 23:59:59', new DateTimeZone('UTC')) + ); + $expectedIcal = str_replace("\r\n", " \n", $expectedIcal->serialize()); + + $expected = << + + + /calendars/user1/UUID-123467/UUID-2345 + + + $expectedIcal + "e207e33c10e5fb9c12cfb35b5d9116e1" + + HTTP/1.1 200 OK + + + +XML; + + $this->assertXmlStringEqualsXmlString($expected, $this->response->getBodyAsString()); + + } + + /** + * @depends testSupportedReportSetProperty + * @depends testCalendarMultiGetReport + */ + function testCalendarQueryReport() { + + $body = + '' . + '' . + '' . + ' ' . + ' ' . + ' ' . + ' ' . + '' . + '' . + ' ' . + ' ' . + ' ' . + '' . + ''; + + $request = new HTTP\Request('REPORT', '/calendars/user1/UUID-123467', ['Depth' => '1']); + $request->setBody($body); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(207, $this->response->status, 'Received an unexpected status. Full response body: ' . $this->response->body); + + $expectedIcal = TestUtil::getTestCalendarData(); + $expectedIcal = \Sabre\VObject\Reader::read($expectedIcal); + $expectedIcal = $expectedIcal->expand( + new DateTime('2000-01-01 00:00:00', new DateTimeZone('UTC')), + new DateTime('2010-12-31 23:59:59', new DateTimeZone('UTC')) + ); + $expectedIcal = str_replace("\r\n", " \n", $expectedIcal->serialize()); + + $expected = << + + + /calendars/user1/UUID-123467/UUID-2345 + + + $expectedIcal + "e207e33c10e5fb9c12cfb35b5d9116e1" + + HTTP/1.1 200 OK + + + +XML; + + $this->assertXmlStringEqualsXmlString($expected, $this->response->getBodyAsString()); + + } + + /** + * @depends testSupportedReportSetProperty + * @depends testCalendarMultiGetReport + */ + function testCalendarQueryReportWindowsPhone() { + + $body = + '' . + '' . + '' . + ' ' . + ' ' . + ' ' . + ' ' . + '' . + '' . + ' ' . + ' ' . + ' ' . + '' . + ''; + + $request = new HTTP\Request('REPORT', '/calendars/user1/UUID-123467', [ + 'Depth' => '0', + 'User-Agent' => 'MSFT-WP/8.10.14219 (gzip)', + ]); + + $request->setBody($body); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(207, $this->response->status, 'Received an unexpected status. Full response body: ' . $this->response->body); + + $expectedIcal = TestUtil::getTestCalendarData(); + $expectedIcal = \Sabre\VObject\Reader::read($expectedIcal); + $expectedIcal = $expectedIcal->expand( + new DateTime('2000-01-01 00:00:00', new DateTimeZone('UTC')), + new DateTime('2010-12-31 23:59:59', new DateTimeZone('UTC')) + ); + $expectedIcal = str_replace("\r\n", " \n", $expectedIcal->serialize()); + + $expected = << + + + /calendars/user1/UUID-123467/UUID-2345 + + + $expectedIcal + "e207e33c10e5fb9c12cfb35b5d9116e1" + + HTTP/1.1 200 OK + + + +XML; + + $this->assertXmlStringEqualsXmlString($expected, $this->response->getBodyAsString()); + + } + + /** + * @depends testSupportedReportSetProperty + * @depends testCalendarMultiGetReport + */ + function testCalendarQueryReportBadDepth() { + + $body = + '' . + '' . + '' . + ' ' . + ' ' . + ' ' . + ' ' . + '' . + '' . + ' ' . + ' ' . + ' ' . + '' . + ''; + + $request = new HTTP\Request('REPORT', '/calendars/user1/UUID-123467', [ + 'Depth' => '0', + ]); + $request->setBody($body); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(400, $this->response->status, 'Received an unexpected status. Full response body: ' . $this->response->body); + + } + + /** + * @depends testCalendarQueryReport + */ + function testCalendarQueryReportNoCalData() { + + $body = + '' . + '' . + '' . + ' ' . + '' . + '' . + ' ' . + ' ' . + ' ' . + '' . + ''; + + $request = new HTTP\Request('REPORT', '/calendars/user1/UUID-123467', [ + 'Depth' => '1', + ]); + $request->setBody($body); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(207, $this->response->status, 'Received an unexpected status. Full response body: ' . $this->response->body); + + $expected = << + + + /calendars/user1/UUID-123467/UUID-2345 + + + "e207e33c10e5fb9c12cfb35b5d9116e1" + + HTTP/1.1 200 OK + + + +XML; + + $this->assertXmlStringEqualsXmlString($expected, $this->response->getBodyAsString()); + + } + + /** + * @depends testCalendarQueryReport + */ + function testCalendarQueryReportNoFilters() { + + $body = + '' . + '' . + '' . + ' ' . + ' ' . + '' . + ''; + + $request = new HTTP\Request('REPORT', '/calendars/user1/UUID-123467'); + $request->setBody($body); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(400, $this->response->status, 'Received an unexpected status. Full response body: ' . $this->response->body); + + } + + /** + * @depends testSupportedReportSetProperty + * @depends testCalendarMultiGetReport + */ + function testCalendarQueryReport1Object() { + + $body = + '' . + '' . + '' . + ' ' . + ' ' . + ' ' . + ' ' . + '' . + '' . + ' ' . + ' ' . + ' ' . + '' . + ''; + + $request = new HTTP\Request('REPORT', '/calendars/user1/UUID-123467/UUID-2345', ['Depth' => '0']); + $request->setBody($body); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(207, $this->response->status, 'Received an unexpected status. Full response body: ' . $this->response->body); + + $expectedIcal = TestUtil::getTestCalendarData(); + $expectedIcal = \Sabre\VObject\Reader::read($expectedIcal); + $expectedIcal = $expectedIcal->expand( + new DateTime('2000-01-01 00:00:00', new DateTimeZone('UTC')), + new DateTime('2010-12-31 23:59:59', new DateTimeZone('UTC')) + ); + $expectedIcal = str_replace("\r\n", " \n", $expectedIcal->serialize()); + + $expected = << + + + /calendars/user1/UUID-123467/UUID-2345 + + + $expectedIcal + "e207e33c10e5fb9c12cfb35b5d9116e1" + + HTTP/1.1 200 OK + + + +XML; + + $this->assertXmlStringEqualsXmlString($expected, $this->response->getBodyAsString()); + + } + + /** + * @depends testSupportedReportSetProperty + * @depends testCalendarMultiGetReport + */ + function testCalendarQueryReport1ObjectNoCalData() { + + $body = + '' . + '' . + '' . + ' ' . + '' . + '' . + ' ' . + ' ' . + ' ' . + '' . + ''; + + $request = new HTTP\Request('REPORT', '/calendars/user1/UUID-123467/UUID-2345', ['Depth' => '0']); + $request->setBody($body); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(207, $this->response->status, 'Received an unexpected status. Full response body: ' . $this->response->body); + + $expected = << + + + /calendars/user1/UUID-123467/UUID-2345 + + + "e207e33c10e5fb9c12cfb35b5d9116e1" + + HTTP/1.1 200 OK + + + +XML; + + $this->assertXmlStringEqualsXmlString($expected, $this->response->getBodyAsString()); + + } + + function testHTMLActionsPanel() { + + $output = ''; + $r = $this->server->emit('onHTMLActionsPanel', [$this->server->tree->getNodeForPath('calendars/user1'), &$output]); + $this->assertFalse($r); + + $this->assertTrue(!!strpos($output, 'Display name')); + + } + + /** + * @depends testCalendarMultiGetReport + */ + function testCalendarMultiGetReportNoEnd() { + + $body = + '' . + '' . + '' . + ' ' . + ' ' . + ' ' . + ' ' . + '' . + '/calendars/user1/UUID-123467/UUID-2345' . + ''; + + $request = new HTTP\Request('REPORT', '/calendars/user1', ['Depth' => '1']); + $request->setBody($body); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(400, $this->response->status, 'Invalid HTTP status received. Full response body: ' . $this->response->body); + + } + + /** + * @depends testCalendarMultiGetReport + */ + function testCalendarMultiGetReportNoStart() { + + $body = + '' . + '' . + '' . + ' ' . + ' ' . + ' ' . + ' ' . + '' . + '/calendars/user1/UUID-123467/UUID-2345' . + ''; + + $request = new HTTP\Request('REPORT', '/calendars/user1', ['Depth' => '1']); + $request->setBody($body); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(400, $this->response->status, 'Invalid HTTP status received. Full response body: ' . $this->response->body); + + } + + /** + * @depends testCalendarMultiGetReport + */ + function testCalendarMultiGetReportEndBeforeStart() { + + $body = + '' . + '' . + '' . + ' ' . + ' ' . + ' ' . + ' ' . + '' . + '/calendars/user1/UUID-123467/UUID-2345' . + ''; + + $request = new HTTP\Request('REPORT', '/calendars/user1', ['Depth' => '1']); + $request->setBody($body); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(400, $this->response->status, 'Invalid HTTP status received. Full response body: ' . $this->response->body); + + } + + /** + * @depends testSupportedReportSetPropertyNonCalendar + */ + function testCalendarProperties() { + + $ns = '{urn:ietf:params:xml:ns:caldav}'; + $props = $this->server->getProperties('calendars/user1/UUID-123467', [ + $ns . 'max-resource-size', + $ns . 'supported-calendar-data', + $ns . 'supported-collation-set', + ]); + + $this->assertEquals([ + $ns . 'max-resource-size' => 10000000, + $ns . 'supported-calendar-data' => new Xml\Property\SupportedCalendarData(), + $ns . 'supported-collation-set' => new Xml\Property\SupportedCollationSet(), + ], $props); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Principal/CollectionTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Principal/CollectionTest.php new file mode 100644 index 00000000000..23c2488257f --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Principal/CollectionTest.php @@ -0,0 +1,20 @@ +getChildForPrincipal([ + 'uri' => 'principals/admin', + ]); + $this->assertInstanceOf('Sabre\\CalDAV\\Principal\\User', $r); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Principal/ProxyReadTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Principal/ProxyReadTest.php new file mode 100644 index 00000000000..fe07f013108 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Principal/ProxyReadTest.php @@ -0,0 +1,102 @@ + 'principal/user', + ]); + $this->backend = $backend; + return $principal; + + } + + function testGetName() { + + $i = $this->getInstance(); + $this->assertEquals('calendar-proxy-read', $i->getName()); + + } + function testGetDisplayName() { + + $i = $this->getInstance(); + $this->assertEquals('calendar-proxy-read', $i->getDisplayName()); + + } + + function testGetLastModified() { + + $i = $this->getInstance(); + $this->assertNull($i->getLastModified()); + + } + + /** + * @expectedException Sabre\DAV\Exception\Forbidden + */ + function testDelete() { + + $i = $this->getInstance(); + $i->delete(); + + } + + /** + * @expectedException Sabre\DAV\Exception\Forbidden + */ + function testSetName() { + + $i = $this->getInstance(); + $i->setName('foo'); + + } + + function testGetAlternateUriSet() { + + $i = $this->getInstance(); + $this->assertEquals([], $i->getAlternateUriSet()); + + } + + function testGetPrincipalUri() { + + $i = $this->getInstance(); + $this->assertEquals('principal/user/calendar-proxy-read', $i->getPrincipalUrl()); + + } + + function testGetGroupMemberSet() { + + $i = $this->getInstance(); + $this->assertEquals([], $i->getGroupMemberSet()); + + } + + function testGetGroupMembership() { + + $i = $this->getInstance(); + $this->assertEquals([], $i->getGroupMembership()); + + } + + function testSetGroupMemberSet() { + + $i = $this->getInstance(); + $i->setGroupMemberSet(['principals/foo']); + + $expected = [ + $i->getPrincipalUrl() => ['principals/foo'] + ]; + + $this->assertEquals($expected, $this->backend->groupMembers); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Principal/ProxyWriteTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Principal/ProxyWriteTest.php new file mode 100644 index 00000000000..6cdb9b30e2b --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Principal/ProxyWriteTest.php @@ -0,0 +1,40 @@ + 'principal/user', + ]); + $this->backend = $backend; + return $principal; + + } + + function testGetName() { + + $i = $this->getInstance(); + $this->assertEquals('calendar-proxy-write', $i->getName()); + + } + function testGetDisplayName() { + + $i = $this->getInstance(); + $this->assertEquals('calendar-proxy-write', $i->getDisplayName()); + + } + + function testGetPrincipalUri() { + + $i = $this->getInstance(); + $this->assertEquals('principal/user/calendar-proxy-write', $i->getPrincipalUrl()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Principal/UserTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Principal/UserTest.php new file mode 100644 index 00000000000..420bb3b1afc --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Principal/UserTest.php @@ -0,0 +1,127 @@ +addPrincipal([ + 'uri' => 'principals/user/calendar-proxy-read', + ]); + $backend->addPrincipal([ + 'uri' => 'principals/user/calendar-proxy-write', + ]); + $backend->addPrincipal([ + 'uri' => 'principals/user/random', + ]); + return new User($backend, [ + 'uri' => 'principals/user', + ]); + + } + + /** + * @expectedException Sabre\DAV\Exception\Forbidden + */ + function testCreateFile() { + + $u = $this->getInstance(); + $u->createFile('test'); + + } + + /** + * @expectedException Sabre\DAV\Exception\Forbidden + */ + function testCreateDirectory() { + + $u = $this->getInstance(); + $u->createDirectory('test'); + + } + + function testGetChildProxyRead() { + + $u = $this->getInstance(); + $child = $u->getChild('calendar-proxy-read'); + $this->assertInstanceOf('Sabre\\CalDAV\\Principal\\ProxyRead', $child); + + } + + function testGetChildProxyWrite() { + + $u = $this->getInstance(); + $child = $u->getChild('calendar-proxy-write'); + $this->assertInstanceOf('Sabre\\CalDAV\\Principal\\ProxyWrite', $child); + + } + + /** + * @expectedException Sabre\DAV\Exception\NotFound + */ + function testGetChildNotFound() { + + $u = $this->getInstance(); + $child = $u->getChild('foo'); + + } + + /** + * @expectedException Sabre\DAV\Exception\NotFound + */ + function testGetChildNotFound2() { + + $u = $this->getInstance(); + $child = $u->getChild('random'); + + } + + function testGetChildren() { + + $u = $this->getInstance(); + $children = $u->getChildren(); + $this->assertEquals(2, count($children)); + $this->assertInstanceOf('Sabre\\CalDAV\\Principal\\ProxyRead', $children[0]); + $this->assertInstanceOf('Sabre\\CalDAV\\Principal\\ProxyWrite', $children[1]); + + } + + function testChildExist() { + + $u = $this->getInstance(); + $this->assertTrue($u->childExists('calendar-proxy-read')); + $this->assertTrue($u->childExists('calendar-proxy-write')); + $this->assertFalse($u->childExists('foo')); + + } + + function testGetACL() { + + $expected = [ + [ + 'privilege' => '{DAV:}all', + 'principal' => '{DAV:}owner', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'principals/user/calendar-proxy-read', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'principals/user/calendar-proxy-write', + 'protected' => true, + ], + ]; + + $u = $this->getInstance(); + $this->assertEquals($expected, $u->getACL()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/DeliverNewEventTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/DeliverNewEventTest.php new file mode 100644 index 00000000000..79e323f5c32 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/DeliverNewEventTest.php @@ -0,0 +1,92 @@ +caldavBackend->createCalendar( + 'principals/user1', + 'default', + [ + + ] + ); + $this->caldavBackend->createCalendar( + 'principals/user2', + 'default', + [ + + ] + ); + + } + + function testDelivery() { + + $request = new Request('PUT', '/calendars/user1/default/foo.ics'); + $request->setBody(<<server->on('schedule', function($message) use (&$messages) { + $messages[] = $message; + }); + + $response = $this->request($request); + + $this->assertEquals(201, $response->getStatus(), 'Incorrect status code received. Response body:' . $response->getBodyAsString()); + + $result = $this->request(new Request('GET', '/calendars/user1/default/foo.ics'))->getBody(); + $resultVObj = VObject\Reader::read($result); + + $this->assertEquals( + '1.2', + $resultVObj->VEVENT->ATTENDEE[1]['SCHEDULE-STATUS']->getValue() + ); + + $this->assertEquals(1, count($messages)); + $message = $messages[0]; + + $this->assertInstanceOf('\Sabre\VObject\ITip\Message', $message); + $this->assertEquals('mailto:user2.sabredav@sabredav.org', $message->recipient); + $this->assertEquals('Roxy Kesh', $message->recipientName); + $this->assertEquals('mailto:user1.sabredav@sabredav.org', $message->sender); + $this->assertEquals('Administrator', $message->senderName); + $this->assertEquals('REQUEST', $message->method); + + $this->assertEquals('REQUEST', $message->message->METHOD->getValue()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/FreeBusyRequestTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/FreeBusyRequestTest.php new file mode 100644 index 00000000000..0e0b609a11c --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/FreeBusyRequestTest.php @@ -0,0 +1,611 @@ + 'principals/user2', + 'id' => 1, + 'uri' => 'calendar1', + $caldavNS . 'calendar-timezone' => "BEGIN:VCALENDAR\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nEND:VTIMEZONE\r\nEND:VCALENDAR", + ], + [ + 'principaluri' => 'principals/user2', + 'id' => 2, + 'uri' => 'calendar2', + $caldavNS . 'schedule-calendar-transp' => new ScheduleCalendarTransp(ScheduleCalendarTransp::TRANSPARENT), + ], + ]; + $calendarobjects = [ + 1 => ['1.ics' => [ + 'uri' => '1.ics', + 'calendardata' => 'BEGIN:VCALENDAR +BEGIN:VEVENT +DTSTART:20110101T130000 +DURATION:PT1H +END:VEVENT +END:VCALENDAR', + 'calendarid' => 1, + ]], + 2 => ['2.ics' => [ + 'uri' => '2.ics', + 'calendardata' => 'BEGIN:VCALENDAR +BEGIN:VEVENT +DTSTART:20110101T080000 +DURATION:PT1H +END:VEVENT +END:VCALENDAR', + 'calendarid' => 2, + ]] + + ]; + + $principalBackend = new DAVACL\PrincipalBackend\Mock(); + $this->caldavBackend = new CalDAV\Backend\MockScheduling($calendars, $calendarobjects); + + $tree = [ + new DAVACL\PrincipalCollection($principalBackend), + new CalDAV\CalendarRoot($principalBackend, $this->caldavBackend), + ]; + + $this->request = HTTP\Sapi::createFromServerArray([ + 'CONTENT_TYPE' => 'text/calendar', + ]); + $this->response = new HTTP\ResponseMock(); + + $this->server = new DAV\Server($tree); + $this->server->httpRequest = $this->request; + $this->server->httpResponse = $this->response; + + $this->aclPlugin = new DAVACL\Plugin(); + $this->aclPlugin->allowUnauthenticatedAccess = false; + $this->server->addPlugin($this->aclPlugin); + + $authBackend = new DAV\Auth\Backend\Mock(); + $authBackend->setPrincipal('principals/user1'); + $this->authPlugin = new DAV\Auth\Plugin($authBackend); + // Forcing authentication to work. + $this->authPlugin->beforeMethod($this->request, $this->response); + $this->server->addPlugin($this->authPlugin); + + // CalDAV plugin + $this->plugin = new CalDAV\Plugin(); + $this->server->addPlugin($this->plugin); + + // Scheduling plugin + $this->plugin = new Plugin(); + $this->server->addPlugin($this->plugin); + + } + + function testWrongContentType() { + + $this->server->httpRequest = new HTTP\Request( + 'POST', + '/calendars/user1/outbox', + ['Content-Type' => 'text/plain'] + ); + + $this->assertNull( + $this->plugin->httpPost($this->server->httpRequest, $this->server->httpResponse) + ); + + } + + function testNotFound() { + + $this->server->httpRequest = new HTTP\Request( + 'POST', + '/calendars/user1/blabla', + ['Content-Type' => 'text/calendar'] + ); + + $this->assertNull( + $this->plugin->httpPost($this->server->httpRequest, $this->server->httpResponse) + ); + + } + + function testNotOutbox() { + + $this->server->httpRequest = new HTTP\Request( + 'POST', + '/calendars/user1/inbox', + ['Content-Type' => 'text/calendar'] + ); + + $this->assertNull( + $this->plugin->httpPost($this->server->httpRequest, $this->server->httpResponse) + ); + + } + + /** + * @expectedException Sabre\DAV\Exception\BadRequest + */ + function testNoItipMethod() { + + $this->server->httpRequest = new HTTP\Request( + 'POST', + '/calendars/user1/outbox', + ['Content-Type' => 'text/calendar'] + ); + + $body = <<server->httpRequest->setBody($body); + $this->plugin->httpPost($this->server->httpRequest, $this->server->httpResponse); + + } + + /** + * @expectedException \Sabre\DAV\Exception\NotImplemented + */ + function testNoVFreeBusy() { + + $this->server->httpRequest = new HTTP\Request( + 'POST', + '/calendars/user1/outbox', + ['Content-Type' => 'text/calendar'] + ); + + $body = <<server->httpRequest->setBody($body); + $this->plugin->httpPost($this->server->httpRequest, $this->server->httpResponse); + + } + + /** + * @expectedException Sabre\DAV\Exception\Forbidden + */ + function testIncorrectOrganizer() { + + $this->server->httpRequest = new HTTP\Request( + 'POST', + '/calendars/user1/outbox', + ['Content-Type' => 'text/calendar'] + ); + + + $body = <<server->httpRequest->setBody($body); + $this->plugin->httpPost($this->server->httpRequest, $this->server->httpResponse); + + } + + /** + * @expectedException Sabre\DAV\Exception\BadRequest + */ + function testNoAttendees() { + + $this->server->httpRequest = new HTTP\Request( + 'POST', + '/calendars/user1/outbox', + ['Content-Type' => 'text/calendar'] + ); + + $body = <<server->httpRequest->setBody($body); + $this->plugin->httpPost($this->server->httpRequest, $this->server->httpResponse); + + } + + /** + * @expectedException Sabre\DAV\Exception\BadRequest + */ + function testNoDTStart() { + + $this->server->httpRequest = new HTTP\Request( + 'POST', + '/calendars/user1/outbox', + ['Content-Type' => 'text/calendar'] + ); + + $body = <<server->httpRequest->setBody($body); + $this->plugin->httpPost($this->server->httpRequest, $this->server->httpResponse); + + } + + function testSucceed() { + + $this->server->httpRequest = new HTTP\Request( + 'POST', + '/calendars/user1/outbox', + ['Content-Type' => 'text/calendar'] + ); + + $body = <<server->httpRequest->setBody($body); + + // Lazily making the current principal an admin. + $this->aclPlugin->adminPrincipals[] = 'principals/user1'; + + $this->assertFalse( + $this->plugin->httpPost($this->server->httpRequest, $this->response) + ); + + $this->assertEquals(200, $this->response->status); + $this->assertEquals([ + 'Content-Type' => ['application/xml'], + ], $this->response->getHeaders()); + + $strings = [ + 'mailto:user2.sabredav@sabredav.org', + 'mailto:user3.sabredav@sabredav.org', + '2.0;Success', + '3.7;Could not find principal', + 'FREEBUSY:20110101T120000Z/20110101T130000Z', + ]; + + foreach ($strings as $string) { + $this->assertTrue( + strpos($this->response->body, $string) !== false, + 'The response body did not contain: ' . $string . 'Full response: ' . $this->response->body + ); + } + + $this->assertTrue( + strpos($this->response->body, 'FREEBUSY;FBTYPE=BUSY:20110101T080000Z/20110101T090000Z') == false, + 'The response body did contain free busy info from a transparent calendar.' + ); + + } + + /** + * Testing if the freebusy request still works, even if there are no + * calendars in the target users' account. + */ + function testSucceedNoCalendars() { + + // Deleting calendars + $this->caldavBackend->deleteCalendar(1); + $this->caldavBackend->deleteCalendar(2); + + $this->server->httpRequest = new HTTP\Request( + 'POST', + '/calendars/user1/outbox', + ['Content-Type' => 'text/calendar'] + ); + + $body = <<server->httpRequest->setBody($body); + + // Lazily making the current principal an admin. + $this->aclPlugin->adminPrincipals[] = 'principals/user1'; + + $this->assertFalse( + $this->plugin->httpPost($this->server->httpRequest, $this->response) + ); + + $this->assertEquals(200, $this->response->status); + $this->assertEquals([ + 'Content-Type' => ['application/xml'], + ], $this->response->getHeaders()); + + $strings = [ + 'mailto:user2.sabredav@sabredav.org', + '2.0;Success', + ]; + + foreach ($strings as $string) { + $this->assertTrue( + strpos($this->response->body, $string) !== false, + 'The response body did not contain: ' . $string . 'Full response: ' . $this->response->body + ); + } + + } + + function testNoCalendarHomeFound() { + + $this->server->httpRequest = new HTTP\Request( + 'POST', + '/calendars/user1/outbox', + ['Content-Type' => 'text/calendar'] + ); + + $body = <<server->httpRequest->setBody($body); + + // Lazily making the current principal an admin. + $this->aclPlugin->adminPrincipals[] = 'principals/user1'; + + // Removing the calendar home + $this->server->on('propFind', function(DAV\PropFind $propFind) { + + $propFind->set('{' . Plugin::NS_CALDAV . '}calendar-home-set', null, 403); + + }); + + $this->assertFalse( + $this->plugin->httpPost($this->server->httpRequest, $this->response) + ); + + $this->assertEquals(200, $this->response->status); + $this->assertEquals([ + 'Content-Type' => ['application/xml'], + ], $this->response->getHeaders()); + + $strings = [ + 'mailto:user2.sabredav@sabredav.org', + '3.7;No calendar-home-set property found', + ]; + + foreach ($strings as $string) { + $this->assertTrue( + strpos($this->response->body, $string) !== false, + 'The response body did not contain: ' . $string . 'Full response: ' . $this->response->body + ); + } + + } + + function testNoInboxFound() { + + $this->server->httpRequest = new HTTP\Request( + 'POST', + '/calendars/user1/outbox', + ['Content-Type' => 'text/calendar'] + ); + + $body = <<server->httpRequest->setBody($body); + + // Lazily making the current principal an admin. + $this->aclPlugin->adminPrincipals[] = 'principals/user1'; + + // Removing the inbox + $this->server->on('propFind', function(DAV\PropFind $propFind) { + + $propFind->set('{' . Plugin::NS_CALDAV . '}schedule-inbox-URL', null, 403); + + }); + + $this->assertFalse( + $this->plugin->httpPost($this->server->httpRequest, $this->response) + ); + + $this->assertEquals(200, $this->response->status); + $this->assertEquals([ + 'Content-Type' => ['application/xml'], + ], $this->response->getHeaders()); + + $strings = [ + 'mailto:user2.sabredav@sabredav.org', + '3.7;No schedule-inbox-URL property found', + ]; + + foreach ($strings as $string) { + $this->assertTrue( + strpos($this->response->body, $string) !== false, + 'The response body did not contain: ' . $string . 'Full response: ' . $this->response->body + ); + } + + } + + function testSucceedUseVAVAILABILITY() { + + $this->server->httpRequest = new HTTP\Request( + 'POST', + '/calendars/user1/outbox', + ['Content-Type' => 'text/calendar'] + ); + + $body = <<server->httpRequest->setBody($body); + + // Lazily making the current principal an admin. + $this->aclPlugin->adminPrincipals[] = 'principals/user1'; + + // Adding VAVAILABILITY manually + $this->server->on('propFind', function(DAV\PropFind $propFind) { + + $propFind->handle('{' . Plugin::NS_CALDAV . '}calendar-availability', function() { + + $avail = <<assertFalse( + $this->plugin->httpPost($this->server->httpRequest, $this->response) + ); + + $this->assertEquals(200, $this->response->status); + $this->assertEquals([ + 'Content-Type' => ['application/xml'], + ], $this->response->getHeaders()); + + $strings = [ + 'mailto:user2.sabredav@sabredav.org', + '2.0;Success', + 'FREEBUSY;FBTYPE=BUSY-UNAVAILABLE:20110101T080000Z/20110101T090000Z', + 'FREEBUSY:20110101T120000Z/20110101T130000Z', + 'FREEBUSY;FBTYPE=BUSY-UNAVAILABLE:20110101T170000Z/20110101T180000Z', + ]; + + foreach ($strings as $string) { + $this->assertTrue( + strpos($this->response->body, $string) !== false, + 'The response body did not contain: ' . $string . 'Full response: ' . $this->response->body + ); + } + + } + + /* + function testNoPrivilege() { + + $this->markTestIncomplete('Currently there\'s no "no privilege" situation'); + + $this->server->httpRequest = HTTP\Sapi::createFromServerArray(array( + 'CONTENT_TYPE' => 'text/calendar', + 'REQUEST_METHOD' => 'POST', + 'REQUEST_URI' => '/calendars/user1/outbox', + )); + + $body = <<server->httpRequest->setBody($body); + + $this->assertFalse( + $this->plugin->httpPost($this->server->httpRequest, $this->response) + ); + + $this->assertEquals(200, $this->response->status); + $this->assertEquals([ + 'Content-Type' => 'application/xml', + ], $this->response->getHeaders()); + + $strings = [ + 'mailto:user2.sabredav@sabredav.org', + '3.7;No calendar-home-set property found', + ]; + + foreach($strings as $string) { + $this->assertTrue( + strpos($this->response->body, $string)!==false, + 'The response body did not contain: ' . $string .'Full response: ' . $this->response->body + ); + } + + + }*/ + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/IMip/MockPlugin.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/IMip/MockPlugin.php new file mode 100644 index 00000000000..02846609354 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/IMip/MockPlugin.php @@ -0,0 +1,50 @@ +emails[] = [ + 'to' => $to, + 'subject' => $subject, + 'body' => $body, + 'headers' => $headers, + ]; + + } + + function getSentEmails() { + + return $this->emails; + + } + + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/IMipPluginTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/IMipPluginTest.php new file mode 100644 index 00000000000..7311999f5b4 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/IMipPluginTest.php @@ -0,0 +1,221 @@ +assertEquals( + 'imip', + $plugin->getPluginInfo()['name'] + ); + + } + + function testDeliverReply() { + + $message = new Message(); + $message->sender = 'mailto:sender@example.org'; + $message->senderName = 'Sender'; + $message->recipient = 'mailto:recipient@example.org'; + $message->recipientName = 'Recipient'; + $message->method = 'REPLY'; + + $ics = <<message = Reader::read($ics); + + $result = $this->schedule($message); + + $expected = [ + [ + 'to' => 'Recipient ', + 'subject' => 'Re: Birthday party', + 'body' => $ics, + 'headers' => [ + 'Reply-To: Sender ', + 'From: system@example.org', + 'Content-Type: text/calendar; charset=UTF-8; method=REPLY', + 'X-Sabre-Version: ' . \Sabre\DAV\Version::VERSION, + ], + ] + ]; + + $this->assertEquals($expected, $result); + + } + + function testDeliverReplyNoMailto() { + + $message = new Message(); + $message->sender = 'mailto:sender@example.org'; + $message->senderName = 'Sender'; + $message->recipient = 'http://example.org/recipient'; + $message->recipientName = 'Recipient'; + $message->method = 'REPLY'; + + $ics = <<message = Reader::read($ics); + + $result = $this->schedule($message); + + $expected = []; + + $this->assertEquals($expected, $result); + + } + + function testDeliverRequest() { + + $message = new Message(); + $message->sender = 'mailto:sender@example.org'; + $message->senderName = 'Sender'; + $message->recipient = 'mailto:recipient@example.org'; + $message->recipientName = 'Recipient'; + $message->method = 'REQUEST'; + + $ics = <<message = Reader::read($ics); + + $result = $this->schedule($message); + + $expected = [ + [ + 'to' => 'Recipient ', + 'subject' => 'Birthday party', + 'body' => $ics, + 'headers' => [ + 'Reply-To: Sender ', + 'From: system@example.org', + 'Content-Type: text/calendar; charset=UTF-8; method=REQUEST', + 'X-Sabre-Version: ' . \Sabre\DAV\Version::VERSION, + ], + ] + ]; + + $this->assertEquals($expected, $result); + + } + + function testDeliverCancel() { + + $message = new Message(); + $message->sender = 'mailto:sender@example.org'; + $message->senderName = 'Sender'; + $message->recipient = 'mailto:recipient@example.org'; + $message->recipientName = 'Recipient'; + $message->method = 'CANCEL'; + + $ics = <<message = Reader::read($ics); + + $result = $this->schedule($message); + + $expected = [ + [ + 'to' => 'Recipient ', + 'subject' => 'Cancelled: Birthday party', + 'body' => $ics, + 'headers' => [ + 'Reply-To: Sender ', + 'From: system@example.org', + 'Content-Type: text/calendar; charset=UTF-8; method=CANCEL', + 'X-Sabre-Version: ' . \Sabre\DAV\Version::VERSION, + ], + ] + ]; + + $this->assertEquals($expected, $result); + $this->assertEquals('1.1', substr($message->scheduleStatus, 0, 3)); + + } + + function schedule(Message $message) { + + $plugin = new IMip\MockPlugin('system@example.org'); + + $server = new Server(); + $server->addPlugin($plugin); + $server->emit('schedule', [$message]); + + return $plugin->getSentEmails(); + + } + + function testDeliverInsignificantRequest() { + + $message = new Message(); + $message->sender = 'mailto:sender@example.org'; + $message->senderName = 'Sender'; + $message->recipient = 'mailto:recipient@example.org'; + $message->recipientName = 'Recipient'; + $message->method = 'REQUEST'; + $message->significantChange = false; + + $ics = <<message = Reader::read($ics); + + $result = $this->schedule($message); + + $expected = []; + $this->assertEquals($expected, $result); + $this->assertEquals('1.0', $message->getScheduleStatus()[0]); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/InboxTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/InboxTest.php new file mode 100644 index 00000000000..01c3488afd6 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/InboxTest.php @@ -0,0 +1,136 @@ +assertEquals('inbox', $inbox->getName()); + $this->assertEquals([], $inbox->getChildren()); + $this->assertEquals('principals/user1', $inbox->getOwner()); + $this->assertEquals(null, $inbox->getGroup()); + + $this->assertEquals([ + [ + 'privilege' => '{DAV:}read', + 'principal' => '{DAV:}authenticated', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write-properties', + 'principal' => 'principals/user1', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}unbind', + 'principal' => 'principals/user1', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}unbind', + 'principal' => 'principals/user1/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{urn:ietf:params:xml:ns:caldav}schedule-deliver', + 'principal' => '{DAV:}authenticated', + 'protected' => true, + ], + ], $inbox->getACL()); + + $ok = false; + + } + + /** + * @depends testSetup + */ + function testGetChildren() { + + $backend = new CalDAV\Backend\MockScheduling(); + $inbox = new Inbox( + $backend, + 'principals/user1' + ); + + $this->assertEquals( + 0, + count($inbox->getChildren()) + ); + $backend->createSchedulingObject('principals/user1', 'schedule1.ics', "BEGIN:VCALENDAR\r\nEND:VCALENDAR"); + $this->assertEquals( + 1, + count($inbox->getChildren()) + ); + $this->assertInstanceOf('Sabre\CalDAV\Schedule\SchedulingObject', $inbox->getChildren()[0]); + $this->assertEquals( + 'schedule1.ics', + $inbox->getChildren()[0]->getName() + ); + + } + + /** + * @depends testGetChildren + */ + function testCreateFile() { + + $backend = new CalDAV\Backend\MockScheduling(); + $inbox = new Inbox( + $backend, + 'principals/user1' + ); + + $this->assertEquals( + 0, + count($inbox->getChildren()) + ); + $inbox->createFile('schedule1.ics', "BEGIN:VCALENDAR\r\nEND:VCALENDAR"); + $this->assertEquals( + 1, + count($inbox->getChildren()) + ); + $this->assertInstanceOf('Sabre\CalDAV\Schedule\SchedulingObject', $inbox->getChildren()[0]); + $this->assertEquals( + 'schedule1.ics', + $inbox->getChildren()[0]->getName() + ); + + } + + /** + * @depends testSetup + */ + function testCalendarQuery() { + + $backend = new CalDAV\Backend\MockScheduling(); + $inbox = new Inbox( + $backend, + 'principals/user1' + ); + + $this->assertEquals( + 0, + count($inbox->getChildren()) + ); + $backend->createSchedulingObject('principals/user1', 'schedule1.ics', "BEGIN:VCALENDAR\r\nEND:VCALENDAR"); + $this->assertEquals( + ['schedule1.ics'], + $inbox->calendarQuery([ + 'name' => 'VCALENDAR', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false + ]) + ); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/OutboxPostTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/OutboxPostTest.php new file mode 100644 index 00000000000..3ab2c2288cf --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/OutboxPostTest.php @@ -0,0 +1,134 @@ + 'POST', + 'REQUEST_URI' => '/notfound', + 'HTTP_CONTENT_TYPE' => 'text/calendar', + ]); + + $this->assertHTTPStatus(501, $req); + + } + + function testPostPassThruNotTextCalendar() { + + $req = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'POST', + 'REQUEST_URI' => '/calendars/user1/outbox', + ]); + + $this->assertHTTPStatus(501, $req); + + } + + function testPostPassThruNoOutBox() { + + $req = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'POST', + 'REQUEST_URI' => '/calendars', + 'HTTP_CONTENT_TYPE' => 'text/calendar', + ]); + + $this->assertHTTPStatus(501, $req); + + } + + function testInvalidIcalBody() { + + $req = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'POST', + 'REQUEST_URI' => '/calendars/user1/outbox', + 'HTTP_ORIGINATOR' => 'mailto:user1.sabredav@sabredav.org', + 'HTTP_RECIPIENT' => 'mailto:user2@example.org', + 'HTTP_CONTENT_TYPE' => 'text/calendar', + ]); + $req->setBody('foo'); + + $this->assertHTTPStatus(400, $req); + + } + + function testNoVEVENT() { + + $req = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'POST', + 'REQUEST_URI' => '/calendars/user1/outbox', + 'HTTP_ORIGINATOR' => 'mailto:user1.sabredav@sabredav.org', + 'HTTP_RECIPIENT' => 'mailto:user2@example.org', + 'HTTP_CONTENT_TYPE' => 'text/calendar', + ]); + + $body = [ + 'BEGIN:VCALENDAR', + 'BEGIN:VTIMEZONE', + 'END:VTIMEZONE', + 'END:VCALENDAR', + ]; + + $req->setBody(implode("\r\n", $body)); + + $this->assertHTTPStatus(400, $req); + + } + + function testNoMETHOD() { + + $req = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'POST', + 'REQUEST_URI' => '/calendars/user1/outbox', + 'HTTP_ORIGINATOR' => 'mailto:user1.sabredav@sabredav.org', + 'HTTP_RECIPIENT' => 'mailto:user2@example.org', + 'HTTP_CONTENT_TYPE' => 'text/calendar', + ]); + + $body = [ + 'BEGIN:VCALENDAR', + 'BEGIN:VEVENT', + 'END:VEVENT', + 'END:VCALENDAR', + ]; + + $req->setBody(implode("\r\n", $body)); + + $this->assertHTTPStatus(400, $req); + + } + + function testUnsupportedMethod() { + + $req = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'POST', + 'REQUEST_URI' => '/calendars/user1/outbox', + 'HTTP_ORIGINATOR' => 'mailto:user1.sabredav@sabredav.org', + 'HTTP_RECIPIENT' => 'mailto:user2@example.org', + 'HTTP_CONTENT_TYPE' => 'text/calendar', + ]); + + $body = [ + 'BEGIN:VCALENDAR', + 'METHOD:PUBLISH', + 'BEGIN:VEVENT', + 'END:VEVENT', + 'END:VCALENDAR', + ]; + + $req->setBody(implode("\r\n", $body)); + + $this->assertHTTPStatus(501, $req); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/OutboxTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/OutboxTest.php new file mode 100644 index 00000000000..04d4b12379e --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/OutboxTest.php @@ -0,0 +1,48 @@ +assertEquals('outbox', $outbox->getName()); + $this->assertEquals([], $outbox->getChildren()); + $this->assertEquals('principals/user1', $outbox->getOwner()); + $this->assertEquals(null, $outbox->getGroup()); + + $this->assertEquals([ + [ + 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-send', + 'principal' => 'principals/user1', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'principals/user1', + 'protected' => true, + ], + [ + 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-send', + 'principal' => 'principals/user1/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'principals/user1/calendar-proxy-read', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'principals/user1/calendar-proxy-write', + 'protected' => true, + ], + ], $outbox->getACL()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/PluginBasicTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/PluginBasicTest.php new file mode 100644 index 00000000000..cee911b6e0b --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/PluginBasicTest.php @@ -0,0 +1,39 @@ +assertEquals( + 'caldav-schedule', + $plugin->getPluginInfo()['name'] + ); + + } + + function testOptions() { + + $plugin = new Plugin(); + $expected = [ + 'calendar-auto-schedule', + 'calendar-availability', + ]; + $this->assertEquals($expected, $plugin->getFeatures()); + + } + + function testGetHTTPMethods() { + + $this->assertEquals([], $this->caldavSchedulePlugin->getHTTPMethods('notfound')); + $this->assertEquals([], $this->caldavSchedulePlugin->getHTTPMethods('calendars/user1')); + $this->assertEquals(['POST'], $this->caldavSchedulePlugin->getHTTPMethods('calendars/user1/outbox')); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/PluginPropertiesTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/PluginPropertiesTest.php new file mode 100644 index 00000000000..2d0391893ef --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/PluginPropertiesTest.php @@ -0,0 +1,146 @@ +caldavBackend->createCalendar( + 'principals/user1', + 'default', + [ + + ] + ); + $this->principalBackend->addPrincipal([ + 'uri' => 'principals/user1/calendar-proxy-read' + ]); + + } + + function testPrincipalProperties() { + + $props = $this->server->getPropertiesForPath('/principals/user1', [ + '{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL', + '{urn:ietf:params:xml:ns:caldav}schedule-outbox-URL', + '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set', + '{urn:ietf:params:xml:ns:caldav}calendar-user-type', + '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL', + ]); + + $this->assertArrayHasKey(0, $props); + $this->assertArrayHasKey(200, $props[0]); + + $this->assertArrayHasKey('{urn:ietf:params:xml:ns:caldav}schedule-outbox-URL', $props[0][200]); + $prop = $props[0][200]['{urn:ietf:params:xml:ns:caldav}schedule-outbox-URL']; + $this->assertTrue($prop instanceof DAV\Xml\Property\Href); + $this->assertEquals('calendars/user1/outbox/', $prop->getHref()); + + $this->assertArrayHasKey('{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL', $props[0][200]); + $prop = $props[0][200]['{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL']; + $this->assertTrue($prop instanceof DAV\Xml\Property\Href); + $this->assertEquals('calendars/user1/inbox/', $prop->getHref()); + + $this->assertArrayHasKey('{urn:ietf:params:xml:ns:caldav}calendar-user-address-set', $props[0][200]); + $prop = $props[0][200]['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set']; + $this->assertTrue($prop instanceof DAV\Xml\Property\Href); + $this->assertEquals(['mailto:user1.sabredav@sabredav.org', '/principals/user1/'], $prop->getHrefs()); + + $this->assertArrayHasKey('{urn:ietf:params:xml:ns:caldav}calendar-user-type', $props[0][200]); + $prop = $props[0][200]['{urn:ietf:params:xml:ns:caldav}calendar-user-type']; + $this->assertEquals('INDIVIDUAL', $prop); + + $this->assertArrayHasKey('{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL', $props[0][200]); + $prop = $props[0][200]['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL']; + $this->assertEquals('calendars/user1/default/', $prop->getHref()); + + } + function testPrincipalPropertiesBadPrincipal() { + + $props = $this->server->getPropertiesForPath('principals/user1/calendar-proxy-read', [ + '{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL', + '{urn:ietf:params:xml:ns:caldav}schedule-outbox-URL', + '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set', + '{urn:ietf:params:xml:ns:caldav}calendar-user-type', + '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL', + ]); + + $this->assertArrayHasKey(0, $props); + $this->assertArrayHasKey(200, $props[0]); + $this->assertArrayHasKey(404, $props[0]); + + $this->assertArrayHasKey('{urn:ietf:params:xml:ns:caldav}schedule-outbox-URL', $props[0][404]); + $this->assertArrayHasKey('{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL', $props[0][404]); + + $prop = $props[0][200]['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set']; + $this->assertTrue($prop instanceof DAV\Xml\Property\Href); + $this->assertEquals(['/principals/user1/calendar-proxy-read/'], $prop->getHrefs()); + + $this->assertArrayHasKey('{urn:ietf:params:xml:ns:caldav}calendar-user-type', $props[0][200]); + $prop = $props[0][200]['{urn:ietf:params:xml:ns:caldav}calendar-user-type']; + $this->assertEquals('INDIVIDUAL', $prop); + + $this->assertArrayHasKey('{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL', $props[0][404]); + + } + function testNoDefaultCalendar() { + + foreach ($this->caldavBackend->getCalendarsForUser('principals/user1') as $calendar) { + $this->caldavBackend->deleteCalendar($calendar['id']); + } + $props = $this->server->getPropertiesForPath('/principals/user1', [ + '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL', + ]); + + $this->assertArrayHasKey(0, $props); + $this->assertArrayHasKey(404, $props[0]); + + $this->assertArrayHasKey('{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL', $props[0][404]); + + } + + /** + * There are two properties for availability. The server should + * automatically map the old property to the standard property. + */ + function testAvailabilityMapping() { + + $path = 'calendars/user1/inbox'; + $oldProp = '{http://calendarserver.org/ns/}calendar-availability'; + $newProp = '{urn:ietf:params:xml:ns:caldav}calendar-availability'; + $value1 = 'first value'; + $value2 = 'second value'; + + // Storing with the old name + $this->server->updateProperties($path, [ + $oldProp => $value1 + ]); + + // Retrieving with the new name + $this->assertEquals( + [$newProp => $value1], + $this->server->getProperties($path, [$newProp]) + ); + + // Storing with the new name + $this->server->updateProperties($path, [ + $newProp => $value2 + ]); + + // Retrieving with the old name + $this->assertEquals( + [$oldProp => $value2], + $this->server->getProperties($path, [$oldProp]) + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/PluginPropertiesWithSharedCalendarTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/PluginPropertiesWithSharedCalendarTest.php new file mode 100644 index 00000000000..870f14c1447 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/PluginPropertiesWithSharedCalendarTest.php @@ -0,0 +1,71 @@ +caldavBackend->createCalendar( + 'principals/user1', + 'shared', + [ + 'share-access' => DAV\Sharing\Plugin::ACCESS_READWRITE + ] + ); + $this->caldavBackend->createCalendar( + 'principals/user1', + 'default', + [ + + ] + ); + + } + + function testPrincipalProperties() { + + $props = $this->server->getPropertiesForPath('/principals/user1', [ + '{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL', + '{urn:ietf:params:xml:ns:caldav}schedule-outbox-URL', + '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set', + '{urn:ietf:params:xml:ns:caldav}calendar-user-type', + '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL', + ]); + + $this->assertArrayHasKey(0, $props); + $this->assertArrayHasKey(200, $props[0]); + + $this->assertArrayHasKey('{urn:ietf:params:xml:ns:caldav}schedule-outbox-URL', $props[0][200]); + $prop = $props[0][200]['{urn:ietf:params:xml:ns:caldav}schedule-outbox-URL']; + $this->assertTrue($prop instanceof DAV\Xml\Property\Href); + $this->assertEquals('calendars/user1/outbox/', $prop->getHref()); + + $this->assertArrayHasKey('{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL', $props[0][200]); + $prop = $props[0][200]['{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL']; + $this->assertTrue($prop instanceof DAV\Xml\Property\Href); + $this->assertEquals('calendars/user1/inbox/', $prop->getHref()); + + $this->assertArrayHasKey('{urn:ietf:params:xml:ns:caldav}calendar-user-address-set', $props[0][200]); + $prop = $props[0][200]['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set']; + $this->assertTrue($prop instanceof DAV\Xml\Property\Href); + $this->assertEquals(['mailto:user1.sabredav@sabredav.org', '/principals/user1/'], $prop->getHrefs()); + + $this->assertArrayHasKey('{urn:ietf:params:xml:ns:caldav}calendar-user-type', $props[0][200]); + $prop = $props[0][200]['{urn:ietf:params:xml:ns:caldav}calendar-user-type']; + $this->assertEquals('INDIVIDUAL', $prop); + + $this->assertArrayHasKey('{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL', $props[0][200]); + $prop = $props[0][200]['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL']; + $this->assertEquals('calendars/user1/default/', $prop->getHref()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/ScheduleDeliverTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/ScheduleDeliverTest.php new file mode 100644 index 00000000000..8123c685c76 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/ScheduleDeliverTest.php @@ -0,0 +1,666 @@ + 'principals/user1', + 'uri' => 'cal', + ], + [ + 'principaluri' => 'principals/user2', + 'uri' => 'cal', + ], + ]; + + function setUp() { + + $this->calendarObjectUri = '/calendars/user1/cal/object.ics'; + + parent::setUp(); + + } + + function testNewInvite() { + + $newObject = <<deliver(null, $newObject); + $this->assertItemsInInbox('user2', 1); + + $expected = <<assertVObjectEqualsVObject( + $expected, + $newObject + ); + + } + + function testNewOnWrongCollection() { + + $newObject = <<calendarObjectUri = '/calendars/user1/object.ics'; + $this->deliver(null, $newObject); + $this->assertItemsInInbox('user2', 0); + + + } + function testNewInviteSchedulingDisabled() { + + $newObject = <<deliver(null, $newObject, true); + $this->assertItemsInInbox('user2', 0); + + } + function testUpdatedInvite() { + + $newObject = <<deliver($oldObject, $newObject); + $this->assertItemsInInbox('user2', 1); + + $expected = <<assertVObjectEqualsVObject( + $expected, + $newObject + ); + + + } + function testUpdatedInviteSchedulingDisabled() { + + $newObject = <<deliver($oldObject, $newObject, true); + $this->assertItemsInInbox('user2', 0); + + } + + function testUpdatedInviteWrongPath() { + + $newObject = <<calendarObjectUri = '/calendars/user1/inbox/foo.ics'; + $this->deliver($oldObject, $newObject); + $this->assertItemsInInbox('user2', 0); + + } + + function testDeletedInvite() { + + $newObject = null; + + $oldObject = <<deliver($oldObject, $newObject); + $this->assertItemsInInbox('user2', 1); + + } + + function testDeletedInviteSchedulingDisabled() { + + $newObject = null; + + $oldObject = <<deliver($oldObject, $newObject, true); + $this->assertItemsInInbox('user2', 0); + + } + + /** + * A MOVE request will trigger an unbind on a scheduling resource. + * + * However, we must not treat it as a cancellation, it just got moved to a + * different calendar. + */ + function testUnbindIgnoredOnMove() { + + $newObject = null; + + $oldObject = <<server->httpRequest->setMethod('MOVE'); + $this->deliver($oldObject, $newObject); + $this->assertItemsInInbox('user2', 0); + + } + + function testDeletedInviteWrongUrl() { + + $newObject = null; + + $oldObject = <<calendarObjectUri = '/calendars/user1/inbox/foo.ics'; + $this->deliver($oldObject, $newObject); + $this->assertItemsInInbox('user2', 0); + + } + + function testReply() { + + $oldObject = <<putPath('calendars/user2/cal/foo.ics', $oldObject); + + $this->deliver($oldObject, $newObject); + $this->assertItemsInInbox('user2', 1); + $this->assertItemsInInbox('user1', 0); + + $expected = <<assertVObjectEqualsVObject( + $expected, + $newObject + ); + + } + + + + function testInviteUnknownUser() { + + $newObject = <<deliver(null, $newObject); + + $expected = <<assertVObjectEqualsVObject( + $expected, + $newObject + ); + + } + + function testInviteNoInboxUrl() { + + $newObject = <<server->on('propFind', function($propFind) { + $propFind->set('{' . Plugin::NS_CALDAV . '}schedule-inbox-URL', null, 403); + }); + $this->deliver(null, $newObject); + + $expected = <<assertVObjectEqualsVObject( + $expected, + $newObject + ); + + } + + function testInviteNoCalendarHomeSet() { + + $newObject = <<server->on('propFind', function($propFind) { + $propFind->set('{' . Plugin::NS_CALDAV . '}calendar-home-set', null, 403); + }); + $this->deliver(null, $newObject); + + $expected = <<assertVObjectEqualsVObject( + $expected, + $newObject + ); + + } + function testInviteNoDefaultCalendar() { + + $newObject = <<server->on('propFind', function($propFind) { + $propFind->set('{' . Plugin::NS_CALDAV . '}schedule-default-calendar-URL', null, 403); + }); + $this->deliver(null, $newObject); + + $expected = <<assertVObjectEqualsVObject( + $expected, + $newObject + ); + + } + function testInviteNoScheduler() { + + $newObject = <<server->removeAllListeners('schedule'); + $this->deliver(null, $newObject); + + $expected = <<assertVObjectEqualsVObject( + $expected, + $newObject + ); + + } + function testInviteNoACLPlugin() { + + $this->setupACL = false; + parent::setUp(); + + $newObject = <<deliver(null, $newObject); + + $expected = <<assertVObjectEqualsVObject( + $expected, + $newObject + ); + + } + + protected $calendarObjectUri; + + function deliver($oldObject, &$newObject, $disableScheduling = false) { + + $this->server->httpRequest->setUrl($this->calendarObjectUri); + if ($disableScheduling) { + $this->server->httpRequest->setHeader('Schedule-Reply', 'F'); + } + + if ($oldObject && $newObject) { + // update + $this->putPath($this->calendarObjectUri, $oldObject); + + $stream = fopen('php://memory', 'r+'); + fwrite($stream, $newObject); + rewind($stream); + $modified = false; + + $this->server->emit('beforeWriteContent', [ + $this->calendarObjectUri, + $this->server->tree->getNodeForPath($this->calendarObjectUri), + &$stream, + &$modified + ]); + if ($modified) { + $newObject = $stream; + } + + } elseif ($oldObject && !$newObject) { + // delete + $this->putPath($this->calendarObjectUri, $oldObject); + + $this->caldavSchedulePlugin->beforeUnbind( + $this->calendarObjectUri + ); + } else { + + // create + $stream = fopen('php://memory', 'r+'); + fwrite($stream, $newObject); + rewind($stream); + $modified = false; + $this->server->emit('beforeCreateFile', [ + $this->calendarObjectUri, + &$stream, + $this->server->tree->getNodeForPath(dirname($this->calendarObjectUri)), + &$modified + ]); + + if ($modified) { + $newObject = $stream; + } + } + + } + + + /** + * Creates or updates a node at the specified path. + * + * This circumvents sabredav's internal server apis, so all events and + * access control is skipped. + * + * @param string $path + * @param string $data + * @return void + */ + function putPath($path, $data) { + + list($parent, $base) = \Sabre\HTTP\UrlUtil::splitPath($path); + $parentNode = $this->server->tree->getNodeForPath($parent); + + /* + if ($parentNode->childExists($base)) { + $childNode = $parentNode->getChild($base); + $childNode->put($data); + } else {*/ + $parentNode->createFile($base, $data); + //} + + } + + function assertItemsInInbox($user, $count) { + + $inboxNode = $this->server->tree->getNodeForPath('calendars/' . $user . '/inbox'); + $this->assertEquals($count, count($inboxNode->getChildren())); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/SchedulingObjectTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/SchedulingObjectTest.php new file mode 100644 index 00000000000..be83cd08110 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Schedule/SchedulingObjectTest.php @@ -0,0 +1,378 @@ +markTestSkipped('SQLite driver is not available'); + $this->backend = new Backend\MockScheduling(); + + $this->data = <<data = <<inbox = new Inbox($this->backend, 'principals/user1'); + $this->inbox->createFile('item1.ics', $this->data); + + } + + function teardown() { + + unset($this->inbox); + unset($this->backend); + + } + + function testSetup() { + + $children = $this->inbox->getChildren(); + $this->assertTrue($children[0] instanceof SchedulingObject); + + $this->assertInternalType('string', $children[0]->getName()); + $this->assertInternalType('string', $children[0]->get()); + $this->assertInternalType('string', $children[0]->getETag()); + $this->assertEquals('text/calendar; charset=utf-8', $children[0]->getContentType()); + + } + + /** + * @expectedException InvalidArgumentException + */ + function testInvalidArg1() { + + $obj = new SchedulingObject( + new Backend\MockScheduling([], []), + [], + [] + ); + + } + + /** + * @expectedException InvalidArgumentException + */ + function testInvalidArg2() { + + $obj = new SchedulingObject( + new Backend\MockScheduling([], []), + [], + ['calendarid' => '1'] + ); + + } + + /** + * @depends testSetup + * @expectedException \Sabre\DAV\Exception\MethodNotAllowed + */ + function testPut() { + + $children = $this->inbox->getChildren(); + $this->assertTrue($children[0] instanceof SchedulingObject); + + $children[0]->put(''); + + } + + /** + * @depends testSetup + */ + function testDelete() { + + $children = $this->inbox->getChildren(); + $this->assertTrue($children[0] instanceof SchedulingObject); + + $obj = $children[0]; + $obj->delete(); + + $children2 = $this->inbox->getChildren(); + $this->assertEquals(count($children) - 1, count($children2)); + + } + + /** + * @depends testSetup + */ + function testGetLastModified() { + + $children = $this->inbox->getChildren(); + $this->assertTrue($children[0] instanceof SchedulingObject); + + $obj = $children[0]; + + $lastMod = $obj->getLastModified(); + $this->assertTrue(is_int($lastMod) || ctype_digit($lastMod) || is_null($lastMod)); + + } + + /** + * @depends testSetup + */ + function testGetSize() { + + $children = $this->inbox->getChildren(); + $this->assertTrue($children[0] instanceof SchedulingObject); + + $obj = $children[0]; + + $size = $obj->getSize(); + $this->assertInternalType('int', $size); + + } + + function testGetOwner() { + + $children = $this->inbox->getChildren(); + $this->assertTrue($children[0] instanceof SchedulingObject); + + $obj = $children[0]; + $this->assertEquals('principals/user1', $obj->getOwner()); + + } + + function testGetGroup() { + + $children = $this->inbox->getChildren(); + $this->assertTrue($children[0] instanceof SchedulingObject); + + $obj = $children[0]; + $this->assertNull($obj->getGroup()); + + } + + function testGetACL() { + + $expected = [ + [ + 'privilege' => '{DAV:}all', + 'principal' => '{DAV:}owner', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}all', + 'principal' => 'principals/user1/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'principals/user1/calendar-proxy-read', + 'protected' => true, + ], + ]; + + $children = $this->inbox->getChildren(); + $this->assertTrue($children[0] instanceof SchedulingObject); + + $obj = $children[0]; + $this->assertEquals($expected, $obj->getACL()); + + } + + function testDefaultACL() { + + $backend = new Backend\MockScheduling([], []); + $calendarObject = new SchedulingObject($backend, ['calendarid' => 1, 'uri' => 'foo', 'principaluri' => 'principals/user1']); + $expected = [ + [ + 'privilege' => '{DAV:}all', + 'principal' => '{DAV:}owner', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}all', + 'principal' => 'principals/user1/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'principals/user1/calendar-proxy-read', + 'protected' => true, + ], + ]; + $this->assertEquals($expected, $calendarObject->getACL()); + + + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + */ + function testSetACL() { + + $children = $this->inbox->getChildren(); + $this->assertTrue($children[0] instanceof SchedulingObject); + + $obj = $children[0]; + $obj->setACL([]); + + } + + function testGet() { + + $children = $this->inbox->getChildren(); + $this->assertTrue($children[0] instanceof SchedulingObject); + + $obj = $children[0]; + + $this->assertEquals($this->data, $obj->get()); + + } + + function testGetRefetch() { + + $backend = new Backend\MockScheduling(); + $backend->createSchedulingObject('principals/user1', 'foo', 'foo'); + + $obj = new SchedulingObject($backend, [ + 'calendarid' => 1, + 'uri' => 'foo', + 'principaluri' => 'principals/user1', + ]); + + $this->assertEquals('foo', $obj->get()); + + } + + function testGetEtag1() { + + $objectInfo = [ + 'calendardata' => 'foo', + 'uri' => 'foo', + 'etag' => 'bar', + 'calendarid' => 1 + ]; + + $backend = new Backend\MockScheduling([], []); + $obj = new SchedulingObject($backend, $objectInfo); + + $this->assertEquals('bar', $obj->getETag()); + + } + + function testGetEtag2() { + + $objectInfo = [ + 'calendardata' => 'foo', + 'uri' => 'foo', + 'calendarid' => 1 + ]; + + $backend = new Backend\MockScheduling([], []); + $obj = new SchedulingObject($backend, $objectInfo); + + $this->assertEquals('"' . md5('foo') . '"', $obj->getETag()); + + } + + function testGetSupportedPrivilegesSet() { + + $objectInfo = [ + 'calendardata' => 'foo', + 'uri' => 'foo', + 'calendarid' => 1 + ]; + + $backend = new Backend\MockScheduling([], []); + $obj = new SchedulingObject($backend, $objectInfo); + $this->assertNull($obj->getSupportedPrivilegeSet()); + + } + + function testGetSize1() { + + $objectInfo = [ + 'calendardata' => 'foo', + 'uri' => 'foo', + 'calendarid' => 1 + ]; + + $backend = new Backend\MockScheduling([], []); + $obj = new SchedulingObject($backend, $objectInfo); + $this->assertEquals(3, $obj->getSize()); + + } + + function testGetSize2() { + + $objectInfo = [ + 'uri' => 'foo', + 'calendarid' => 1, + 'size' => 4, + ]; + + $backend = new Backend\MockScheduling([], []); + $obj = new SchedulingObject($backend, $objectInfo); + $this->assertEquals(4, $obj->getSize()); + + } + + function testGetContentType() { + + $objectInfo = [ + 'uri' => 'foo', + 'calendarid' => 1, + ]; + + $backend = new Backend\MockScheduling([], []); + $obj = new SchedulingObject($backend, $objectInfo); + $this->assertEquals('text/calendar; charset=utf-8', $obj->getContentType()); + + } + + function testGetContentType2() { + + $objectInfo = [ + 'uri' => 'foo', + 'calendarid' => 1, + 'component' => 'VEVENT', + ]; + + $backend = new Backend\MockScheduling([], []); + $obj = new SchedulingObject($backend, $objectInfo); + $this->assertEquals('text/calendar; charset=utf-8; component=VEVENT', $obj->getContentType()); + + } + function testGetACL2() { + + $objectInfo = [ + 'uri' => 'foo', + 'calendarid' => 1, + 'acl' => [], + ]; + + $backend = new Backend\MockScheduling([], []); + $obj = new SchedulingObject($backend, $objectInfo); + $this->assertEquals([], $obj->getACL()); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/SharedCalendarTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/SharedCalendarTest.php new file mode 100644 index 00000000000..f71c195238c --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/SharedCalendarTest.php @@ -0,0 +1,176 @@ + 1, + '{http://calendarserver.org/ns/}shared-url' => 'calendars/owner/original', + '{http://sabredav.org/ns}owner-principal' => 'principals/owner', + '{http://sabredav.org/ns}read-only' => false, + 'share-access' => Sharing\Plugin::ACCESS_READWRITE, + 'principaluri' => 'principals/sharee', + ]; + } + + $this->backend = new Backend\MockSharing( + [$props], + [], + [] + ); + + $sharee = new Sharee(); + $sharee->href = 'mailto:removeme@example.org'; + $sharee->properties['{DAV:}displayname'] = 'To be removed'; + $sharee->access = Sharing\Plugin::ACCESS_READ; + $this->backend->updateInvites(1, [$sharee]); + + return new SharedCalendar($this->backend, $props); + + } + + function testGetInvites() { + + $sharee = new Sharee(); + $sharee->href = 'mailto:removeme@example.org'; + $sharee->properties['{DAV:}displayname'] = 'To be removed'; + $sharee->access = Sharing\Plugin::ACCESS_READ; + $sharee->inviteStatus = Sharing\Plugin::INVITE_NORESPONSE; + + $this->assertEquals( + [$sharee], + $this->getInstance()->getInvites() + ); + + } + + function testGetOwner() { + $this->assertEquals('principals/sharee', $this->getInstance()->getOwner()); + } + + function testGetACL() { + + $expected = [ + [ + 'privilege' => '{DAV:}write', + 'principal' => 'principals/sharee', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write', + 'principal' => 'principals/sharee/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write-properties', + 'principal' => 'principals/sharee', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write-properties', + 'principal' => 'principals/sharee/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'principals/sharee', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'principals/sharee/calendar-proxy-read', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'principals/sharee/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{' . Plugin::NS_CALDAV . '}read-free-busy', + 'principal' => '{DAV:}authenticated', + 'protected' => true, + ], + ]; + + $this->assertEquals($expected, $this->getInstance()->getACL()); + + } + + function testGetChildACL() { + + $expected = [ + [ + 'privilege' => '{DAV:}write', + 'principal' => 'principals/sharee', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write', + 'principal' => 'principals/sharee/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'principals/sharee', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'principals/sharee/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'principals/sharee/calendar-proxy-read', + 'protected' => true, + ], + + ]; + + $this->assertEquals($expected, $this->getInstance()->getChildACL()); + + } + + function testUpdateInvites() { + + $instance = $this->getInstance(); + $newSharees = [ + new Sharee(), + new Sharee() + ]; + $newSharees[0]->href = 'mailto:test@example.org'; + $newSharees[0]->properties['{DAV:}displayname'] = 'Foo Bar'; + $newSharees[0]->comment = 'Booh'; + $newSharees[0]->access = Sharing\Plugin::ACCESS_READWRITE; + + $newSharees[1]->href = 'mailto:removeme@example.org'; + $newSharees[1]->access = Sharing\Plugin::ACCESS_NOACCESS; + + $instance->updateInvites($newSharees); + + $expected = [ + clone $newSharees[0] + ]; + $expected[0]->inviteStatus = Sharing\Plugin::INVITE_NORESPONSE; + $this->assertEquals($expected, $instance->getInvites()); + + } + + function testPublish() { + + $instance = $this->getInstance(); + $this->assertNull($instance->setPublishStatus(true)); + $this->assertNull($instance->setPublishStatus(false)); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/SharingPluginTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/SharingPluginTest.php new file mode 100644 index 00000000000..9589176a368 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/SharingPluginTest.php @@ -0,0 +1,396 @@ +caldavCalendars = [ + [ + 'principaluri' => 'principals/user1', + 'id' => 1, + 'uri' => 'cal1', + ], + [ + 'principaluri' => 'principals/user1', + 'id' => 2, + 'uri' => 'cal2', + 'share-access' => \Sabre\DAV\Sharing\Plugin::ACCESS_READWRITE, + ], + [ + 'principaluri' => 'principals/user1', + 'id' => 3, + 'uri' => 'cal3', + ], + ]; + + parent::setUp(); + + // Making the logged in user an admin, for full access: + $this->aclPlugin->adminPrincipals[] = 'principals/user2'; + + } + + function testSimple() { + + $this->assertInstanceOf('Sabre\\CalDAV\\SharingPlugin', $this->server->getPlugin('caldav-sharing')); + $this->assertEquals( + 'caldav-sharing', + $this->caldavSharingPlugin->getPluginInfo()['name'] + ); + + } + + /** + * @expectedException \LogicException + */ + function testSetupWithoutCoreSharingPlugin() { + + $server = new DAV\Server(); + $server->addPlugin( + new SharingPlugin() + ); + + } + + function testGetFeatures() { + + $this->assertEquals(['calendarserver-sharing'], $this->caldavSharingPlugin->getFeatures()); + + } + + function testBeforeGetShareableCalendar() { + + // Forcing the server to authenticate: + $this->authPlugin->beforeMethod(new HTTP\Request(), new HTTP\Response()); + $props = $this->server->getProperties('calendars/user1/cal1', [ + '{' . Plugin::NS_CALENDARSERVER . '}invite', + '{' . Plugin::NS_CALENDARSERVER . '}allowed-sharing-modes', + ]); + + $this->assertInstanceOf('Sabre\\CalDAV\\Xml\\Property\\Invite', $props['{' . Plugin::NS_CALENDARSERVER . '}invite']); + $this->assertInstanceOf('Sabre\\CalDAV\\Xml\\Property\\AllowedSharingModes', $props['{' . Plugin::NS_CALENDARSERVER . '}allowed-sharing-modes']); + + } + + function testBeforeGetSharedCalendar() { + + $props = $this->server->getProperties('calendars/user1/cal2', [ + '{' . Plugin::NS_CALENDARSERVER . '}shared-url', + '{' . Plugin::NS_CALENDARSERVER . '}invite', + ]); + + $this->assertInstanceOf('Sabre\\CalDAV\\Xml\\Property\\Invite', $props['{' . Plugin::NS_CALENDARSERVER . '}invite']); + //$this->assertInstanceOf('Sabre\\DAV\\Xml\\Property\\Href', $props['{' . Plugin::NS_CALENDARSERVER . '}shared-url']); + + } + + function testUpdateResourceType() { + + $this->caldavBackend->updateInvites(1, + [ + new Sharee([ + 'href' => 'mailto:joe@example.org', + ]) + ] + ); + $result = $this->server->updateProperties('calendars/user1/cal1', [ + '{DAV:}resourcetype' => new DAV\Xml\Property\ResourceType(['{DAV:}collection']) + ]); + + $this->assertEquals([ + '{DAV:}resourcetype' => 200 + ], $result); + + $this->assertEquals(0, count($this->caldavBackend->getInvites(1))); + + } + + function testUpdatePropertiesPassThru() { + + $result = $this->server->updateProperties('calendars/user1/cal3', [ + '{DAV:}foo' => 'bar', + ]); + + $this->assertEquals([ + '{DAV:}foo' => 200, + ], $result); + + } + + function testUnknownMethodNoPOST() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'PATCH', + 'REQUEST_URI' => '/', + ]); + + $response = $this->request($request); + + $this->assertEquals(501, $response->status, $response->body); + + } + + function testUnknownMethodNoXML() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'POST', + 'REQUEST_URI' => '/', + 'CONTENT_TYPE' => 'text/plain', + ]); + + $response = $this->request($request); + + $this->assertEquals(501, $response->status, $response->body); + + } + + function testUnknownMethodNoNode() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'POST', + 'REQUEST_URI' => '/foo', + 'CONTENT_TYPE' => 'text/xml', + ]); + + $response = $this->request($request); + + $this->assertEquals(501, $response->status, $response->body); + + } + + function testShareRequest() { + + $request = new HTTP\Request('POST', '/calendars/user1/cal1', ['Content-Type' => 'text/xml']); + + $xml = << + + + mailto:joe@example.org + Joe Shmoe + + + + mailto:nancy@example.org + + +RRR; + + $request->setBody($xml); + + $response = $this->request($request, 200); + + $this->assertEquals( + [ + new Sharee([ + 'href' => 'mailto:joe@example.org', + 'properties' => [ + '{DAV:}displayname' => 'Joe Shmoe', + ], + 'access' => \Sabre\DAV\Sharing\Plugin::ACCESS_READWRITE, + 'inviteStatus' => \Sabre\DAV\Sharing\Plugin::INVITE_NORESPONSE, + 'comment' => '', + ]), + ], + $this->caldavBackend->getInvites(1) + ); + + // Wiping out tree cache + $this->server->tree->markDirty(''); + + // Verifying that the calendar is now marked shared. + $props = $this->server->getProperties('calendars/user1/cal1', ['{DAV:}resourcetype']); + $this->assertTrue( + $props['{DAV:}resourcetype']->is('{http://calendarserver.org/ns/}shared-owner') + ); + + } + + function testShareRequestNoShareableCalendar() { + + $request = new HTTP\Request( + 'POST', + '/calendars/user1/cal2', + ['Content-Type' => 'text/xml'] + ); + + $xml = ' + + + mailto:joe@example.org + Joe Shmoe + + + + mailto:nancy@example.org + + +'; + + $request->setBody($xml); + + $response = $this->request($request, 403); + + } + + function testInviteReply() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'POST', + 'REQUEST_URI' => '/calendars/user1', + 'CONTENT_TYPE' => 'text/xml', + ]); + + $xml = ' + + /principals/owner + + +'; + + $request->setBody($xml); + $response = $this->request($request); + $this->assertEquals(200, $response->status, $response->body); + + } + + function testInviteBadXML() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'POST', + 'REQUEST_URI' => '/calendars/user1', + 'CONTENT_TYPE' => 'text/xml', + ]); + + $xml = ' + + +'; + $request->setBody($xml); + $response = $this->request($request); + $this->assertEquals(400, $response->status, $response->body); + + } + + function testInviteWrongUrl() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'POST', + 'REQUEST_URI' => '/calendars/user1/cal1', + 'CONTENT_TYPE' => 'text/xml', + ]); + + $xml = ' + + /principals/owner + +'; + $request->setBody($xml); + $response = $this->request($request); + $this->assertEquals(501, $response->status, $response->body); + + // If the plugin did not handle this request, it must ensure that the + // body is still accessible by other plugins. + $this->assertEquals($xml, $request->getBody(true)); + + } + + function testPublish() { + + $request = new HTTP\Request('POST', '/calendars/user1/cal1', ['Content-Type' => 'text/xml']); + + $xml = ' + +'; + + $request->setBody($xml); + + $response = $this->request($request); + $this->assertEquals(202, $response->status, $response->body); + + } + + + function testUnpublish() { + + $request = new HTTP\Request( + 'POST', + '/calendars/user1/cal1', + ['Content-Type' => 'text/xml'] + ); + + $xml = ' + +'; + + $request->setBody($xml); + + $response = $this->request($request); + $this->assertEquals(200, $response->status, $response->body); + + } + + function testPublishWrongUrl() { + + $request = new HTTP\Request( + 'POST', + '/calendars/user1', + ['Content-Type' => 'text/xml'] + ); + + $xml = ' + +'; + + $request->setBody($xml); + $this->request($request, 501); + + } + + function testUnpublishWrongUrl() { + + $request = new HTTP\Request( + 'POST', + '/calendars/user1', + ['Content-Type' => 'text/xml'] + ); + $xml = ' + +'; + + $request->setBody($xml); + + $this->request($request, 501); + + } + + function testUnknownXmlDoc() { + + + $request = new HTTP\Request( + 'POST', + '/calendars/user1/cal2', + ['Content-Type' => 'text/xml'] + ); + + $xml = ' +'; + + $request->setBody($xml); + + $response = $this->request($request); + $this->assertEquals(501, $response->status, $response->body); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Subscriptions/CreateSubscriptionTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Subscriptions/CreateSubscriptionTest.php new file mode 100644 index 00000000000..8ad0f8ac574 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Subscriptions/CreateSubscriptionTest.php @@ -0,0 +1,123 @@ + + + + + + + + + + #1C4587FF + Jewish holidays + Foo + 19 + + webcal://www.example.org/ + + P1W + + + + +XML; + + $headers = [ + 'Content-Type' => 'application/xml', + ]; + $request = new Request('MKCOL', '/calendars/user1/subscription1', $headers, $body); + + $response = $this->request($request); + $this->assertEquals(201, $response->getStatus()); + $subscriptions = $this->caldavBackend->getSubscriptionsForUser('principals/user1'); + $this->assertSubscription($subscriptions[0]); + + + } + /** + * OS X 10.9.2 and up + */ + function testMKCALENDAR() { + + $body = << + + + + + + + + + + + + P1W + + webcal://www.example.org/ + + #1C4587FF + 19 + Foo + + Jewish holidays + + + +XML; + + $headers = [ + 'Content-Type' => 'application/xml', + ]; + $request = new Request('MKCALENDAR', '/calendars/user1/subscription1', $headers, $body); + + $response = $this->request($request); + $this->assertEquals(201, $response->getStatus()); + $subscriptions = $this->caldavBackend->getSubscriptionsForUser('principals/user1'); + $this->assertSubscription($subscriptions[0]); + + // Also seeing if it works when calling this as a PROPFIND. + $this->assertEquals([ + '{http://calendarserver.org/ns/}subscribed-strip-alarms' => '', + ], + $this->server->getProperties('calendars/user1/subscription1', ['{http://calendarserver.org/ns/}subscribed-strip-alarms']) + ); + + + } + + function assertSubscription($subscription) { + + $this->assertEquals('', $subscription['{http://calendarserver.org/ns/}subscribed-strip-attachments']); + $this->assertEquals('', $subscription['{http://calendarserver.org/ns/}subscribed-strip-todos']); + $this->assertEquals('#1C4587FF', $subscription['{http://apple.com/ns/ical/}calendar-color']); + $this->assertEquals('Jewish holidays', $subscription['{DAV:}displayname']); + $this->assertEquals('Foo', $subscription['{urn:ietf:params:xml:ns:caldav}calendar-description']); + $this->assertEquals('19', $subscription['{http://apple.com/ns/ical/}calendar-order']); + $this->assertEquals('webcal://www.example.org/', $subscription['{http://calendarserver.org/ns/}source']->getHref()); + $this->assertEquals('P1W', $subscription['{http://apple.com/ns/ical/}refreshrate']); + $this->assertEquals('subscription1', $subscription['uri']); + $this->assertEquals('principals/user1', $subscription['principaluri']); + $this->assertEquals('webcal://www.example.org/', $subscription['source']); + $this->assertEquals(['principals/user1', 1], $subscription['id']); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Subscriptions/PluginTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Subscriptions/PluginTest.php new file mode 100644 index 00000000000..dc6d2d5f04c --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Subscriptions/PluginTest.php @@ -0,0 +1,50 @@ +addPlugin($plugin); + + $this->assertEquals( + '{http://calendarserver.org/ns/}subscribed', + $server->resourceTypeMapping['Sabre\\CalDAV\\Subscriptions\\ISubscription'] + ); + $this->assertEquals( + 'Sabre\\DAV\\Xml\\Property\\Href', + $server->xml->elementMap['{http://calendarserver.org/ns/}source'] + ); + + $this->assertEquals( + ['calendarserver-subscribed'], + $plugin->getFeatures() + ); + + $this->assertEquals( + 'subscriptions', + $plugin->getPluginInfo()['name'] + ); + + } + + function testPropFind() { + + $propName = '{http://calendarserver.org/ns/}subscribed-strip-alarms'; + $propFind = new PropFind('foo', [$propName]); + $propFind->set($propName, null, 200); + + $plugin = new Plugin(); + $plugin->propFind($propFind, new \Sabre\DAV\SimpleCollection('hi')); + + $this->assertFalse(is_null($propFind->get($propName))); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Subscriptions/SubscriptionTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Subscriptions/SubscriptionTest.php new file mode 100644 index 00000000000..559d526cd1d --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Subscriptions/SubscriptionTest.php @@ -0,0 +1,131 @@ + new Href('http://example.org/src', false), + 'lastmodified' => date('2013-04-06 11:40:00'), // tomorrow is my birthday! + '{DAV:}displayname' => 'displayname', + ]; + + + $id = $caldavBackend->createSubscription('principals/user1', 'uri', array_merge($info, $override)); + $subInfo = $caldavBackend->getSubscriptionsForUser('principals/user1'); + + $this->assertEquals(1, count($subInfo)); + $subscription = new Subscription($caldavBackend, $subInfo[0]); + + $this->backend = $caldavBackend; + return $subscription; + + } + + function testValues() { + + $sub = $this->getSub(); + + $this->assertEquals('uri', $sub->getName()); + $this->assertEquals(date('2013-04-06 11:40:00'), $sub->getLastModified()); + $this->assertEquals([], $sub->getChildren()); + + $this->assertEquals( + [ + '{DAV:}displayname' => 'displayname', + '{http://calendarserver.org/ns/}source' => new Href('http://example.org/src', false), + ], + $sub->getProperties(['{DAV:}displayname', '{http://calendarserver.org/ns/}source']) + ); + + $this->assertEquals('principals/user1', $sub->getOwner()); + $this->assertNull($sub->getGroup()); + + $acl = [ + [ + 'privilege' => '{DAV:}all', + 'principal' => 'principals/user1', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}all', + 'principal' => 'principals/user1/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'principals/user1/calendar-proxy-read', + 'protected' => true, + ] + ]; + $this->assertEquals($acl, $sub->getACL()); + + $this->assertNull($sub->getSupportedPrivilegeSet()); + + } + + function testValues2() { + + $sub = $this->getSub([ + 'lastmodified' => null, + ]); + + $this->assertEquals(null, $sub->getLastModified()); + + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + */ + function testSetACL() { + + $sub = $this->getSub(); + $sub->setACL([]); + + } + + function testDelete() { + + $sub = $this->getSub(); + $sub->delete(); + + $this->assertEquals([], $this->backend->getSubscriptionsForUser('principals1/user1')); + + } + + function testUpdateProperties() { + + $sub = $this->getSub(); + $propPatch = new PropPatch([ + '{DAV:}displayname' => 'foo', + ]); + $sub->propPatch($propPatch); + $this->assertTrue($propPatch->commit()); + + $this->assertEquals( + 'foo', + $this->backend->getSubscriptionsForUser('principals/user1')[0]['{DAV:}displayname'] + ); + + } + + /** + * @expectedException \InvalidArgumentException + */ + function testBadConstruct() { + + $caldavBackend = new \Sabre\CalDAV\Backend\MockSubscriptionSupport([], []); + new Subscription($caldavBackend, []); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/TestUtil.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/TestUtil.php new file mode 100644 index 00000000000..673d39c0aa5 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/TestUtil.php @@ -0,0 +1,189 @@ +createCalendar( + 'principals/user1', + 'UUID-123467', + [ + '{DAV:}displayname' => 'user1 calendar', + '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'Calendar description', + '{http://apple.com/ns/ical/}calendar-order' => '1', + '{http://apple.com/ns/ical/}calendar-color' => '#FF0000', + ] + ); + $backend->createCalendar( + 'principals/user1', + 'UUID-123468', + [ + '{DAV:}displayname' => 'user1 calendar2', + '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'Calendar description', + '{http://apple.com/ns/ical/}calendar-order' => '1', + '{http://apple.com/ns/ical/}calendar-color' => '#FF0000', + ] + ); + $backend->createCalendarObject($calendarId, 'UUID-2345', self::getTestCalendarData()); + return $backend; + + } + + static function getTestCalendarData($type = 1) { + + $calendarData = 'BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//iCal 4.0.1//EN +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Asia/Seoul +BEGIN:DAYLIGHT +TZOFFSETFROM:+0900 +RRULE:FREQ=YEARLY;UNTIL=19880507T150000Z;BYMONTH=5;BYDAY=2SU +DTSTART:19870510T000000 +TZNAME:GMT+09:00 +TZOFFSETTO:+1000 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+1000 +DTSTART:19881009T000000 +TZNAME:GMT+09:00 +TZOFFSETTO:+0900 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20100225T154229Z +UID:39A6B5ED-DD51-4AFE-A683-C35EE3749627 +TRANSP:TRANSPARENT +SUMMARY:Something here +DTSTAMP:20100228T130202Z'; + + switch ($type) { + case 1 : + $calendarData .= "\nDTSTART;TZID=Asia/Seoul:20100223T060000\nDTEND;TZID=Asia/Seoul:20100223T070000\n"; + break; + case 2 : + $calendarData .= "\nDTSTART:20100223T060000\nDTEND:20100223T070000\n"; + break; + case 3 : + $calendarData .= "\nDTSTART;VALUE=DATE:20100223\nDTEND;VALUE=DATE:20100223\n"; + break; + case 4 : + $calendarData .= "\nDTSTART;TZID=Asia/Seoul:20100223T060000\nDURATION:PT1H\n"; + break; + case 5 : + $calendarData .= "\nDTSTART;TZID=Asia/Seoul:20100223T060000\nDURATION:-P5D\n"; + break; + case 6 : + $calendarData .= "\nDTSTART;VALUE=DATE:20100223\n"; + break; + case 7 : + $calendarData .= "\nDTSTART;VALUE=DATETIME:20100223T060000\n"; + break; + + // No DTSTART, so intentionally broken + case 'X' : + $calendarData .= "\n"; + break; + } + + + $calendarData .= 'ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com +SEQUENCE:2 +END:VEVENT +END:VCALENDAR'; + + return $calendarData; + + } + + static function getTestTODO($type = 'due') { + + switch ($type) { + + case 'due' : + $extra = "DUE:20100104T000000Z"; + break; + case 'due2' : + $extra = "DUE:20060104T000000Z"; + break; + case 'due_date' : + $extra = "DUE;VALUE=DATE:20060104"; + break; + case 'due_tz' : + $extra = "DUE;TZID=Asia/Seoul:20060104T000000Z"; + break; + case 'due_dtstart' : + $extra = "DTSTART:20050223T060000Z\nDUE:20060104T000000Z"; + break; + case 'due_dtstart2' : + $extra = "DTSTART:20090223T060000Z\nDUE:20100104T000000Z"; + break; + case 'dtstart' : + $extra = 'DTSTART:20100223T060000Z'; + break; + case 'dtstart2' : + $extra = 'DTSTART:20060223T060000Z'; + break; + case 'dtstart_date' : + $extra = 'DTSTART;VALUE=DATE:20100223'; + break; + case 'dtstart_tz' : + $extra = 'DTSTART;TZID=Asia/Seoul:20100223T060000Z'; + break; + case 'dtstart_duration' : + $extra = "DTSTART:20061023T060000Z\nDURATION:PT1H"; + break; + case 'dtstart_duration2' : + $extra = "DTSTART:20101023T060000Z\nDURATION:PT1H"; + break; + case 'completed' : + $extra = 'COMPLETED:20060601T000000Z'; + break; + case 'completed2' : + $extra = 'COMPLETED:20090601T000000Z'; + break; + case 'created' : + $extra = 'CREATED:20060601T000000Z'; + break; + case 'created2' : + $extra = 'CREATED:20090601T000000Z'; + break; + case 'completedcreated' : + $extra = "CREATED:20060601T000000Z\nCOMPLETED:20070101T000000Z"; + break; + case 'completedcreated2' : + $extra = "CREATED:20090601T000000Z\nCOMPLETED:20100101T000000Z"; + break; + case 'notime' : + $extra = 'X-FILLER:oh hello'; + break; + default : + throw new Exception('Unknown type: ' . $type); + + } + + $todo = 'BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Example Corp.//CalDAV Client//EN +BEGIN:VTODO +DTSTAMP:20060205T235335Z +' . $extra . ' +STATUS:NEEDS-ACTION +SUMMARY:Task #1 +UID:DDDEEB7915FA61233B861457@example.com +BEGIN:VALARM +ACTION:AUDIO +TRIGGER;RELATED=START:-PT10M +END:VALARM +END:VTODO +END:VCALENDAR'; + + return $todo; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/ValidateICalTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/ValidateICalTest.php new file mode 100644 index 00000000000..629df90c119 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/ValidateICalTest.php @@ -0,0 +1,406 @@ + 'calendar1', + 'principaluri' => 'principals/admin', + 'uri' => 'calendar1', + '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => new Xml\Property\SupportedCalendarComponentSet(['VEVENT', 'VTODO', 'VJOURNAL']), + ], + [ + 'id' => 'calendar2', + 'principaluri' => 'principals/admin', + 'uri' => 'calendar2', + '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => new Xml\Property\SupportedCalendarComponentSet(['VTODO', 'VJOURNAL']), + ] + ]; + + $this->calBackend = new Backend\Mock($calendars, []); + $principalBackend = new DAVACL\PrincipalBackend\Mock(); + + $tree = [ + new CalendarRoot($principalBackend, $this->calBackend), + ]; + + $this->server = new DAV\Server($tree); + $this->server->sapi = new HTTP\SapiMock(); + $this->server->debugExceptions = true; + + $plugin = new Plugin(); + $this->server->addPlugin($plugin); + + $response = new HTTP\ResponseMock(); + $this->server->httpResponse = $response; + + } + + function request(HTTP\Request $request) { + + $this->server->httpRequest = $request; + $this->server->exec(); + + return $this->server->httpResponse; + + } + + function testCreateFile() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'PUT', + 'REQUEST_URI' => '/calendars/admin/calendar1/blabla.ics', + ]); + + $response = $this->request($request); + + $this->assertEquals(415, $response->status); + + } + + function testCreateFileValid() { + + $request = new HTTP\Request( + 'PUT', + '/calendars/admin/calendar1/blabla.ics', + ['Prefer' => 'handling=strict'] + ); + + $ics = <<setBody($ics); + + $response = $this->request($request); + + $this->assertEquals(201, $response->status, 'Incorrect status returned! Full response body: ' . $response->body); + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Length' => ['0'], + 'ETag' => ['"' . md5($ics) . '"'], + ], $response->getHeaders()); + + $expected = [ + 'uri' => 'blabla.ics', + 'calendardata' => $ics, + 'calendarid' => 'calendar1', + 'lastmodified' => null, + ]; + + $this->assertEquals($expected, $this->calBackend->getCalendarObject('calendar1', 'blabla.ics')); + + } + + function testCreateFileNoVersion() { + + $request = new HTTP\Request( + 'PUT', + '/calendars/admin/calendar1/blabla.ics', + ['Prefer' => 'handling=strict'] + ); + + $ics = <<setBody($ics); + + $response = $this->request($request); + + $this->assertEquals(415, $response->status, 'Incorrect status returned! Full response body: ' . $response->body); + + } + + function testCreateFileNoVersionFixed() { + + $request = new HTTP\Request( + 'PUT', + '/calendars/admin/calendar1/blabla.ics', + ['Prefer' => 'handling=lenient'] + ); + + $ics = <<setBody($ics); + + $response = $this->request($request); + + $this->assertEquals(201, $response->status, 'Incorrect status returned! Full response body: ' . $response->body); + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Length' => ['0'], + 'X-Sabre-Ew-Gross' => ['iCalendar validation warning: VERSION MUST appear exactly once in a VCALENDAR component'], + ], $response->getHeaders()); + + $ics = << 'blabla.ics', + 'calendardata' => $ics, + 'calendarid' => 'calendar1', + 'lastmodified' => null, + ]; + + $this->assertEquals($expected, $this->calBackend->getCalendarObject('calendar1', 'blabla.ics')); + + } + + function testCreateFileNoComponents() { + + $request = new HTTP\Request( + 'PUT', + '/calendars/admin/calendar1/blabla.ics', + ['Prefer' => 'handling=strict'] + ); + $ics = <<setBody($ics); + + $response = $this->request($request); + $this->assertEquals(403, $response->status, 'Incorrect status returned! Full response body: ' . $response->body); + + } + + function testCreateFileNoUID() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'PUT', + 'REQUEST_URI' => '/calendars/admin/calendar1/blabla.ics', + ]); + $request->setBody("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"); + + $response = $this->request($request); + + $this->assertEquals(415, $response->status, 'Incorrect status returned! Full response body: ' . $response->body); + + } + + function testCreateFileVCard() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'PUT', + 'REQUEST_URI' => '/calendars/admin/calendar1/blabla.ics', + ]); + $request->setBody("BEGIN:VCARD\r\nEND:VCARD\r\n"); + + $response = $this->request($request); + + $this->assertEquals(415, $response->status, 'Incorrect status returned! Full response body: ' . $response->body); + + } + + function testCreateFile2Components() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'PUT', + 'REQUEST_URI' => '/calendars/admin/calendar1/blabla.ics', + ]); + $request->setBody("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nUID:foo\r\nEND:VEVENT\r\nBEGIN:VJOURNAL\r\nUID:foo\r\nEND:VJOURNAL\r\nEND:VCALENDAR\r\n"); + + $response = $this->request($request); + + $this->assertEquals(415, $response->status, 'Incorrect status returned! Full response body: ' . $response->body); + + } + + function testCreateFile2UIDS() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'PUT', + 'REQUEST_URI' => '/calendars/admin/calendar1/blabla.ics', + ]); + $request->setBody("BEGIN:VCALENDAR\r\nBEGIN:VTIMEZONE\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nUID:foo\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:bar\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"); + + $response = $this->request($request); + + $this->assertEquals(415, $response->status, 'Incorrect status returned! Full response body: ' . $response->body); + + } + + function testCreateFileWrongComponent() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'PUT', + 'REQUEST_URI' => '/calendars/admin/calendar1/blabla.ics', + ]); + $request->setBody("BEGIN:VCALENDAR\r\nBEGIN:VTIMEZONE\r\nEND:VTIMEZONE\r\nBEGIN:VFREEBUSY\r\nUID:foo\r\nEND:VFREEBUSY\r\nEND:VCALENDAR\r\n"); + + $response = $this->request($request); + + $this->assertEquals(403, $response->status, 'Incorrect status returned! Full response body: ' . $response->body); + + } + + function testUpdateFile() { + + $this->calBackend->createCalendarObject('calendar1', 'blabla.ics', 'foo'); + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'PUT', + 'REQUEST_URI' => '/calendars/admin/calendar1/blabla.ics', + ]); + + $response = $this->request($request); + + $this->assertEquals(415, $response->status); + + } + + function testUpdateFileParsableBody() { + + $this->calBackend->createCalendarObject('calendar1', 'blabla.ics', 'foo'); + $request = new HTTP\Request( + 'PUT', + '/calendars/admin/calendar1/blabla.ics' + ); + $ics = <<setBody($ics); + $response = $this->request($request); + + $this->assertEquals(204, $response->status); + + $expected = [ + 'uri' => 'blabla.ics', + 'calendardata' => $ics, + 'calendarid' => 'calendar1', + 'lastmodified' => null, + ]; + + $this->assertEquals($expected, $this->calBackend->getCalendarObject('calendar1', 'blabla.ics')); + + } + + function testCreateFileInvalidComponent() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'PUT', + 'REQUEST_URI' => '/calendars/admin/calendar2/blabla.ics', + ]); + $request->setBody("BEGIN:VCALENDAR\r\nBEGIN:VTIMEZONE\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nUID:foo\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"); + + $response = $this->request($request); + + $this->assertEquals(403, $response->status, 'Incorrect status returned! Full response body: ' . $response->body); + + } + + function testUpdateFileInvalidComponent() { + + $this->calBackend->createCalendarObject('calendar2', 'blabla.ics', 'foo'); + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'PUT', + 'REQUEST_URI' => '/calendars/admin/calendar2/blabla.ics', + ]); + $request->setBody("BEGIN:VCALENDAR\r\nBEGIN:VTIMEZONE\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nUID:foo\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"); + + $response = $this->request($request); + + $this->assertEquals(403, $response->status, 'Incorrect status returned! Full response body: ' . $response->body); + + } + + /** + * What we are testing here, is if we send in a latin1 character, the + * server should automatically transform this into UTF-8. + * + * More importantly. If any transformation happens, the etag must no longer + * be returned by the server. + */ + function testCreateFileModified() { + + $request = new HTTP\Request( + 'PUT', + '/calendars/admin/calendar1/blabla.ics' + ); + $ics = <<setBody($ics); + + $response = $this->request($request); + + $this->assertEquals(201, $response->status, 'Incorrect status returned! Full response body: ' . $response->body); + $this->assertNull($response->getHeader('ETag')); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Notification/InviteReplyTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Notification/InviteReplyTest.php new file mode 100644 index 00000000000..cd700893d67 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Notification/InviteReplyTest.php @@ -0,0 +1,146 @@ +assertEquals('foo', $notification->getId()); + $this->assertEquals('"1"', $notification->getETag()); + + $simpleExpected = '' . "\n" . ''; + + $writer = new Writer(); + $writer->namespaceMap = [ + 'http://calendarserver.org/ns/' => 'cs', + ]; + $writer->openMemory(); + $writer->startDocument('1.0', 'UTF-8'); + $writer->startElement('{http://calendarserver.org/ns/}root'); + $writer->write($notification); + $writer->endElement(); + + $this->assertEquals($simpleExpected, $writer->outputMemory()); + + $writer = new Writer(); + $writer->contextUri = '/'; + $writer->namespaceMap = [ + 'http://calendarserver.org/ns/' => 'cs', + 'DAV:' => 'd', + ]; + $writer->openMemory(); + $writer->startDocument('1.0', 'UTF-8'); + $writer->startElement('{http://calendarserver.org/ns/}root'); + $notification->xmlSerializeFull($writer); + $writer->endElement(); + + $this->assertXmlStringEqualsXmlString($expected, $writer->outputMemory()); + + + } + + function dataProvider() { + + $dtStamp = new \DateTime('2012-01-01 00:00:00 GMT'); + return [ + [ + [ + 'id' => 'foo', + 'dtStamp' => $dtStamp, + 'etag' => '"1"', + 'inReplyTo' => 'bar', + 'href' => 'mailto:foo@example.org', + 'type' => DAV\Sharing\Plugin::INVITE_ACCEPTED, + 'hostUrl' => 'calendar' + ], +<< + + 20120101T000000Z + + foo + bar + mailto:foo@example.org + + + /calendar + + + + +FOO + ], + [ + [ + 'id' => 'foo', + 'dtStamp' => $dtStamp, + 'etag' => '"1"', + 'inReplyTo' => 'bar', + 'href' => 'mailto:foo@example.org', + 'type' => DAV\Sharing\Plugin::INVITE_DECLINED, + 'hostUrl' => 'calendar', + 'summary' => 'Summary!' + ], +<< + + 20120101T000000Z + + foo + bar + mailto:foo@example.org + + + /calendar + + Summary! + + + +FOO + ], + + ]; + + } + + /** + * @expectedException InvalidArgumentException + */ + function testMissingArg() { + + new InviteReply([]); + + } + + /** + * @expectedException InvalidArgumentException + */ + function testUnknownArg() { + + new InviteReply([ + 'foo-i-will-break' => true, + + 'id' => 1, + 'etag' => '"bla"', + 'href' => 'abc', + 'dtStamp' => 'def', + 'inReplyTo' => 'qrs', + 'type' => 'ghi', + 'hostUrl' => 'jkl', + ]); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Notification/InviteTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Notification/InviteTest.php new file mode 100644 index 00000000000..f03093916e9 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Notification/InviteTest.php @@ -0,0 +1,165 @@ +assertEquals('foo', $notification->getId()); + $this->assertEquals('"1"', $notification->getETag()); + + $simpleExpected = '' . "\n"; + $this->namespaceMap['http://calendarserver.org/ns/'] = 'cs'; + + $xml = $this->write($notification); + + $this->assertXmlStringEqualsXmlString($simpleExpected, $xml); + + $this->namespaceMap['urn:ietf:params:xml:ns:caldav'] = 'cal'; + $xml = $this->writeFull($notification); + + $this->assertXmlStringEqualsXmlString($expected, $xml); + + + } + + function dataProvider() { + + $dtStamp = new \DateTime('2012-01-01 00:00:00', new \DateTimeZone('GMT')); + return [ + [ + [ + 'id' => 'foo', + 'dtStamp' => $dtStamp, + 'etag' => '"1"', + 'href' => 'mailto:foo@example.org', + 'type' => DAV\Sharing\Plugin::INVITE_ACCEPTED, + 'readOnly' => true, + 'hostUrl' => 'calendar', + 'organizer' => 'principal/user1', + 'commonName' => 'John Doe', + 'summary' => 'Awesome stuff!' + ], +<< + + 20120101T000000Z + + foo + mailto:foo@example.org + + + /calendar + + Awesome stuff! + + + + + /principal/user1 + John Doe + + John Doe + + + +FOO + ], + [ + [ + 'id' => 'foo', + 'dtStamp' => $dtStamp, + 'etag' => '"1"', + 'href' => 'mailto:foo@example.org', + 'type' => DAV\Sharing\Plugin::INVITE_NORESPONSE, + 'readOnly' => true, + 'hostUrl' => 'calendar', + 'organizer' => 'principal/user1', + 'firstName' => 'Foo', + 'lastName' => 'Bar', + ], +<< + + 20120101T000000Z + + foo + mailto:foo@example.org + + + /calendar + + + + + + /principal/user1 + Foo + Bar + + Foo + Bar + + + +FOO + ], + + ]; + + } + + /** + * @expectedException InvalidArgumentException + */ + function testMissingArg() { + + new Invite([]); + + } + + /** + * @expectedException InvalidArgumentException + */ + function testUnknownArg() { + + new Invite([ + 'foo-i-will-break' => true, + + 'id' => 1, + 'etag' => '"bla"', + 'href' => 'abc', + 'dtStamp' => 'def', + 'type' => 'ghi', + 'readOnly' => true, + 'hostUrl' => 'jkl', + 'organizer' => 'mno', + ]); + + } + + function writeFull($input) { + + $writer = new Writer(); + $writer->contextUri = '/'; + $writer->namespaceMap = $this->namespaceMap; + $writer->openMemory(); + $writer->startElement('{http://calendarserver.org/ns/}root'); + $input->xmlSerializeFull($writer); + $writer->endElement(); + return $writer->outputMemory(); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Notification/SystemStatusTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Notification/SystemStatusTest.php new file mode 100644 index 00000000000..1f9034340f5 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Notification/SystemStatusTest.php @@ -0,0 +1,69 @@ +assertEquals('foo', $notification->getId()); + $this->assertEquals('"1"', $notification->getETag()); + + $writer = new Writer(); + $writer->namespaceMap = [ + 'http://calendarserver.org/ns/' => 'cs', + ]; + $writer->openMemory(); + $writer->startDocument('1.0', 'UTF-8'); + $writer->startElement('{http://calendarserver.org/ns/}root'); + $writer->write($notification); + $writer->endElement(); + $this->assertXmlStringEqualsXmlString($expected1, $writer->outputMemory()); + + $writer = new Writer(); + $writer->namespaceMap = [ + 'http://calendarserver.org/ns/' => 'cs', + 'DAV:' => 'd', + ]; + $writer->openMemory(); + $writer->startDocument('1.0', 'UTF-8'); + $writer->startElement('{http://calendarserver.org/ns/}root'); + $notification->xmlSerializeFull($writer); + $writer->endElement(); + $this->assertXmlStringEqualsXmlString($expected2, $writer->outputMemory()); + + } + + function dataProvider() { + + return [ + + [ + new SystemStatus('foo', '"1"'), + '' . "\n" . '' . "\n", + '' . "\n" . '' . "\n", + ], + [ + new SystemStatus('foo', '"1"', SystemStatus::TYPE_MEDIUM, 'bar'), + '' . "\n" . '' . "\n", + '' . "\n" . 'bar' . "\n", + ], + [ + new SystemStatus('foo', '"1"', SystemStatus::TYPE_LOW, null, 'http://example.org/'), + '' . "\n" . '' . "\n", + '' . "\n" . 'http://example.org/' . "\n", + ] + ]; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Property/AllowedSharingModesTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Property/AllowedSharingModesTest.php new file mode 100644 index 00000000000..0602d4f24f4 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Property/AllowedSharingModesTest.php @@ -0,0 +1,38 @@ +assertInstanceOf('Sabre\CalDAV\Xml\Property\AllowedSharingModes', $sccs); + + } + + /** + * @depends testSimple + */ + function testSerialize() { + + $property = new AllowedSharingModes(true, true); + + $this->namespaceMap[CalDAV\Plugin::NS_CALDAV] = 'cal'; + $this->namespaceMap[CalDAV\Plugin::NS_CALENDARSERVER] = 'cs'; + $xml = $this->write(['{DAV:}root' => $property]); + + $this->assertXmlStringEqualsXmlString( +' + + + + +', $xml); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Property/EmailAddressSetTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Property/EmailAddressSetTest.php new file mode 100644 index 00000000000..30651a080e4 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Property/EmailAddressSetTest.php @@ -0,0 +1,40 @@ + 'cs', + 'DAV:' => 'd', + ]; + + function testSimple() { + + $eas = new EmailAddressSet(['foo@example.org']); + $this->assertEquals(['foo@example.org'], $eas->getValue()); + + } + + /** + * @depends testSimple + */ + function testSerialize() { + + $property = new EmailAddressSet(['foo@example.org']); + + $xml = $this->write([ + '{DAV:}root' => $property + ]); + + $this->assertXmlStringEqualsXmlString( +' + +foo@example.org +', $xml); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Property/InviteTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Property/InviteTest.php new file mode 100644 index 00000000000..1397dcca2b7 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Property/InviteTest.php @@ -0,0 +1,112 @@ +namespaceMap[CalDAV\Plugin::NS_CALDAV] = 'cal'; + $this->namespaceMap[CalDAV\Plugin::NS_CALENDARSERVER] = 'cs'; + + + } + + function testSimple() { + + $invite = new Invite([]); + $this->assertInstanceOf('Sabre\CalDAV\Xml\Property\Invite', $invite); + $this->assertEquals([], $invite->getValue()); + + } + + /** + * @depends testSimple + */ + function testSerialize() { + + $property = new Invite([ + new Sharee([ + 'href' => 'mailto:thedoctor@example.org', + 'properties' => ['{DAV:}displayname' => 'The Doctor'], + 'inviteStatus' => SP::INVITE_ACCEPTED, + 'access' => SP::ACCESS_SHAREDOWNER, + ]), + new Sharee([ + 'href' => 'mailto:user1@example.org', + 'inviteStatus' => SP::INVITE_ACCEPTED, + 'access' => SP::ACCESS_READWRITE, + ]), + new Sharee([ + 'href' => 'mailto:user2@example.org', + 'properties' => ['{DAV:}displayname' => 'John Doe'], + 'inviteStatus' => SP::INVITE_DECLINED, + 'access' => SP::ACCESS_READ, + ]), + new Sharee([ + 'href' => 'mailto:user3@example.org', + 'properties' => ['{DAV:}displayname' => 'Joe Shmoe'], + 'inviteStatus' => SP::INVITE_NORESPONSE, + 'access' => SP::ACCESS_READ, + 'comment' => 'Something, something', + ]), + new Sharee([ + 'href' => 'mailto:user4@example.org', + 'properties' => ['{DAV:}displayname' => 'Hoe Boe'], + 'inviteStatus' => SP::INVITE_INVALID, + 'access' => SP::ACCESS_READ, + ]), + ]); + + $xml = $this->write(['{DAV:}root' => $property]); + + $this->assertXmlStringEqualsXmlString( +' + + + mailto:thedoctor@example.org + The Doctor + + + + + + + mailto:user1@example.org + + + + + + + mailto:user2@example.org + John Doe + + + + + + + mailto:user3@example.org + Joe Shmoe + Something, something + + + + + + + mailto:user4@example.org + Hoe Boe + + +', $xml); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Property/ScheduleCalendarTranspTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Property/ScheduleCalendarTranspTest.php new file mode 100644 index 00000000000..729db4569e5 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Property/ScheduleCalendarTranspTest.php @@ -0,0 +1,118 @@ +namespaceMap[CalDAV\Plugin::NS_CALDAV] = 'cal'; + $this->namespaceMap[CalDAV\Plugin::NS_CALENDARSERVER] = 'cs'; + + + } + + function testSimple() { + + $prop = new ScheduleCalendarTransp(ScheduleCalendarTransp::OPAQUE); + $this->assertEquals( + ScheduleCalendarTransp::OPAQUE, + $prop->getValue() + ); + + } + + /** + * @expectedException \InvalidArgumentException + */ + function testBadValue() { + + new ScheduleCalendarTransp('ahhh'); + + } + + /** + * @depends testSimple + */ + function testSerializeOpaque() { + + $property = new ScheduleCalendarTransp(ScheduleCalendarTransp::OPAQUE); + $xml = $this->write(['{DAV:}root' => $property]); + + $this->assertXmlStringEqualsXmlString( +' + + + +', $xml); + + } + + /** + * @depends testSimple + */ + function testSerializeTransparent() { + + $property = new ScheduleCalendarTransp(ScheduleCalendarTransp::TRANSPARENT); + $xml = $this->write(['{DAV:}root' => $property]); + + $this->assertXmlStringEqualsXmlString( +' + + + +', $xml); + + } + + function testUnserializeTransparent() { + + $cal = CalDAV\Plugin::NS_CALDAV; + $cs = CalDAV\Plugin::NS_CALENDARSERVER; + +$xml = << + + + +XML; + + $result = $this->parse( + $xml, + ['{DAV:}root' => 'Sabre\\CalDAV\\Xml\\Property\\ScheduleCalendarTransp'] + ); + + $this->assertEquals( + new ScheduleCalendarTransp(ScheduleCalendarTransp::TRANSPARENT), + $result['value'] + ); + + } + + function testUnserializeOpaque() { + + $cal = CalDAV\Plugin::NS_CALDAV; + $cs = CalDAV\Plugin::NS_CALENDARSERVER; + +$xml = << + + + +XML; + + $result = $this->parse( + $xml, + ['{DAV:}root' => 'Sabre\\CalDAV\\Xml\\Property\\ScheduleCalendarTransp'] + ); + + $this->assertEquals( + new ScheduleCalendarTransp(ScheduleCalendarTransp::OPAQUE), + $result['value'] + ); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Property/SupportedCalendarComponentSetTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Property/SupportedCalendarComponentSetTest.php new file mode 100644 index 00000000000..1acc402d36b --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Property/SupportedCalendarComponentSetTest.php @@ -0,0 +1,102 @@ +namespaceMap[CalDAV\Plugin::NS_CALDAV] = 'cal'; + $this->namespaceMap[CalDAV\Plugin::NS_CALENDARSERVER] = 'cs'; + + } + + function testSimple() { + + $prop = new SupportedCalendarComponentSet(['VEVENT']); + $this->assertEquals( + ['VEVENT'], + $prop->getValue() + ); + + } + + function testMultiple() { + + $prop = new SupportedCalendarComponentSet(['VEVENT', 'VTODO']); + $this->assertEquals( + ['VEVENT', 'VTODO'], + $prop->getValue() + ); + + } + + /** + * @depends testSimple + * @depends testMultiple + */ + function testSerialize() { + + $property = new SupportedCalendarComponentSet(['VEVENT', 'VTODO']); + $xml = $this->write(['{DAV:}root' => $property]); + + $this->assertXmlStringEqualsXmlString( +' + + + + +', $xml); + + } + + function testUnserialize() { + + $cal = CalDAV\Plugin::NS_CALDAV; + $cs = CalDAV\Plugin::NS_CALENDARSERVER; + +$xml = << + + + + +XML; + + $result = $this->parse( + $xml, + ['{DAV:}root' => 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet'] + ); + + $this->assertEquals( + new SupportedCalendarComponentSet(['VEVENT', 'VTODO']), + $result['value'] + ); + + } + + /** + * @expectedException \Sabre\Xml\ParseException + */ + function testUnserializeEmpty() { + + $cal = CalDAV\Plugin::NS_CALDAV; + $cs = CalDAV\Plugin::NS_CALENDARSERVER; + +$xml = << + + +XML; + + $result = $this->parse( + $xml, + ['{DAV:}root' => 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet'] + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Property/SupportedCalendarDataTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Property/SupportedCalendarDataTest.php new file mode 100644 index 00000000000..442b6a059f4 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Property/SupportedCalendarDataTest.php @@ -0,0 +1,36 @@ +assertInstanceOf('Sabre\CalDAV\Xml\Property\SupportedCalendarData', $sccs); + + } + + /** + * @depends testSimple + */ + function testSerialize() { + + $this->namespaceMap[CalDAV\Plugin::NS_CALDAV] = 'cal'; + $property = new SupportedCalendarData(); + + $xml = $this->write(['{DAV:}root' => $property]); + + $this->assertXmlStringEqualsXmlString( +' + + + +', $xml); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Property/SupportedCollationSetTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Property/SupportedCollationSetTest.php new file mode 100644 index 00000000000..e009fb6cd85 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Property/SupportedCollationSetTest.php @@ -0,0 +1,37 @@ +assertInstanceOf('Sabre\CalDAV\Xml\Property\SupportedCollationSet', $scs); + + } + + /** + * @depends testSimple + */ + function testSerialize() { + + $property = new SupportedCollationSet(); + + $this->namespaceMap[CalDAV\Plugin::NS_CALDAV] = 'cal'; + $xml = $this->write(['{DAV:}root' => $property]); + + $this->assertXmlStringEqualsXmlString( +' + +i;ascii-casemap +i;octet +i;unicode-casemap +', $xml); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Request/CalendarQueryReportTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Request/CalendarQueryReportTest.php new file mode 100644 index 00000000000..d5e87db854b --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Request/CalendarQueryReportTest.php @@ -0,0 +1,369 @@ + 'Sabre\\CalDAV\\Xml\\Request\CalendarQueryReport', + ]; + + function testDeserialize() { + + $xml = << + + + + + + + + +XML; + + $result = $this->parse($xml); + $calendarQueryReport = new CalendarQueryReport(); + $calendarQueryReport->properties = [ + '{DAV:}getetag', + ]; + $calendarQueryReport->filters = [ + 'name' => 'VCALENDAR', + 'is-not-defined' => false, + 'comp-filters' => [], + 'prop-filters' => [], + 'time-range' => false, + ]; + + $this->assertEquals( + $calendarQueryReport, + $result['value'] + ); + + } + + /** + * @expectedException Sabre\DAV\Exception\BadRequest + */ + function testDeserializeNoFilter() { + + $xml = << + + + + + +XML; + + $this->parse($xml); + + } + + function testDeserializeComplex() { + + $xml = << + + + + + + + + + + + + + + + + + + + + + + hi + + + + + + + + + + Hello + + + + + +XML; + + $result = $this->parse($xml); + $calendarQueryReport = new CalendarQueryReport(); + $calendarQueryReport->version = '2.0'; + $calendarQueryReport->contentType = 'application/json+calendar'; + $calendarQueryReport->properties = [ + '{DAV:}getetag', + '{urn:ietf:params:xml:ns:caldav}calendar-data', + ]; + $calendarQueryReport->expand = [ + 'start' => new DateTimeImmutable('2015-01-01 00:00:00', new DateTimeZone('UTC')), + 'end' => new DateTimeImmutable('2016-01-01 00:00:00', new DateTimeZone('UTC')), + ]; + $calendarQueryReport->filters = [ + 'name' => 'VCALENDAR', + 'is-not-defined' => false, + 'comp-filters' => [ + [ + 'name' => 'VEVENT', + 'is-not-defined' => false, + 'comp-filters' => [ + [ + 'name' => 'VALARM', + 'is-not-defined' => true, + 'comp-filters' => [], + 'prop-filters' => [], + 'time-range' => false, + ], + ], + 'prop-filters' => [ + [ + 'name' => 'UID', + 'is-not-defined' => false, + 'time-range' => false, + 'text-match' => null, + 'param-filters' => [], + ], + [ + 'name' => 'X-PROP', + 'is-not-defined' => false, + 'time-range' => false, + 'text-match' => null, + 'param-filters' => [ + [ + 'name' => 'X-PARAM', + 'is-not-defined' => false, + 'text-match' => null, + ], + [ + 'name' => 'X-PARAM2', + 'is-not-defined' => true, + 'text-match' => null, + ], + [ + 'name' => 'X-PARAM3', + 'is-not-defined' => false, + 'text-match' => [ + 'negate-condition' => true, + 'collation' => 'i;ascii-casemap', + 'value' => 'hi', + ], + ], + ], + ], + [ + 'name' => 'X-PROP2', + 'is-not-defined' => true, + 'time-range' => false, + 'text-match' => null, + 'param-filters' => [], + ], + [ + 'name' => 'X-PROP3', + 'is-not-defined' => false, + 'time-range' => [ + 'start' => new DateTimeImmutable('2015-01-01 00:00:00', new DateTimeZone('UTC')), + 'end' => new DateTimeImmutable('2016-01-01 00:00:00', new DateTimeZone('UTC')), + ], + 'text-match' => null, + 'param-filters' => [], + ], + [ + 'name' => 'X-PROP4', + 'is-not-defined' => false, + 'time-range' => false, + 'text-match' => [ + 'negate-condition' => false, + 'collation' => 'i;ascii-casemap', + 'value' => 'Hello', + ], + 'param-filters' => [], + ], + ], + 'time-range' => [ + 'start' => new DateTimeImmutable('2015-01-01 00:00:00', new DateTimeZone('UTC')), + 'end' => new DateTimeImmutable('2016-01-01 00:00:00', new DateTimeZone('UTC')), + ] + ], + ], + 'prop-filters' => [], + 'time-range' => false, + ]; + + $this->assertEquals( + $calendarQueryReport, + $result['value'] + ); + + } + + /** + * @expectedException \Sabre\DAV\Exception\BadRequest + */ + function testDeserializeDoubleTopCompFilter() { + + $xml = << + + + + + + + + + + + + +XML; + + $this->parse($xml); + + } + + /** + * @expectedException \Sabre\DAV\Exception\BadRequest + */ + function testDeserializeMissingExpandEnd() { + + $xml = << + + + + + + + + + + + +XML; + + $this->parse($xml); + + } + + /** + * @expectedException \Sabre\DAV\Exception\BadRequest + */ + function testDeserializeExpandEndBeforeStart() { + + $xml = << + + + + + + + + + + + +XML; + + $this->parse($xml); + + } + + /** + * @expectedException \Sabre\DAV\Exception\BadRequest + */ + function testDeserializeTimeRangeOnVCALENDAR() { + + $xml = << + + + + + + + + + + + +XML; + + $this->parse($xml); + + } + + /** + * @expectedException \Sabre\DAV\Exception\BadRequest + */ + function testDeserializeTimeRangeEndBeforeStart() { + + $xml = << + + + + + + + + + + + + + +XML; + + $this->parse($xml); + + } + + /** + * @expectedException \Sabre\DAV\Exception\BadRequest + */ + function testDeserializeTimeRangePropEndBeforeStart() { + + $xml = << + + + + + + + + + + + + + + + +XML; + + $this->parse($xml); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Request/InviteReplyTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Request/InviteReplyTest.php new file mode 100644 index 00000000000..b0770899914 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Request/InviteReplyTest.php @@ -0,0 +1,78 @@ + 'Sabre\\CalDAV\\Xml\\Request\\InviteReply', + ]; + + function testDeserialize() { + + $xml = << + + /principal/1 + /calendar/1 + + blabla + Summary + +XML; + + $result = $this->parse($xml); + $inviteReply = new InviteReply('/principal/1', '/calendar/1', 'blabla', 'Summary', DAV\Sharing\Plugin::INVITE_ACCEPTED); + + $this->assertEquals( + $inviteReply, + $result['value'] + ); + + } + + function testDeserializeDeclined() { + + $xml = << + + /principal/1 + /calendar/1 + + blabla + Summary + +XML; + + $result = $this->parse($xml); + $inviteReply = new InviteReply('/principal/1', '/calendar/1', 'blabla', 'Summary', DAV\Sharing\Plugin::INVITE_DECLINED); + + $this->assertEquals( + $inviteReply, + $result['value'] + ); + + } + + /** + * @expectedException \Sabre\DAV\Exception\BadRequest + */ + function testDeserializeNoHostUrl() { + + $xml = << + + /principal/1 + + blabla + Summary + +XML; + + $this->parse($xml); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Request/ShareTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Request/ShareTest.php new file mode 100644 index 00000000000..73a2c3a13d2 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CalDAV/Xml/Request/ShareTest.php @@ -0,0 +1,83 @@ + 'Sabre\\CalDAV\\Xml\\Request\\Share', + ]; + + function testDeserialize() { + + $xml = << + + + mailto:eric@example.com + Eric York + Shared workspace + + + + mailto:foo@bar + + +XML; + + $result = $this->parse($xml); + $share = new Share([ + new Sharee([ + 'href' => 'mailto:eric@example.com', + 'access' => \Sabre\DAV\Sharing\Plugin::ACCESS_READWRITE, + 'properties' => [ + '{DAV:}displayname' => 'Eric York', + ], + 'comment' => 'Shared workspace', + ]), + new Sharee([ + 'href' => 'mailto:foo@bar', + 'access' => \Sabre\DAV\Sharing\Plugin::ACCESS_NOACCESS, + ]), + ]); + + $this->assertEquals( + $share, + $result['value'] + ); + + } + + function testDeserializeMinimal() { + + $xml = << + + + mailto:eric@example.com + + + +XML; + + $result = $this->parse($xml); + $share = new Share([ + new Sharee([ + 'href' => 'mailto:eric@example.com', + 'access' => \Sabre\DAV\Sharing\Plugin::ACCESS_READ, + ]), + ]); + + $this->assertEquals( + $share, + $result['value'] + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/AbstractPluginTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/AbstractPluginTest.php new file mode 100644 index 00000000000..552e2ba77e5 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/AbstractPluginTest.php @@ -0,0 +1,43 @@ +backend = new Backend\Mock(); + $principalBackend = new DAVACL\PrincipalBackend\Mock(); + + $tree = [ + new AddressBookRoot($principalBackend, $this->backend), + new DAVACL\PrincipalCollection($principalBackend) + ]; + + $this->plugin = new Plugin(); + $this->plugin->directories = ['directory']; + $this->server = new DAV\Server($tree); + $this->server->sapi = new HTTP\SapiMock(); + $this->server->addPlugin($this->plugin); + $this->server->debugExceptions = true; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/AddressBookHomeTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/AddressBookHomeTest.php new file mode 100644 index 00000000000..871f4a457c1 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/AddressBookHomeTest.php @@ -0,0 +1,159 @@ +backend = new Backend\Mock(); + $this->s = new AddressBookHome( + $this->backend, + 'principals/user1' + ); + + } + + function testGetName() { + + $this->assertEquals('user1', $this->s->getName()); + + } + + /** + * @expectedException Sabre\DAV\Exception\MethodNotAllowed + */ + function testSetName() { + + $this->s->setName('user2'); + + } + + /** + * @expectedException Sabre\DAV\Exception\MethodNotAllowed + */ + function testDelete() { + + $this->s->delete(); + + } + + function testGetLastModified() { + + $this->assertNull($this->s->getLastModified()); + + } + + /** + * @expectedException Sabre\DAV\Exception\MethodNotAllowed + */ + function testCreateFile() { + + $this->s->createFile('bla'); + + } + + /** + * @expectedException Sabre\DAV\Exception\MethodNotAllowed + */ + function testCreateDirectory() { + + $this->s->createDirectory('bla'); + + } + + function testGetChild() { + + $child = $this->s->getChild('book1'); + $this->assertInstanceOf('Sabre\\CardDAV\\AddressBook', $child); + $this->assertEquals('book1', $child->getName()); + + } + + /** + * @expectedException Sabre\DAV\Exception\NotFound + */ + function testGetChild404() { + + $this->s->getChild('book2'); + + } + + function testGetChildren() { + + $children = $this->s->getChildren(); + $this->assertEquals(2, count($children)); + $this->assertInstanceOf('Sabre\\CardDAV\\AddressBook', $children[0]); + $this->assertEquals('book1', $children[0]->getName()); + + } + + function testCreateExtendedCollection() { + + $resourceType = [ + '{' . Plugin::NS_CARDDAV . '}addressbook', + '{DAV:}collection', + ]; + $this->s->createExtendedCollection('book2', new MkCol($resourceType, ['{DAV:}displayname' => 'a-book 2'])); + + $this->assertEquals([ + 'id' => 'book2', + 'uri' => 'book2', + '{DAV:}displayname' => 'a-book 2', + 'principaluri' => 'principals/user1', + ], $this->backend->addressBooks[2]); + + } + + /** + * @expectedException Sabre\DAV\Exception\InvalidResourceType + */ + function testCreateExtendedCollectionInvalid() { + + $resourceType = [ + '{DAV:}collection', + ]; + $this->s->createExtendedCollection('book2', new MkCol($resourceType, ['{DAV:}displayname' => 'a-book 2'])); + + } + + + function testACLMethods() { + + $this->assertEquals('principals/user1', $this->s->getOwner()); + $this->assertNull($this->s->getGroup()); + $this->assertEquals([ + [ + 'privilege' => '{DAV:}all', + 'principal' => '{DAV:}owner', + 'protected' => true, + ], + ], $this->s->getACL()); + + } + + /** + * @expectedException Sabre\DAV\Exception\Forbidden + */ + function testSetACL() { + + $this->s->setACL([]); + + } + + function testGetSupportedPrivilegeSet() { + + $this->assertNull( + $this->s->getSupportedPrivilegeSet() + ); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/AddressBookQueryTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/AddressBookQueryTest.php new file mode 100644 index 00000000000..f8da38a16dc --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/AddressBookQueryTest.php @@ -0,0 +1,355 @@ + '1'] + ); + + $request->setBody( +' + + + + + + + +' + ); + + $response = new HTTP\ResponseMock(); + + $this->server->httpRequest = $request; + $this->server->httpResponse = $response; + + $this->server->exec(); + + $this->assertEquals(207, $response->status, 'Incorrect status code. Full response body:' . $response->body); + + // using the client for parsing + $client = new DAV\Client(['baseUri' => '/']); + + $result = $client->parseMultiStatus($response->body); + + $this->assertEquals([ + '/addressbooks/user1/book1/card1' => [ + 200 => [ + '{DAV:}getetag' => '"' . md5("BEGIN:VCARD\nVERSION:3.0\nUID:12345\nEND:VCARD") . '"', + ], + ], + '/addressbooks/user1/book1/card2' => [ + 404 => [ + '{DAV:}getetag' => null, + ], + ] + ], $result); + + + } + + function testQueryDepth0() { + + $request = new HTTP\Request( + 'REPORT', + '/addressbooks/user1/book1/card1', + ['Depth' => '0'] + ); + + $request->setBody( +' + + + + + + + +' + ); + + $response = new HTTP\ResponseMock(); + + $this->server->httpRequest = $request; + $this->server->httpResponse = $response; + + $this->server->exec(); + + $this->assertEquals(207, $response->status, 'Incorrect status code. Full response body:' . $response->body); + + // using the client for parsing + $client = new DAV\Client(['baseUri' => '/']); + + $result = $client->parseMultiStatus($response->body); + + $this->assertEquals([ + '/addressbooks/user1/book1/card1' => [ + 200 => [ + '{DAV:}getetag' => '"' . md5("BEGIN:VCARD\nVERSION:3.0\nUID:12345\nEND:VCARD") . '"', + ], + ], + ], $result); + + + } + + function testQueryNoMatch() { + + $request = new HTTP\Request( + 'REPORT', + '/addressbooks/user1/book1', + ['Depth' => '1'] + ); + + $request->setBody( +' + + + + + + + +' + ); + + $response = new HTTP\ResponseMock(); + + $this->server->httpRequest = $request; + $this->server->httpResponse = $response; + + $this->server->exec(); + + $this->assertEquals(207, $response->status, 'Incorrect status code. Full response body:' . $response->body); + + // using the client for parsing + $client = new DAV\Client(['baseUri' => '/']); + + $result = $client->parseMultiStatus($response->body); + + $this->assertEquals([], $result); + + } + + function testQueryLimit() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'REPORT', + 'REQUEST_URI' => '/addressbooks/user1/book1', + 'HTTP_DEPTH' => '1', + ]); + + $request->setBody( +' + + + + + + + + 1 +' + ); + + $response = new HTTP\ResponseMock(); + + $this->server->httpRequest = $request; + $this->server->httpResponse = $response; + + $this->server->exec(); + + $this->assertEquals(207, $response->status, 'Incorrect status code. Full response body:' . $response->body); + + // using the client for parsing + $client = new DAV\Client(['baseUri' => '/']); + + $result = $client->parseMultiStatus($response->body); + + $this->assertEquals([ + '/addressbooks/user1/book1/card1' => [ + 200 => [ + '{DAV:}getetag' => '"' . md5("BEGIN:VCARD\nVERSION:3.0\nUID:12345\nEND:VCARD") . '"', + ], + ], + ], $result); + + + } + + function testJson() { + + $request = new HTTP\Request( + 'REPORT', + '/addressbooks/user1/book1/card1', + ['Depth' => '0'] + ); + + $request->setBody( +' + + + + + +' + ); + + $response = new HTTP\ResponseMock(); + + $this->server->httpRequest = $request; + $this->server->httpResponse = $response; + + $this->server->exec(); + + $this->assertEquals(207, $response->status, 'Incorrect status code. Full response body:' . $response->body); + + // using the client for parsing + $client = new DAV\Client(['baseUri' => '/']); + + $result = $client->parseMultiStatus($response->body); + + $vobjVersion = \Sabre\VObject\Version::VERSION; + + $this->assertEquals([ + '/addressbooks/user1/book1/card1' => [ + 200 => [ + '{DAV:}getetag' => '"' . md5("BEGIN:VCARD\nVERSION:3.0\nUID:12345\nEND:VCARD") . '"', + '{urn:ietf:params:xml:ns:carddav}address-data' => '["vcard",[["version",{},"text","4.0"],["prodid",{},"text","-\/\/Sabre\/\/Sabre VObject ' . $vobjVersion . '\/\/EN"],["uid",{},"text","12345"]]]', + ], + ], + ], $result); + + } + + function testVCard4() { + + $request = new HTTP\Request( + 'REPORT', + '/addressbooks/user1/book1/card1', + ['Depth' => '0'] + ); + + $request->setBody( +' + + + + + +' + ); + + $response = new HTTP\ResponseMock(); + + $this->server->httpRequest = $request; + $this->server->httpResponse = $response; + + $this->server->exec(); + + $this->assertEquals(207, $response->status, 'Incorrect status code. Full response body:' . $response->body); + + // using the client for parsing + $client = new DAV\Client(['baseUri' => '/']); + + $result = $client->parseMultiStatus($response->body); + + $vobjVersion = \Sabre\VObject\Version::VERSION; + + $this->assertEquals([ + '/addressbooks/user1/book1/card1' => [ + 200 => [ + '{DAV:}getetag' => '"' . md5("BEGIN:VCARD\nVERSION:3.0\nUID:12345\nEND:VCARD") . '"', + '{urn:ietf:params:xml:ns:carddav}address-data' => "BEGIN:VCARD\r\nVERSION:4.0\r\nPRODID:-//Sabre//Sabre VObject $vobjVersion//EN\r\nUID:12345\r\nEND:VCARD\r\n", + ], + ], + ], $result); + + } + + function testAddressBookDepth0() { + + $request = new HTTP\Request( + 'REPORT', + '/addressbooks/user1/book1', + ['Depth' => '0'] + ); + + $request->setBody( + ' + + + + + +' + ); + + $response = new HTTP\ResponseMock(); + + $this->server->httpRequest = $request; + $this->server->httpResponse = $response; + + $this->server->exec(); + + $this->assertEquals(415, $response->status, 'Incorrect status code. Full response body:' . $response->body); + } + + function testAddressBookProperties() { + + $request = new HTTP\Request( + 'REPORT', + '/addressbooks/user1/book3', + ['Depth' => '1'] + ); + + $request->setBody( + ' + + + + + + + + +' + ); + + $response = new HTTP\ResponseMock(); + + $this->server->httpRequest = $request; + $this->server->httpResponse = $response; + + $this->server->exec(); + + $this->assertEquals(207, $response->status, 'Incorrect status code. Full response body:' . $response->body); + + // using the client for parsing + $client = new DAV\Client(['baseUri' => '/']); + + $result = $client->parseMultiStatus($response->body); + + $this->assertEquals([ + '/addressbooks/user1/book3/card3' => [ + 200 => [ + '{DAV:}getetag' => '"' . md5("BEGIN:VCARD\nVERSION:3.0\nUID:12345\nFN:Test-Card\nEMAIL;TYPE=home:bar@example.org\nEND:VCARD") . '"', + '{urn:ietf:params:xml:ns:carddav}address-data' => "BEGIN:VCARD\r\nVERSION:3.0\r\nUID:12345\r\nFN:Test-Card\r\nEND:VCARD\r\n", + ], + ], + ], $result); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/AddressBookRootTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/AddressBookRootTest.php new file mode 100644 index 00000000000..fc20480f2d4 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/AddressBookRootTest.php @@ -0,0 +1,31 @@ +assertEquals('addressbooks', $root->getName()); + + } + + function testGetChildForPrincipal() { + + $pBackend = new DAVACL\PrincipalBackend\Mock(); + $cBackend = new Backend\Mock(); + $root = new AddressBookRoot($pBackend, $cBackend); + + $children = $root->getChildren(); + $this->assertEquals(3, count($children)); + + $this->assertInstanceOf('Sabre\\CardDAV\\AddressBookHome', $children[0]); + $this->assertEquals('user1', $children[0]->getName()); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/AddressBookTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/AddressBookTest.php new file mode 100644 index 00000000000..1f0064dd38a --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/AddressBookTest.php @@ -0,0 +1,194 @@ +backend = new Backend\Mock(); + $this->ab = new AddressBook( + $this->backend, + [ + 'uri' => 'book1', + 'id' => 'foo', + '{DAV:}displayname' => 'd-name', + 'principaluri' => 'principals/user1', + ] + ); + + } + + function testGetName() { + + $this->assertEquals('book1', $this->ab->getName()); + + } + + function testGetChild() { + + $card = $this->ab->getChild('card1'); + $this->assertInstanceOf('Sabre\\CardDAV\\Card', $card); + $this->assertEquals('card1', $card->getName()); + + } + + /** + * @expectedException Sabre\DAV\Exception\NotFound + */ + function testGetChildNotFound() { + + $card = $this->ab->getChild('card3'); + + } + + function testGetChildren() { + + $cards = $this->ab->getChildren(); + $this->assertEquals(2, count($cards)); + + $this->assertEquals('card1', $cards[0]->getName()); + $this->assertEquals('card2', $cards[1]->getName()); + + } + + /** + * @expectedException Sabre\DAV\Exception\MethodNotAllowed + */ + function testCreateDirectory() { + + $this->ab->createDirectory('name'); + + } + + function testCreateFile() { + + $file = fopen('php://memory', 'r+'); + fwrite($file, 'foo'); + rewind($file); + $this->ab->createFile('card2', $file); + + $this->assertEquals('foo', $this->backend->cards['foo']['card2']); + + } + + function testDelete() { + + $this->ab->delete(); + $this->assertEquals(1, count($this->backend->addressBooks)); + + } + + /** + * @expectedException Sabre\DAV\Exception\MethodNotAllowed + */ + function testSetName() { + + $this->ab->setName('foo'); + + } + + function testGetLastModified() { + + $this->assertNull($this->ab->getLastModified()); + + } + + function testUpdateProperties() { + + $propPatch = new PropPatch([ + '{DAV:}displayname' => 'barrr', + ]); + $this->ab->propPatch($propPatch); + $this->assertTrue($propPatch->commit()); + + $this->assertEquals('barrr', $this->backend->addressBooks[0]['{DAV:}displayname']); + + } + + function testGetProperties() { + + $props = $this->ab->getProperties(['{DAV:}displayname']); + $this->assertEquals([ + '{DAV:}displayname' => 'd-name', + ], $props); + + } + + function testACLMethods() { + + $this->assertEquals('principals/user1', $this->ab->getOwner()); + $this->assertNull($this->ab->getGroup()); + $this->assertEquals([ + [ + 'privilege' => '{DAV:}all', + 'principal' => '{DAV:}owner', + 'protected' => true, + ], + ], $this->ab->getACL()); + + } + + /** + * @expectedException Sabre\DAV\Exception\Forbidden + */ + function testSetACL() { + + $this->ab->setACL([]); + + } + + function testGetSupportedPrivilegeSet() { + + $this->assertNull( + $this->ab->getSupportedPrivilegeSet() + ); + + } + + function testGetSyncTokenNoSyncSupport() { + + $this->assertNull($this->ab->getSyncToken()); + + } + function testGetChangesNoSyncSupport() { + + $this->assertNull($this->ab->getChanges(1, null)); + + } + + function testGetSyncToken() { + + $this->driver = 'sqlite'; + $this->dropTables(['addressbooks', 'cards', 'addressbookchanges']); + $this->createSchema('addressbooks'); + $backend = new Backend\PDO( + $this->getPDO() + ); + $ab = new AddressBook($backend, ['id' => 1, '{DAV:}sync-token' => 2]); + $this->assertEquals(2, $ab->getSyncToken()); + } + + function testGetSyncToken2() { + + $this->driver = 'sqlite'; + $this->dropTables(['addressbooks', 'cards', 'addressbookchanges']); + $this->createSchema('addressbooks'); + $backend = new Backend\PDO( + $this->getPDO() + ); + $ab = new AddressBook($backend, ['id' => 1, '{http://sabredav.org/ns}sync-token' => 2]); + $this->assertEquals(2, $ab->getSyncToken()); + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Backend/AbstractPDOTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Backend/AbstractPDOTest.php new file mode 100644 index 00000000000..f62bfb1ae68 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Backend/AbstractPDOTest.php @@ -0,0 +1,373 @@ +dropTables([ + 'addressbooks', + 'cards', + 'addressbookchanges', + ]); + $this->createSchema('addressbooks'); + $pdo = $this->getPDO(); + + $this->backend = new PDO($pdo); + $pdo->exec("INSERT INTO addressbooks (principaluri, displayname, uri, description, synctoken) VALUES ('principals/user1', 'book1', 'book1', 'addressbook 1', 1)"); + $pdo->exec("INSERT INTO cards (addressbookid, carddata, uri, lastmodified, etag, size) VALUES (1, 'card1', 'card1', 0, '" . md5('card1') . "', 5)"); + + } + + function testGetAddressBooksForUser() { + + $result = $this->backend->getAddressBooksForUser('principals/user1'); + + $expected = [ + [ + 'id' => 1, + 'uri' => 'book1', + 'principaluri' => 'principals/user1', + '{DAV:}displayname' => 'book1', + '{' . CardDAV\Plugin::NS_CARDDAV . '}addressbook-description' => 'addressbook 1', + '{http://calendarserver.org/ns/}getctag' => 1, + '{http://sabredav.org/ns}sync-token' => 1 + ] + ]; + + $this->assertEquals($expected, $result); + + } + + function testUpdateAddressBookInvalidProp() { + + $propPatch = new PropPatch([ + '{DAV:}displayname' => 'updated', + '{' . CardDAV\Plugin::NS_CARDDAV . '}addressbook-description' => 'updated', + '{DAV:}foo' => 'bar', + ]); + + $this->backend->updateAddressBook(1, $propPatch); + $result = $propPatch->commit(); + + $this->assertFalse($result); + + $result = $this->backend->getAddressBooksForUser('principals/user1'); + + $expected = [ + [ + 'id' => 1, + 'uri' => 'book1', + 'principaluri' => 'principals/user1', + '{DAV:}displayname' => 'book1', + '{' . CardDAV\Plugin::NS_CARDDAV . '}addressbook-description' => 'addressbook 1', + '{http://calendarserver.org/ns/}getctag' => 1, + '{http://sabredav.org/ns}sync-token' => 1 + ] + ]; + + $this->assertEquals($expected, $result); + + } + + function testUpdateAddressBookNoProps() { + + $propPatch = new PropPatch([ + ]); + + $this->backend->updateAddressBook(1, $propPatch); + $result = $propPatch->commit(); + $this->assertTrue($result); + + $result = $this->backend->getAddressBooksForUser('principals/user1'); + + $expected = [ + [ + 'id' => 1, + 'uri' => 'book1', + 'principaluri' => 'principals/user1', + '{DAV:}displayname' => 'book1', + '{' . CardDAV\Plugin::NS_CARDDAV . '}addressbook-description' => 'addressbook 1', + '{http://calendarserver.org/ns/}getctag' => 1, + '{http://sabredav.org/ns}sync-token' => 1 + ] + ]; + + $this->assertEquals($expected, $result); + + + } + + function testUpdateAddressBookSuccess() { + + $propPatch = new PropPatch([ + '{DAV:}displayname' => 'updated', + '{' . CardDAV\Plugin::NS_CARDDAV . '}addressbook-description' => 'updated', + ]); + + $this->backend->updateAddressBook(1, $propPatch); + $result = $propPatch->commit(); + + $this->assertTrue($result); + + $result = $this->backend->getAddressBooksForUser('principals/user1'); + + $expected = [ + [ + 'id' => 1, + 'uri' => 'book1', + 'principaluri' => 'principals/user1', + '{DAV:}displayname' => 'updated', + '{' . CardDAV\Plugin::NS_CARDDAV . '}addressbook-description' => 'updated', + '{http://calendarserver.org/ns/}getctag' => 2, + '{http://sabredav.org/ns}sync-token' => 2 + ] + ]; + + $this->assertEquals($expected, $result); + + + } + + function testDeleteAddressBook() { + + $this->backend->deleteAddressBook(1); + + $this->assertEquals([], $this->backend->getAddressBooksForUser('principals/user1')); + + } + + /** + * @expectedException Sabre\DAV\Exception\BadRequest + */ + function testCreateAddressBookUnsupportedProp() { + + $this->backend->createAddressBook('principals/user1', 'book2', [ + '{DAV:}foo' => 'bar', + ]); + + } + + function testCreateAddressBookSuccess() { + + $this->backend->createAddressBook('principals/user1', 'book2', [ + '{DAV:}displayname' => 'book2', + '{' . CardDAV\Plugin::NS_CARDDAV . '}addressbook-description' => 'addressbook 2', + ]); + + $expected = [ + [ + 'id' => 1, + 'uri' => 'book1', + 'principaluri' => 'principals/user1', + '{DAV:}displayname' => 'book1', + '{' . CardDAV\Plugin::NS_CARDDAV . '}addressbook-description' => 'addressbook 1', + '{http://calendarserver.org/ns/}getctag' => 1, + '{http://sabredav.org/ns}sync-token' => 1, + ], + [ + 'id' => 2, + 'uri' => 'book2', + 'principaluri' => 'principals/user1', + '{DAV:}displayname' => 'book2', + '{' . CardDAV\Plugin::NS_CARDDAV . '}addressbook-description' => 'addressbook 2', + '{http://calendarserver.org/ns/}getctag' => 1, + '{http://sabredav.org/ns}sync-token' => 1, + ] + ]; + $result = $this->backend->getAddressBooksForUser('principals/user1'); + $this->assertEquals($expected, $result); + + } + + function testGetCards() { + + $result = $this->backend->getCards(1); + + $expected = [ + [ + 'id' => 1, + 'uri' => 'card1', + 'lastmodified' => 0, + 'etag' => '"' . md5('card1') . '"', + 'size' => 5 + ] + ]; + + $this->assertEquals($expected, $result); + + } + + function testGetCard() { + + $result = $this->backend->getCard(1, 'card1'); + + $expected = [ + 'id' => 1, + 'uri' => 'card1', + 'carddata' => 'card1', + 'lastmodified' => 0, + 'etag' => '"' . md5('card1') . '"', + 'size' => 5 + ]; + + if (is_resource($result['carddata'])) { + $result['carddata'] = stream_get_contents($result['carddata']); + } + + $this->assertEquals($expected, $result); + + } + + /** + * @depends testGetCard + */ + function testCreateCard() { + + $result = $this->backend->createCard(1, 'card2', 'data2'); + $this->assertEquals('"' . md5('data2') . '"', $result); + $result = $this->backend->getCard(1, 'card2'); + $this->assertEquals(2, $result['id']); + $this->assertEquals('card2', $result['uri']); + if (is_resource($result['carddata'])) { + $result['carddata'] = stream_get_contents($result['carddata']); + } + $this->assertEquals('data2', $result['carddata']); + + } + + /** + * @depends testCreateCard + */ + function testGetMultiple() { + + $result = $this->backend->createCard(1, 'card2', 'data2'); + $result = $this->backend->createCard(1, 'card3', 'data3'); + $check = [ + [ + 'id' => 1, + 'uri' => 'card1', + 'carddata' => 'card1', + 'lastmodified' => 0, + ], + [ + 'id' => 2, + 'uri' => 'card2', + 'carddata' => 'data2', + 'lastmodified' => time(), + ], + [ + 'id' => 3, + 'uri' => 'card3', + 'carddata' => 'data3', + 'lastmodified' => time(), + ], + ]; + + $result = $this->backend->getMultipleCards(1, ['card1', 'card2', 'card3']); + + foreach ($check as $index => $node) { + + foreach ($node as $k => $v) { + + $expected = $v; + $actual = $result[$index][$k]; + + switch ($k) { + case 'lastmodified' : + $this->assertInternalType('int', $actual); + break; + case 'carddata' : + if (is_resource($actual)) { + $actual = stream_get_contents($actual); + } + // No break intended. + default : + $this->assertEquals($expected, $actual); + break; + } + + } + + } + + + } + + /** + * @depends testGetCard + */ + function testUpdateCard() { + + $result = $this->backend->updateCard(1, 'card1', 'newdata'); + $this->assertEquals('"' . md5('newdata') . '"', $result); + + $result = $this->backend->getCard(1, 'card1'); + $this->assertEquals(1, $result['id']); + if (is_resource($result['carddata'])) { + $result['carddata'] = stream_get_contents($result['carddata']); + } + $this->assertEquals('newdata', $result['carddata']); + + } + + /** + * @depends testGetCard + */ + function testDeleteCard() { + + $this->backend->deleteCard(1, 'card1'); + $result = $this->backend->getCard(1, 'card1'); + $this->assertFalse($result); + + } + + function testGetChanges() { + + $backend = $this->backend; + $id = $backend->createAddressBook( + 'principals/user1', + 'bla', + [] + ); + $result = $backend->getChangesForAddressBook($id, null, 1); + + $this->assertEquals([ + 'syncToken' => 1, + "added" => [], + 'modified' => [], + 'deleted' => [], + ], $result); + + $currentToken = $result['syncToken']; + + $dummyCard = "BEGIN:VCARD\r\nEND:VCARD\r\n"; + + $backend->createCard($id, "card1.ics", $dummyCard); + $backend->createCard($id, "card2.ics", $dummyCard); + $backend->createCard($id, "card3.ics", $dummyCard); + $backend->updateCard($id, "card1.ics", $dummyCard); + $backend->deleteCard($id, "card2.ics"); + + $result = $backend->getChangesForAddressBook($id, $currentToken, 1); + + $this->assertEquals([ + 'syncToken' => 6, + 'modified' => ["card1.ics"], + 'deleted' => ["card2.ics"], + "added" => ["card3.ics"], + ], $result); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Backend/Mock.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Backend/Mock.php new file mode 100644 index 00000000000..8638dc74a65 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Backend/Mock.php @@ -0,0 +1,258 @@ +addressBooks = $addressBooks; + $this->cards = $cards; + + if (is_null($this->addressBooks)) { + $this->addressBooks = [ + [ + 'id' => 'foo', + 'uri' => 'book1', + 'principaluri' => 'principals/user1', + '{DAV:}displayname' => 'd-name', + ], + [ + 'id' => 'bar', + 'uri' => 'book3', + 'principaluri' => 'principals/user1', + '{DAV:}displayname' => 'd-name', + ], + ]; + + $card2 = fopen('php://memory', 'r+'); + fwrite($card2, "BEGIN:VCARD\nVERSION:3.0\nUID:45678\nEND:VCARD"); + rewind($card2); + $this->cards = [ + 'foo' => [ + 'card1' => "BEGIN:VCARD\nVERSION:3.0\nUID:12345\nEND:VCARD", + 'card2' => $card2, + ], + 'bar' => [ + 'card3' => "BEGIN:VCARD\nVERSION:3.0\nUID:12345\nFN:Test-Card\nEMAIL;TYPE=home:bar@example.org\nEND:VCARD", + ], + ]; + } + + } + + + function getAddressBooksForUser($principalUri) { + + $books = []; + foreach ($this->addressBooks as $book) { + if ($book['principaluri'] === $principalUri) { + $books[] = $book; + } + } + return $books; + + } + + /** + * 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 + * @param \Sabre\DAV\PropPatch $propPatch + * @return void + */ + function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) { + + foreach ($this->addressBooks as &$book) { + if ($book['id'] !== $addressBookId) + continue; + + $propPatch->handleRemaining(function($mutations) use (&$book) { + foreach ($mutations as $key => $value) { + $book[$key] = $value; + } + return true; + }); + + } + + } + + function createAddressBook($principalUri, $url, array $properties) { + + $this->addressBooks[] = array_merge($properties, [ + 'id' => $url, + 'uri' => $url, + 'principaluri' => $principalUri, + ]); + + } + + function deleteAddressBook($addressBookId) { + + foreach ($this->addressBooks as $key => $value) { + if ($value['id'] === $addressBookId) + unset($this->addressBooks[$key]); + } + unset($this->cards[$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 ommit carddata. + * This may speed up certain requests, especially with large cards. + * + * @param mixed $addressBookId + * @return array + */ + function getCards($addressBookId) { + + $cards = []; + foreach ($this->cards[$addressBookId] as $uri => $data) { + if (is_resource($data)) { + $cards[] = [ + 'uri' => $uri, + 'carddata' => $data, + ]; + } else { + $cards[] = [ + 'uri' => $uri, + 'carddata' => $data, + 'etag' => '"' . md5($data) . '"', + 'size' => strlen($data) + ]; + } + } + return $cards; + + } + + /** + * Returns a specfic 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 + */ + function getCard($addressBookId, $cardUri) { + + if (!isset($this->cards[$addressBookId][$cardUri])) { + return false; + } + + $data = $this->cards[$addressBookId][$cardUri]; + return [ + 'uri' => $cardUri, + 'carddata' => $data, + 'etag' => '"' . md5($data) . '"', + 'size' => strlen($data) + ]; + + } + + /** + * 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 + */ + function createCard($addressBookId, $cardUri, $cardData) { + + if (is_resource($cardData)) { + $cardData = stream_get_contents($cardData); + } + $this->cards[$addressBookId][$cardUri] = $cardData; + return '"' . md5($cardData) . '"'; + + } + + /** + * 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 + */ + function updateCard($addressBookId, $cardUri, $cardData) { + + if (is_resource($cardData)) { + $cardData = stream_get_contents($cardData); + } + $this->cards[$addressBookId][$cardUri] = $cardData; + return '"' . md5($cardData) . '"'; + + } + + function deleteCard($addressBookId, $cardUri) { + + unset($this->cards[$addressBookId][$cardUri]); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Backend/PDOMySQLTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Backend/PDOMySQLTest.php new file mode 100644 index 00000000000..c1b0e274ebd --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Backend/PDOMySQLTest.php @@ -0,0 +1,9 @@ +backend = new Backend\Mock(); + $this->card = new Card( + $this->backend, + [ + 'uri' => 'book1', + 'id' => 'foo', + 'principaluri' => 'principals/user1', + ], + [ + 'uri' => 'card1', + 'addressbookid' => 'foo', + 'carddata' => 'card', + ] + ); + + } + + function testGet() { + + $result = $this->card->get(); + $this->assertEquals('card', $result); + + } + function testGet2() { + + $this->card = new Card( + $this->backend, + [ + 'uri' => 'book1', + 'id' => 'foo', + 'principaluri' => 'principals/user1', + ], + [ + 'uri' => 'card1', + 'addressbookid' => 'foo', + ] + ); + $result = $this->card->get(); + $this->assertEquals("BEGIN:VCARD\nVERSION:3.0\nUID:12345\nEND:VCARD", $result); + + } + + + /** + * @depends testGet + */ + function testPut() { + + $file = fopen('php://memory', 'r+'); + fwrite($file, 'newdata'); + rewind($file); + $this->card->put($file); + $result = $this->card->get(); + $this->assertEquals('newdata', $result); + + } + + + function testDelete() { + + $this->card->delete(); + $this->assertEquals(1, count($this->backend->cards['foo'])); + + } + + function testGetContentType() { + + $this->assertEquals('text/vcard; charset=utf-8', $this->card->getContentType()); + + } + + function testGetETag() { + + $this->assertEquals('"' . md5('card') . '"', $this->card->getETag()); + + } + + function testGetETag2() { + + $card = new Card( + $this->backend, + [ + 'uri' => 'book1', + 'id' => 'foo', + 'principaluri' => 'principals/user1', + ], + [ + 'uri' => 'card1', + 'addressbookid' => 'foo', + 'carddata' => 'card', + 'etag' => '"blabla"', + ] + ); + $this->assertEquals('"blabla"', $card->getETag()); + + } + + function testGetLastModified() { + + $this->assertEquals(null, $this->card->getLastModified()); + + } + + function testGetSize() { + + $this->assertEquals(4, $this->card->getSize()); + $this->assertEquals(4, $this->card->getSize()); + + } + + function testGetSize2() { + + $card = new Card( + $this->backend, + [ + 'uri' => 'book1', + 'id' => 'foo', + 'principaluri' => 'principals/user1', + ], + [ + 'uri' => 'card1', + 'addressbookid' => 'foo', + 'etag' => '"blabla"', + 'size' => 4, + ] + ); + $this->assertEquals(4, $card->getSize()); + + } + + function testACLMethods() { + + $this->assertEquals('principals/user1', $this->card->getOwner()); + $this->assertNull($this->card->getGroup()); + $this->assertEquals([ + [ + 'privilege' => '{DAV:}all', + 'principal' => 'principals/user1', + 'protected' => true, + ], + ], $this->card->getACL()); + + } + function testOverrideACL() { + + $card = new Card( + $this->backend, + [ + 'uri' => 'book1', + 'id' => 'foo', + 'principaluri' => 'principals/user1', + ], + [ + 'uri' => 'card1', + 'addressbookid' => 'foo', + 'carddata' => 'card', + 'acl' => [ + [ + 'privilege' => '{DAV:}read', + 'principal' => 'principals/user1', + 'protected' => true, + ], + ], + ] + ); + $this->assertEquals([ + [ + 'privilege' => '{DAV:}read', + 'principal' => 'principals/user1', + 'protected' => true, + ], + ], $card->getACL()); + + } + + /** + * @expectedException Sabre\DAV\Exception\Forbidden + */ + function testSetACL() { + + $this->card->setACL([]); + + } + + function testGetSupportedPrivilegeSet() { + + $this->assertNull( + $this->card->getSupportedPrivilegeSet() + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/IDirectoryTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/IDirectoryTest.php new file mode 100644 index 00000000000..4796a131f87 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/IDirectoryTest.php @@ -0,0 +1,30 @@ +addPlugin($plugin); + + $props = $server->getProperties('directory', ['{DAV:}resourcetype']); + $this->assertTrue($props['{DAV:}resourcetype']->is('{' . Plugin::NS_CARDDAV . '}directory')); + + } + +} + +class DirectoryMock extends DAV\SimpleCollection implements IDirectory { + + + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/MultiGetTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/MultiGetTest.php new file mode 100644 index 00000000000..2d57c6ae759 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/MultiGetTest.php @@ -0,0 +1,99 @@ + 'REPORT', + 'REQUEST_URI' => '/addressbooks/user1/book1', + ]); + + $request->setBody( +' + + + + + + /addressbooks/user1/book1/card1 +' + ); + + $response = new HTTP\ResponseMock(); + + $this->server->httpRequest = $request; + $this->server->httpResponse = $response; + + $this->server->exec(); + + $this->assertEquals(207, $response->status, 'Incorrect status code. Full response body:' . $response->body); + + // using the client for parsing + $client = new DAV\Client(['baseUri' => '/']); + + $result = $client->parseMultiStatus($response->body); + + $this->assertEquals([ + '/addressbooks/user1/book1/card1' => [ + 200 => [ + '{DAV:}getetag' => '"' . md5("BEGIN:VCARD\nVERSION:3.0\nUID:12345\nEND:VCARD") . '"', + '{urn:ietf:params:xml:ns:carddav}address-data' => "BEGIN:VCARD\nVERSION:3.0\nUID:12345\nEND:VCARD", + ] + ] + ], $result); + + } + + function testMultiGetVCard4() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'REPORT', + 'REQUEST_URI' => '/addressbooks/user1/book1', + ]); + + $request->setBody( +' + + + + + + /addressbooks/user1/book1/card1 +' + ); + + $response = new HTTP\ResponseMock(); + + $this->server->httpRequest = $request; + $this->server->httpResponse = $response; + + $this->server->exec(); + + $this->assertEquals(207, $response->status, 'Incorrect status code. Full response body:' . $response->body); + + // using the client for parsing + $client = new DAV\Client(['baseUri' => '/']); + + $result = $client->parseMultiStatus($response->body); + + $prodId = "PRODID:-//Sabre//Sabre VObject " . \Sabre\VObject\Version::VERSION . "//EN"; + + $this->assertEquals([ + '/addressbooks/user1/book1/card1' => [ + 200 => [ + '{DAV:}getetag' => '"' . md5("BEGIN:VCARD\nVERSION:3.0\nUID:12345\nEND:VCARD") . '"', + '{urn:ietf:params:xml:ns:carddav}address-data' => "BEGIN:VCARD\r\nVERSION:4.0\r\n$prodId\r\nUID:12345\r\nEND:VCARD\r\n", + ] + ] + ], $result); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/PluginTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/PluginTest.php new file mode 100644 index 00000000000..6962e7830c5 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/PluginTest.php @@ -0,0 +1,102 @@ +assertEquals('{' . Plugin::NS_CARDDAV . '}addressbook', $this->server->resourceTypeMapping['Sabre\\CardDAV\\IAddressBook']); + + $this->assertTrue(in_array('addressbook', $this->plugin->getFeatures())); + $this->assertEquals('carddav', $this->plugin->getPluginInfo()['name']); + + } + + function testSupportedReportSet() { + + $this->assertEquals([ + '{' . Plugin::NS_CARDDAV . '}addressbook-multiget', + '{' . Plugin::NS_CARDDAV . '}addressbook-query', + ], $this->plugin->getSupportedReportSet('addressbooks/user1/book1')); + + } + + function testSupportedReportSetEmpty() { + + $this->assertEquals([ + ], $this->plugin->getSupportedReportSet('')); + + } + + function testAddressBookHomeSet() { + + $result = $this->server->getProperties('principals/user1', ['{' . Plugin::NS_CARDDAV . '}addressbook-home-set']); + + $this->assertEquals(1, count($result)); + $this->assertTrue(isset($result['{' . Plugin::NS_CARDDAV . '}addressbook-home-set'])); + $this->assertEquals('addressbooks/user1/', $result['{' . Plugin::NS_CARDDAV . '}addressbook-home-set']->getHref()); + + } + + function testDirectoryGateway() { + + $result = $this->server->getProperties('principals/user1', ['{' . Plugin::NS_CARDDAV . '}directory-gateway']); + + $this->assertEquals(1, count($result)); + $this->assertTrue(isset($result['{' . Plugin::NS_CARDDAV . '}directory-gateway'])); + $this->assertEquals(['directory'], $result['{' . Plugin::NS_CARDDAV . '}directory-gateway']->getHrefs()); + + } + + function testReportPassThrough() { + + $this->assertNull($this->plugin->report('{DAV:}foo', new \DomDocument(), '')); + + } + + function testHTMLActionsPanel() { + + $output = ''; + $r = $this->server->emit('onHTMLActionsPanel', [$this->server->tree->getNodeForPath('addressbooks/user1'), &$output]); + $this->assertFalse($r); + + $this->assertTrue(!!strpos($output, 'Display name')); + + } + + function testAddressbookPluginProperties() { + + $ns = '{' . Plugin::NS_CARDDAV . '}'; + $propFind = new DAV\PropFind('addressbooks/user1/book1', [ + $ns . 'supported-address-data', + $ns . 'supported-collation-set', + ]); + $node = $this->server->tree->getNodeForPath('addressbooks/user1/book1'); + $this->plugin->propFindEarly($propFind, $node); + + $this->assertInstanceOf( + 'Sabre\\CardDAV\\Xml\\Property\\SupportedAddressData', + $propFind->get($ns . 'supported-address-data') + ); + $this->assertInstanceOf( + 'Sabre\\CardDAV\\Xml\\Property\\SupportedCollationSet', + $propFind->get($ns . 'supported-collation-set') + ); + + + } + + function testGetTransform() { + + $request = new \Sabre\HTTP\Request('GET', '/addressbooks/user1/book1/card1', ['Accept: application/vcard+json']); + $response = new \Sabre\HTTP\ResponseMock(); + $this->server->invokeMethod($request, $response); + + $this->assertEquals(200, $response->getStatus()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/SogoStripContentTypeTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/SogoStripContentTypeTest.php new file mode 100644 index 00000000000..d4bc48098dc --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/SogoStripContentTypeTest.php @@ -0,0 +1,56 @@ + 1, + 'uri' => 'book1', + 'principaluri' => 'principals/user1', + ], + ]; + protected $carddavCards = [ + 1 => [ + 'card1.vcf' => "BEGIN:VCARD\nVERSION:3.0\nUID:12345\nEND:VCARD", + ], + ]; + + function testDontStrip() { + + $result = $this->server->getProperties('addressbooks/user1/book1/card1.vcf', ['{DAV:}getcontenttype']); + $this->assertEquals([ + '{DAV:}getcontenttype' => 'text/vcard; charset=utf-8' + ], $result); + + } + function testStrip() { + + $this->server->httpRequest = HTTP\Sapi::createFromServerArray([ + 'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:10.0.2) Gecko/20120216 Thunderbird/10.0.2 Lightning/1.2.1', + ]); + $result = $this->server->getProperties('addressbooks/user1/book1/card1.vcf', ['{DAV:}getcontenttype']); + $this->assertEquals([ + '{DAV:}getcontenttype' => 'text/x-vcard' + ], $result); + + } + function testDontTouchOtherMimeTypes() { + + $this->server->httpRequest = new HTTP\Request('GET', '/addressbooks/user1/book1/card1.vcf', [ + 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:10.0.2) Gecko/20120216 Thunderbird/10.0.2 Lightning/1.2.1', + ]); + + $propFind = new PropFind('hello', ['{DAV:}getcontenttype']); + $propFind->set('{DAV:}getcontenttype', 'text/plain'); + $this->carddavPlugin->propFindLate($propFind, new \Sabre\DAV\SimpleCollection('foo')); + $this->assertEquals('text/plain', $propFind->get('{DAV:}getcontenttype')); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/TestUtil.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/TestUtil.php new file mode 100644 index 00000000000..ec8a3501e81 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/TestUtil.php @@ -0,0 +1,62 @@ +createAddressBook( + 'principals/user1', + 'UUID-123467', + [ + '{DAV:}displayname' => 'user1 addressbook', + '{urn:ietf:params:xml:ns:carddav}addressbook-description' => 'AddressBook description', + ] + ); + $backend->createAddressBook( + 'principals/user1', + 'UUID-123468', + [ + '{DAV:}displayname' => 'user1 addressbook2', + '{urn:ietf:params:xml:ns:carddav}addressbook-description' => 'AddressBook description', + ] + ); + $backend->createCard($addressbookId, 'UUID-2345', self::getTestCardData()); + return $pdo; + + } + + static function deleteSQLiteDB() { + $sqliteTest = new Backend\PDOSqliteTest(); + $pdo = $sqliteTest->tearDown(); + } + + static function getTestCardData() { + + $addressbookData = 'BEGIN:VCARD +VERSION:3.0 +PRODID:-//Acme Inc.//RoadRunner 1.0//EN +FN:Wile E. Coyote +N:Coyote;Wile;Erroll;; +ORG:Acme Inc. +UID:39A6B5ED-DD51-4AFE-A683-C35EE3749627 +REV:2012-06-20T07:00:39+00:00 +END:VCARD'; + + return $addressbookData; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/VCFExportTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/VCFExportTest.php new file mode 100644 index 00000000000..82d82faddc1 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/VCFExportTest.php @@ -0,0 +1,135 @@ + 'book1', + 'uri' => 'book1', + 'principaluri' => 'principals/user1', + ] + ]; + protected $carddavCards = [ + 'book1' => [ + "card1" => "BEGIN:VCARD\r\nFN:Person1\r\nEND:VCARD\r\n", + "card2" => "BEGIN:VCARD\r\nFN:Person2\r\nEND:VCARD", + "card3" => "BEGIN:VCARD\r\nFN:Person3\r\nEND:VCARD\r\n", + "card4" => "BEGIN:VCARD\nFN:Person4\nEND:VCARD\n", + ] + ]; + + function setUp() { + + parent::setUp(); + $plugin = new VCFExportPlugin(); + $this->server->addPlugin( + $plugin + ); + + } + + function testSimple() { + + $plugin = $this->server->getPlugin('vcf-export'); + $this->assertInstanceOf('Sabre\\CardDAV\\VCFExportPlugin', $plugin); + + $this->assertEquals( + 'vcf-export', + $plugin->getPluginInfo()['name'] + ); + + } + + function testExport() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_URI' => '/addressbooks/user1/book1?export', + 'QUERY_STRING' => 'export', + 'REQUEST_METHOD' => 'GET', + ]); + + $response = $this->request($request); + $this->assertEquals(200, $response->status, $response->body); + + $expected = "BEGIN:VCARD +FN:Person1 +END:VCARD +BEGIN:VCARD +FN:Person2 +END:VCARD +BEGIN:VCARD +FN:Person3 +END:VCARD +BEGIN:VCARD +FN:Person4 +END:VCARD +"; + // We actually expected windows line endings + $expected = str_replace("\n", "\r\n", $expected); + + $this->assertEquals($expected, $response->body); + + } + + function testBrowserIntegration() { + + $plugin = $this->server->getPlugin('vcf-export'); + $actions = ''; + $addressbook = new AddressBook($this->carddavBackend, []); + $this->server->emit('browserButtonActions', ['/foo', $addressbook, &$actions]); + $this->assertContains('/foo?export', $actions); + + } + + function testContentDisposition() { + + $request = new HTTP\Request( + 'GET', + '/addressbooks/user1/book1?export' + ); + + $response = $this->request($request, 200); + $this->assertEquals('text/directory', $response->getHeader('Content-Type')); + $this->assertEquals( + 'attachment; filename="book1-' . date('Y-m-d') . '.vcf"', + $response->getHeader('Content-Disposition') + ); + + } + + function testContentDispositionBadChars() { + + $this->carddavBackend->createAddressBook( + 'principals/user1', + 'book-b_ad"(ch)ars', + [] + ); + $this->carddavBackend->createCard( + 'book-b_ad"(ch)ars', + 'card1', + "BEGIN:VCARD\r\nFN:Person1\r\nEND:VCARD\r\n" + ); + + $request = new HTTP\Request( + 'GET', + '/addressbooks/user1/book-b_ad"(ch)ars?export' + ); + + $response = $this->request($request, 200); + $this->assertEquals('text/directory', $response->getHeader('Content-Type')); + $this->assertEquals( + 'attachment; filename="book-b_adchars-' . date('Y-m-d') . '.vcf"', + $response->getHeader('Content-Disposition') + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/ValidateFilterTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/ValidateFilterTest.php new file mode 100644 index 00000000000..03c468f868f --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/ValidateFilterTest.php @@ -0,0 +1,209 @@ +assertTrue($this->plugin->validateFilters($input, $filters, $test), $message); + } else { + $this->assertFalse($this->plugin->validateFilters($input, $filters, $test), $message); + } + + } + + function data() { + + $body1 = << 'title', 'is-not-defined' => false, 'param-filters' => [], 'text-matches' => []]; + + // Check if FOO is defined + $filter2 = + ['name' => 'foo', 'is-not-defined' => false, 'param-filters' => [], 'text-matches' => []]; + + // Check if TITLE is not defined + $filter3 = + ['name' => 'title', 'is-not-defined' => true, 'param-filters' => [], 'text-matches' => []]; + + // Check if FOO is not defined + $filter4 = + ['name' => 'foo', 'is-not-defined' => true, 'param-filters' => [], 'text-matches' => []]; + + // Check if TEL[TYPE] is defined + $filter5 = + [ + 'name' => 'tel', + 'is-not-defined' => false, + 'test' => 'anyof', + 'param-filters' => [ + [ + 'name' => 'type', + 'is-not-defined' => false, + 'text-match' => null + ], + ], + 'text-matches' => [], + ]; + + // Check if TEL[FOO] is defined + $filter6 = $filter5; + $filter6['param-filters'][0]['name'] = 'FOO'; + + // Check if TEL[TYPE] is not defined + $filter7 = $filter5; + $filter7['param-filters'][0]['is-not-defined'] = true; + + // Check if TEL[FOO] is not defined + $filter8 = $filter5; + $filter8['param-filters'][0]['name'] = 'FOO'; + $filter8['param-filters'][0]['is-not-defined'] = true; + + // Combining property filters + $filter9 = $filter5; + $filter9['param-filters'][] = $filter6['param-filters'][0]; + + $filter10 = $filter5; + $filter10['param-filters'][] = $filter6['param-filters'][0]; + $filter10['test'] = 'allof'; + + // Check if URL contains 'google' + $filter11 = + [ + 'name' => 'url', + 'is-not-defined' => false, + 'test' => 'anyof', + 'param-filters' => [], + 'text-matches' => [ + [ + 'match-type' => 'contains', + 'value' => 'google', + 'negate-condition' => false, + 'collation' => 'i;octet', + ], + ], + ]; + + // Check if URL contains 'bing' + $filter12 = $filter11; + $filter12['text-matches'][0]['value'] = 'bing'; + + // Check if URL does not contain 'google' + $filter13 = $filter11; + $filter13['text-matches'][0]['negate-condition'] = true; + + // Check if URL does not contain 'bing' + $filter14 = $filter11; + $filter14['text-matches'][0]['value'] = 'bing'; + $filter14['text-matches'][0]['negate-condition'] = true; + + // Param filter with text + $filter15 = $filter5; + $filter15['param-filters'][0]['text-match'] = [ + 'match-type' => 'contains', + 'value' => 'WORK', + 'collation' => 'i;octet', + 'negate-condition' => false, + ]; + $filter16 = $filter15; + $filter16['param-filters'][0]['text-match']['negate-condition'] = true; + + + // Param filter + text filter + $filter17 = $filter5; + $filter17['test'] = 'anyof'; + $filter17['text-matches'][] = [ + 'match-type' => 'contains', + 'value' => '444', + 'collation' => 'i;octet', + 'negate-condition' => false, + ]; + + $filter18 = $filter17; + $filter18['text-matches'][0]['negate-condition'] = true; + + $filter18['test'] = 'allof'; + + return [ + + // Basic filters + [$body1, [$filter1], 'anyof',true], + [$body1, [$filter2], 'anyof',false], + [$body1, [$filter3], 'anyof',false], + [$body1, [$filter4], 'anyof',true], + + // Combinations + [$body1, [$filter1, $filter2], 'anyof',true], + [$body1, [$filter1, $filter2], 'allof',false], + [$body1, [$filter1, $filter4], 'anyof',true], + [$body1, [$filter1, $filter4], 'allof',true], + [$body1, [$filter2, $filter3], 'anyof',false], + [$body1, [$filter2, $filter3], 'allof',false], + + // Basic parameters + [$body1, [$filter5], 'anyof', true, 'TEL;TYPE is defined, so this should return true'], + [$body1, [$filter6], 'anyof', false, 'TEL;FOO is not defined, so this should return false'], + + [$body1, [$filter7], 'anyof', false, 'TEL;TYPE is defined, so this should return false'], + [$body1, [$filter8], 'anyof', true, 'TEL;TYPE is not defined, so this should return true'], + + // Combined parameters + [$body1, [$filter9], 'anyof', true], + [$body1, [$filter10], 'anyof', false], + + // Text-filters + [$body1, [$filter11], 'anyof', true], + [$body1, [$filter12], 'anyof', false], + [$body1, [$filter13], 'anyof', false], + [$body1, [$filter14], 'anyof', true], + + // Param filter with text-match + [$body1, [$filter15], 'anyof', true], + [$body1, [$filter16], 'anyof', false], + + // Param filter + text filter + [$body1, [$filter17], 'anyof', true], + [$body1, [$filter18], 'anyof', false], + [$body1, [$filter18], 'anyof', false], + ]; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/ValidateVCardTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/ValidateVCardTest.php new file mode 100644 index 00000000000..acba2cfc8f3 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/ValidateVCardTest.php @@ -0,0 +1,305 @@ + 'addressbook1', + 'principaluri' => 'principals/admin', + 'uri' => 'addressbook1', + ] + ]; + + $this->cardBackend = new Backend\Mock($addressbooks, []); + $principalBackend = new DAVACL\PrincipalBackend\Mock(); + + $tree = [ + new AddressBookRoot($principalBackend, $this->cardBackend), + ]; + + $this->server = new DAV\Server($tree); + $this->server->sapi = new HTTP\SapiMock(); + $this->server->debugExceptions = true; + + $plugin = new Plugin(); + $this->server->addPlugin($plugin); + + $response = new HTTP\ResponseMock(); + $this->server->httpResponse = $response; + + } + + function request(HTTP\Request $request, $expectedStatus = null) { + + $this->server->httpRequest = $request; + $this->server->exec(); + + if ($expectedStatus) { + + $realStatus = $this->server->httpResponse->getStatus(); + + $msg = ''; + if ($realStatus !== $expectedStatus) { + $msg = 'Response body: ' . $this->server->httpResponse->getBodyAsString(); + } + $this->assertEquals( + $expectedStatus, + $realStatus, + $msg + ); + } + + return $this->server->httpResponse; + + } + + function testCreateFile() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'PUT', + 'REQUEST_URI' => '/addressbooks/admin/addressbook1/blabla.vcf', + ]); + + $response = $this->request($request); + + $this->assertEquals(415, $response->status); + + } + + function testCreateFileValid() { + + $request = new HTTP\Request( + 'PUT', + '/addressbooks/admin/addressbook1/blabla.vcf' + ); + + $vcard = <<setBody($vcard); + + $response = $this->request($request, 201); + + // The custom Ew header should not be set + $this->assertNull( + $response->getHeader('X-Sabre-Ew-Gross') + ); + // Valid, non-auto-fixed responses should contain an ETag. + $this->assertTrue( + $response->getHeader('ETag') !== null, + 'We did not receive an etag' + ); + + + $expected = [ + 'uri' => 'blabla.vcf', + 'carddata' => $vcard, + 'size' => strlen($vcard), + 'etag' => '"' . md5($vcard) . '"', + ]; + + $this->assertEquals($expected, $this->cardBackend->getCard('addressbook1', 'blabla.vcf')); + + } + + /** + * This test creates an intentionally broken vCard that vobject is able + * to automatically repair. + * + * @depends testCreateFileValid + */ + function testCreateVCardAutoFix() { + + $request = new HTTP\Request( + 'PUT', + '/addressbooks/admin/addressbook1/blabla.vcf' + ); + + // The error in this vcard is that there's not enough semi-colons in N + $vcard = <<setBody($vcard); + + $response = $this->request($request, 201); + + // Auto-fixed vcards should NOT return an etag + $this->assertNull( + $response->getHeader('ETag') + ); + + // We should have gotten an Ew header + $this->assertNotNull( + $response->getHeader('X-Sabre-Ew-Gross') + ); + + $expectedVCard = << 'blabla.vcf', + 'carddata' => $expectedVCard, + 'size' => strlen($expectedVCard), + 'etag' => '"' . md5($expectedVCard) . '"', + ]; + + $this->assertEquals($expected, $this->cardBackend->getCard('addressbook1', 'blabla.vcf')); + + } + + /** + * This test creates an intentionally broken vCard that vobject is able + * to automatically repair. + * + * However, we're supplying a heading asking the server to treat the + * request as strict, so the server should still let the request fail. + * + * @depends testCreateFileValid + */ + function testCreateVCardStrictFail() { + + $request = new HTTP\Request( + 'PUT', + '/addressbooks/admin/addressbook1/blabla.vcf', + [ + 'Prefer' => 'handling=strict', + ] + ); + + // The error in this vcard is that there's not enough semi-colons in N + $vcard = <<setBody($vcard); + $this->request($request, 415); + + } + + function testCreateFileNoUID() { + + $request = new HTTP\Request( + 'PUT', + '/addressbooks/admin/addressbook1/blabla.vcf' + ); + $vcard = <<setBody($vcard); + + $response = $this->request($request, 201); + + $foo = $this->cardBackend->getCard('addressbook1', 'blabla.vcf'); + $this->assertTrue( + strpos($foo['carddata'], 'UID') !== false, + print_r($foo, true) + ); + } + + function testCreateFileJson() { + + $request = new HTTP\Request( + 'PUT', + '/addressbooks/admin/addressbook1/blabla.vcf' + ); + $request->setBody('[ "vcard" , [ [ "VERSION", {}, "text", "4.0"], [ "UID" , {}, "text", "foo" ], [ "FN", {}, "text", "FirstName LastName"] ] ]'); + + $response = $this->request($request); + + $this->assertEquals(201, $response->status, 'Incorrect status returned! Full response body: ' . $response->body); + + $foo = $this->cardBackend->getCard('addressbook1', 'blabla.vcf'); + $this->assertEquals("BEGIN:VCARD\r\nVERSION:4.0\r\nUID:foo\r\nFN:FirstName LastName\r\nEND:VCARD\r\n", $foo['carddata']); + + } + + function testCreateFileVCalendar() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'PUT', + 'REQUEST_URI' => '/addressbooks/admin/addressbook1/blabla.vcf', + ]); + $request->setBody("BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n"); + + $response = $this->request($request); + + $this->assertEquals(415, $response->status, 'Incorrect status returned! Full response body: ' . $response->body); + + } + + function testUpdateFile() { + + $this->cardBackend->createCard('addressbook1', 'blabla.vcf', 'foo'); + $request = new HTTP\Request( + 'PUT', + '/addressbooks/admin/addressbook1/blabla.vcf' + ); + + $response = $this->request($request, 415); + + } + + function testUpdateFileParsableBody() { + + $this->cardBackend->createCard('addressbook1', 'blabla.vcf', 'foo'); + $request = new HTTP\Request( + 'PUT', + '/addressbooks/admin/addressbook1/blabla.vcf' + ); + + $body = "BEGIN:VCARD\r\nVERSION:4.0\r\nUID:foo\r\nFN:FirstName LastName\r\nEND:VCARD\r\n"; + $request->setBody($body); + + $response = $this->request($request, 204); + + $expected = [ + 'uri' => 'blabla.vcf', + 'carddata' => $body, + 'size' => strlen($body), + 'etag' => '"' . md5($body) . '"', + ]; + + $this->assertEquals($expected, $this->cardBackend->getCard('addressbook1', 'blabla.vcf')); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Xml/Property/SupportedAddressDataTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Xml/Property/SupportedAddressDataTest.php new file mode 100644 index 00000000000..43abebdcec1 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Xml/Property/SupportedAddressDataTest.php @@ -0,0 +1,38 @@ +assertInstanceOf('Sabre\CardDAV\Xml\Property\SupportedAddressData', $property); + + } + + /** + * @depends testSimple + */ + function testSerialize() { + + $property = new SupportedAddressData(); + + $this->namespaceMap[CardDAV\Plugin::NS_CARDDAV] = 'card'; + $xml = $this->write(['{DAV:}root' => $property]); + + $this->assertXmlStringEqualsXmlString( +' +' . +'' . +'' . +'' . +' +', $xml); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Xml/Property/SupportedCollationSetTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Xml/Property/SupportedCollationSetTest.php new file mode 100644 index 00000000000..e06aff101d9 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Xml/Property/SupportedCollationSetTest.php @@ -0,0 +1,38 @@ +assertInstanceOf('Sabre\CardDAV\Xml\Property\SupportedCollationSet', $property); + + } + + /** + * @depends testSimple + */ + function testSerialize() { + + $property = new SupportedCollationSet(); + + $this->namespaceMap[CardDAV\Plugin::NS_CARDDAV] = 'card'; + $xml = $this->write(['{DAV:}root' => $property]); + + $this->assertXmlStringEqualsXmlString( +' +' . +'i;ascii-casemap' . +'i;octet' . +'i;unicode-casemap' . +' +', $xml); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Xml/Request/AddressBookMultiGetTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Xml/Request/AddressBookMultiGetTest.php new file mode 100644 index 00000000000..ea2ab75ceec --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Xml/Request/AddressBookMultiGetTest.php @@ -0,0 +1,47 @@ + 'Sabre\\CardDAV\\Xml\\Request\AddressBookMultiGetReport', + ]; + + function testDeserialize() { + + /* lines look a bit odd but this triggers an XML parsing bug */ + $xml = << + + + + + /foo.vcf + +XML; + + $result = $this->parse($xml); + $addressBookMultiGetReport = new AddressBookMultiGetReport(); + $addressBookMultiGetReport->properties = [ + '{DAV:}getcontenttype', + '{DAV:}getetag', + '{urn:ietf:params:xml:ns:carddav}address-data', + ]; + $addressBookMultiGetReport->hrefs = ['/foo.vcf']; + $addressBookMultiGetReport->contentType = 'text/vcard'; + $addressBookMultiGetReport->version = '4.0'; + $addressBookMultiGetReport->addressDataProperties = []; + + + $this->assertEquals( + $addressBookMultiGetReport, + $result['value'] + ); + + } + + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Xml/Request/AddressBookQueryReportTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Xml/Request/AddressBookQueryReportTest.php new file mode 100644 index 00000000000..3a2e4b46a0e --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/CardDAV/Xml/Request/AddressBookQueryReportTest.php @@ -0,0 +1,350 @@ + 'Sabre\\CardDAV\\Xml\\Request\AddressBookQueryReport', + ]; + + function testDeserialize() { + + $xml = << + + + + + + + + +XML; + + $result = $this->parse($xml); + $addressBookQueryReport = new AddressBookQueryReport(); + $addressBookQueryReport->properties = [ + '{DAV:}getetag', + ]; + $addressBookQueryReport->test = 'anyof'; + $addressBookQueryReport->filters = [ + [ + 'name' => 'uid', + 'test' => 'anyof', + 'is-not-defined' => false, + 'param-filters' => [], + 'text-matches' => [], + ] + ]; + + $this->assertEquals( + $addressBookQueryReport, + $result['value'] + ); + + } + + function testDeserializeAllOf() { + + $xml = << + + + + + + + + +XML; + + $result = $this->parse($xml); + $addressBookQueryReport = new AddressBookQueryReport(); + $addressBookQueryReport->properties = [ + '{DAV:}getetag', + ]; + $addressBookQueryReport->test = 'allof'; + $addressBookQueryReport->filters = [ + [ + 'name' => 'uid', + 'test' => 'anyof', + 'is-not-defined' => false, + 'param-filters' => [], + 'text-matches' => [], + ] + ]; + + $this->assertEquals( + $addressBookQueryReport, + $result['value'] + ); + + } + + /** + * @expectedException \Sabre\DAV\Exception\BadRequest + */ + function testDeserializeBadTest() { + + $xml = << + + + + + + + + +XML; + + $this->parse($xml); + + } + + /** + * We should error on this, but KDE does this, so we chose to support it. + */ + function testDeserializeNoFilter() { + + $xml = << + + + + + +XML; + + $result = $this->parse($xml); + $addressBookQueryReport = new AddressBookQueryReport(); + $addressBookQueryReport->properties = [ + '{DAV:}getetag', + ]; + $addressBookQueryReport->test = 'anyof'; + $addressBookQueryReport->filters = []; + + $this->assertEquals( + $addressBookQueryReport, + $result['value'] + ); + + } + + function testDeserializeComplex() { + + $xml = << + + + + + + + + + + + + + + + + Hello! + + + + No + + + 10 + +XML; + + $result = $this->parse($xml); + $addressBookQueryReport = new AddressBookQueryReport(); + $addressBookQueryReport->properties = [ + '{DAV:}getetag', + '{urn:ietf:params:xml:ns:carddav}address-data', + ]; + $addressBookQueryReport->test = 'anyof'; + $addressBookQueryReport->filters = [ + [ + 'name' => 'uid', + 'test' => 'anyof', + 'is-not-defined' => true, + 'param-filters' => [], + 'text-matches' => [], + ], + [ + 'name' => 'x-foo', + 'test' => 'allof', + 'is-not-defined' => false, + 'param-filters' => [ + [ + 'name' => 'x-param1', + 'is-not-defined' => false, + 'text-match' => null, + ], + [ + 'name' => 'x-param2', + 'is-not-defined' => true, + 'text-match' => null, + ], + [ + 'name' => 'x-param3', + 'is-not-defined' => false, + 'text-match' => [ + 'negate-condition' => false, + 'value' => 'Hello!', + 'match-type' => 'contains', + 'collation' => 'i;unicode-casemap', + ], + ], + ], + 'text-matches' => [], + ], + [ + 'name' => 'x-prop2', + 'test' => 'anyof', + 'is-not-defined' => false, + 'param-filters' => [], + 'text-matches' => [ + [ + 'negate-condition' => true, + 'value' => 'No', + 'match-type' => 'starts-with', + 'collation' => 'i;unicode-casemap', + ], + ], + ] + ]; + + $addressBookQueryReport->version = '4.0'; + $addressBookQueryReport->contentType = 'application/vcard+json'; + $addressBookQueryReport->limit = 10; + + $this->assertEquals( + $addressBookQueryReport, + $result['value'] + ); + + } + + /** + * @expectedException \Sabre\DAV\Exception\BadRequest + */ + function testDeserializeBadMatchType() { + + $xml = << + + + + + + + + Hello! + + + + +XML; + $this->parse($xml); + + } + + /** + * @expectedException \Sabre\DAV\Exception\BadRequest + */ + function testDeserializeBadMatchType2() { + + $xml = << + + + + + + + No + + + +XML; + $this->parse($xml); + + } + + /** + * @expectedException \Sabre\DAV\Exception\BadRequest + */ + function testDeserializeDoubleFilter() { + + $xml = << + + + + + + + + + +XML; + $this->parse($xml); + + } + + function testDeserializeAddressbookElements() { + + $xml = << + + + + + + + + + + + + + +XML; + + $result = $this->parse($xml); + $addressBookQueryReport = new AddressBookQueryReport(); + $addressBookQueryReport->properties = [ + '{DAV:}getetag', + '{urn:ietf:params:xml:ns:carddav}address-data' + ]; + $addressBookQueryReport->filters = []; + $addressBookQueryReport->test = 'anyof'; + $addressBookQueryReport->contentType = 'text/vcard'; + $addressBookQueryReport->version = '3.0'; + $addressBookQueryReport->addressDataProperties = [ + 'VERSION', + 'UID', + 'NICKNAME', + 'EMAIL', + 'FN', + 'TEL', + ]; + + $this->assertEquals( + $addressBookQueryReport, + $result['value'] + ); + + } + + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/AbstractServer.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/AbstractServer.php new file mode 100644 index 00000000000..6a8d389a008 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/AbstractServer.php @@ -0,0 +1,64 @@ +response = new HTTP\ResponseMock(); + $this->server = new Server($this->getRootNode()); + $this->server->sapi = new HTTP\SapiMock(); + $this->server->httpResponse = $this->response; + $this->server->debugExceptions = true; + $this->deleteTree(SABRE_TEMPDIR, false); + file_put_contents(SABRE_TEMPDIR . '/test.txt', 'Test contents'); + mkdir(SABRE_TEMPDIR . '/dir'); + file_put_contents(SABRE_TEMPDIR . '/dir/child.txt', 'Child contents'); + + + } + + function tearDown() { + + $this->deleteTree(SABRE_TEMPDIR, false); + + } + + protected function getRootNode() { + + return new FS\Directory(SABRE_TEMPDIR); + + } + + private function deleteTree($path, $deleteRoot = true) { + + foreach (scandir($path) as $node) { + + if ($node == '.' || $node == '.svn' || $node == '..') continue; + $myPath = $path . '/' . $node; + if (is_file($myPath)) { + unlink($myPath); + } else { + $this->deleteTree($myPath); + } + + } + if ($deleteRoot) rmdir($path); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractBasicTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractBasicTest.php new file mode 100644 index 00000000000..455403affe7 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractBasicTest.php @@ -0,0 +1,91 @@ +assertFalse( + $backend->check($request, $response)[0] + ); + + } + + function testCheckUnknownUser() { + + $request = HTTP\Sapi::createFromServerArray([ + 'PHP_AUTH_USER' => 'username', + 'PHP_AUTH_PW' => 'wrongpassword', + ]); + $response = new HTTP\Response(); + + $backend = new AbstractBasicMock(); + + $this->assertFalse( + $backend->check($request, $response)[0] + ); + + } + + function testCheckSuccess() { + + $request = HTTP\Sapi::createFromServerArray([ + 'PHP_AUTH_USER' => 'username', + 'PHP_AUTH_PW' => 'password', + ]); + $response = new HTTP\Response(); + + $backend = new AbstractBasicMock(); + $this->assertEquals( + [true, 'principals/username'], + $backend->check($request, $response) + ); + + } + + function testRequireAuth() { + + $request = new HTTP\Request(); + $response = new HTTP\Response(); + + $backend = new AbstractBasicMock(); + $backend->setRealm('writing unittests on a saturday night'); + $backend->challenge($request, $response); + + $this->assertEquals( + 'Basic realm="writing unittests on a saturday night"', + $response->getHeader('WWW-Authenticate') + ); + + } + +} + + +class AbstractBasicMock extends AbstractBasic { + + /** + * 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 + */ + function validateUserPass($username, $password) { + + return ($username == 'username' && $password == 'password'); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractBearerTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractBearerTest.php new file mode 100644 index 00000000000..c3857883007 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractBearerTest.php @@ -0,0 +1,90 @@ +assertFalse( + $backend->check($request, $response)[0] + ); + + } + + function testCheckInvalidToken() { + + $request = HTTP\Sapi::createFromServerArray([ + 'HTTP_AUTHORIZATION' => 'Bearer foo', + ]); + $response = new HTTP\Response(); + + $backend = new AbstractBearerMock(); + + $this->assertFalse( + $backend->check($request, $response)[0] + ); + + } + + function testCheckSuccess() { + + $request = HTTP\Sapi::createFromServerArray([ + 'HTTP_AUTHORIZATION' => 'Bearer valid', + ]); + $response = new HTTP\Response(); + + $backend = new AbstractBearerMock(); + $this->assertEquals( + [true, 'principals/username'], + $backend->check($request, $response) + ); + + } + + function testRequireAuth() { + + $request = new HTTP\Request(); + $response = new HTTP\Response(); + + $backend = new AbstractBearerMock(); + $backend->setRealm('writing unittests on a saturday night'); + $backend->challenge($request, $response); + + $this->assertEquals( + 'Bearer realm="writing unittests on a saturday night"', + $response->getHeader('WWW-Authenticate') + ); + + } + +} + + +class AbstractBearerMock extends AbstractBearer { + + /** + * Validates a bearer token + * + * This method should return true or false depending on if login + * succeeded. + * + * @param string $bearerToken + * @return bool + */ + function validateBearerToken($bearerToken) { + + return 'valid' === $bearerToken ? 'principals/username' : false; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractDigestTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractDigestTest.php new file mode 100644 index 00000000000..14c72aaa0f6 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractDigestTest.php @@ -0,0 +1,138 @@ +assertFalse( + $backend->check($request, $response)[0] + ); + + } + + function testCheckBadGetUserInfoResponse() { + + $header = 'username=null, realm=myRealm, nonce=12345, uri=/, response=HASH, opaque=1, qop=auth, nc=1, cnonce=1'; + $request = HTTP\Sapi::createFromServerArray([ + 'PHP_AUTH_DIGEST' => $header, + ]); + $response = new HTTP\Response(); + + $backend = new AbstractDigestMock(); + $this->assertFalse( + $backend->check($request, $response)[0] + ); + + } + + /** + * @expectedException Sabre\DAV\Exception + */ + function testCheckBadGetUserInfoResponse2() { + + $header = 'username=array, realm=myRealm, nonce=12345, uri=/, response=HASH, opaque=1, qop=auth, nc=1, cnonce=1'; + $request = HTTP\Sapi::createFromServerArray([ + 'PHP_AUTH_DIGEST' => $header, + ]); + + $response = new HTTP\Response(); + + $backend = new AbstractDigestMock(); + $backend->check($request, $response); + + } + + function testCheckUnknownUser() { + + $header = 'username=false, realm=myRealm, nonce=12345, uri=/, response=HASH, opaque=1, qop=auth, nc=1, cnonce=1'; + $request = HTTP\Sapi::createFromServerArray([ + 'PHP_AUTH_DIGEST' => $header, + ]); + + $response = new HTTP\Response(); + + $backend = new AbstractDigestMock(); + $this->assertFalse( + $backend->check($request, $response)[0] + ); + + } + + function testCheckBadPassword() { + + $header = 'username=user, realm=myRealm, nonce=12345, uri=/, response=HASH, opaque=1, qop=auth, nc=1, cnonce=1'; + $request = HTTP\Sapi::createFromServerArray([ + 'PHP_AUTH_DIGEST' => $header, + 'REQUEST_METHOD' => 'PUT', + ]); + + $response = new HTTP\Response(); + + $backend = new AbstractDigestMock(); + $this->assertFalse( + $backend->check($request, $response)[0] + ); + + } + + function testCheck() { + + $digestHash = md5('HELLO:12345:1:1:auth:' . md5('GET:/')); + $header = 'username=user, realm=myRealm, nonce=12345, uri=/, response=' . $digestHash . ', opaque=1, qop=auth, nc=1, cnonce=1'; + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'GET', + 'PHP_AUTH_DIGEST' => $header, + 'REQUEST_URI' => '/', + ]); + + $response = new HTTP\Response(); + + $backend = new AbstractDigestMock(); + $this->assertEquals( + [true, 'principals/user'], + $backend->check($request, $response) + ); + + } + + function testRequireAuth() { + + $request = new HTTP\Request(); + $response = new HTTP\Response(); + + $backend = new AbstractDigestMock(); + $backend->setRealm('writing unittests on a saturday night'); + $backend->challenge($request, $response); + + $this->assertStringStartsWith( + 'Digest realm="writing unittests on a saturday night"', + $response->getHeader('WWW-Authenticate') + ); + + } + +} + + +class AbstractDigestMock extends AbstractDigest { + + function getDigestHash($realm, $userName) { + + switch ($userName) { + case 'null' : return null; + case 'false' : return false; + case 'array' : return []; + case 'user' : return 'HELLO'; + } + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractPDOTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractPDOTest.php new file mode 100644 index 00000000000..b14e9fa2ea3 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/AbstractPDOTest.php @@ -0,0 +1,45 @@ +dropTables('users'); + $this->createSchema('users'); + + $this->getPDO()->query( + "INSERT INTO users (username,digesta1) VALUES ('user','hash')" + + ); + + } + + function testConstruct() { + + $pdo = $this->getPDO(); + $backend = new PDO($pdo); + $this->assertTrue($backend instanceof PDO); + + } + + /** + * @depends testConstruct + */ + function testUserInfo() { + + $pdo = $this->getPDO(); + $backend = new PDO($pdo); + + $this->assertNull($backend->getDigestHash('realm', 'blabla')); + + $expected = 'hash'; + + $this->assertEquals($expected, $backend->getDigestHash('realm', 'user')); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/ApacheTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/ApacheTest.php new file mode 100644 index 00000000000..29cbc216282 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/ApacheTest.php @@ -0,0 +1,71 @@ +assertInstanceOf('Sabre\DAV\Auth\Backend\Apache', $backend); + + } + + function testNoHeader() { + + $request = new HTTP\Request(); + $response = new HTTP\Response(); + $backend = new Apache(); + + $this->assertFalse( + $backend->check($request, $response)[0] + ); + + } + + function testRemoteUser() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REMOTE_USER' => 'username', + ]); + $response = new HTTP\Response(); + $backend = new Apache(); + + $this->assertEquals( + [true, 'principals/username'], + $backend->check($request, $response) + ); + + } + + function testRedirectRemoteUser() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REDIRECT_REMOTE_USER' => 'username', + ]); + $response = new HTTP\Response(); + $backend = new Apache(); + + $this->assertEquals( + [true, 'principals/username'], + $backend->check($request, $response) + ); + + } + + function testRequireAuth() { + + $request = new HTTP\Request(); + $response = new HTTP\Response(); + + $backend = new Apache(); + $backend->challenge($request, $response); + + $this->assertNull( + $response->getHeader('WWW-Authenticate') + ); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/BasicCallBackTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/BasicCallBackTest.php new file mode 100644 index 00000000000..167f31eab37 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/BasicCallBackTest.php @@ -0,0 +1,36 @@ + 'Basic ' . base64_encode('foo:bar'), + ]); + $response = new Response(); + + $this->assertEquals( + [true, 'principals/foo'], + $backend->check($request, $response) + ); + + $this->assertEquals(['foo', 'bar'], $args); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/FileTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/FileTest.php new file mode 100644 index 00000000000..f694f4806ee --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/FileTest.php @@ -0,0 +1,41 @@ +assertTrue($file instanceof File); + + } + + /** + * @expectedException Sabre\DAV\Exception + */ + function testLoadFileBroken() { + + file_put_contents(SABRE_TEMPDIR . '/backend', 'user:realm:hash'); + $file = new File(SABRE_TEMPDIR . '/backend'); + + } + + function testLoadFile() { + + file_put_contents(SABRE_TEMPDIR . '/backend', 'user:realm:' . md5('user:realm:password')); + $file = new File(); + $file->loadFile(SABRE_TEMPDIR . '/backend'); + + $this->assertFalse($file->getDigestHash('realm', 'blabla')); + $this->assertEquals(md5('user:realm:password'), $file->getDigestHash('realm', 'user')); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/Mock.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/Mock.php new file mode 100644 index 00000000000..369bc249e47 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/Mock.php @@ -0,0 +1,87 @@ +principal = $principal; + + } + + /** + * 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] + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return array + */ + function check(RequestInterface $request, ResponseInterface $response) { + + if ($this->invalidCheckResponse) { + return 'incorrect!'; + } + if ($this->fail) { + return [false, "fail!"]; + } + return [true, $this->principal]; + + } + + /** + * This method is called when a user could not be authenticated, and + * authentication was required for the current request. + * + * This gives you the oppurtunity 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. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return void + */ + function challenge(RequestInterface $request, ResponseInterface $response) { + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/PDOMySQLTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/PDOMySQLTest.php new file mode 100644 index 00000000000..18f59793ad7 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Auth/Backend/PDOMySQLTest.php @@ -0,0 +1,9 @@ +assertTrue($plugin instanceof Plugin); + $fakeServer->addPlugin($plugin); + $this->assertEquals($plugin, $fakeServer->getPlugin('auth')); + $this->assertInternalType('array', $plugin->getPluginInfo()); + + } + + /** + * @depends testInit + */ + function testAuthenticate() { + + $fakeServer = new DAV\Server(new DAV\SimpleCollection('bla')); + $plugin = new Plugin(new Backend\Mock()); + $fakeServer->addPlugin($plugin); + $this->assertTrue( + $fakeServer->emit('beforeMethod', [new HTTP\Request(), new HTTP\Response()]) + ); + + } + + /** + * @depends testInit + * @expectedException Sabre\DAV\Exception\NotAuthenticated + */ + function testAuthenticateFail() { + + $fakeServer = new DAV\Server(new DAV\SimpleCollection('bla')); + $backend = new Backend\Mock(); + $backend->fail = true; + + $plugin = new Plugin($backend); + $fakeServer->addPlugin($plugin); + $fakeServer->emit('beforeMethod', [new HTTP\Request(), new HTTP\Response()]); + + } + + /** + * @depends testAuthenticateFail + */ + function testAuthenticateFailDontAutoRequire() { + + $fakeServer = new DAV\Server(new DAV\SimpleCollection('bla')); + $backend = new Backend\Mock(); + $backend->fail = true; + + $plugin = new Plugin($backend); + $plugin->autoRequireLogin = false; + $fakeServer->addPlugin($plugin); + $this->assertTrue( + $fakeServer->emit('beforeMethod', [new HTTP\Request(), new HTTP\Response()]) + ); + $this->assertEquals(1, count($plugin->getLoginFailedReasons())); + + } + + /** + * @depends testAuthenticate + */ + function testMultipleBackend() { + + $fakeServer = new DAV\Server(new DAV\SimpleCollection('bla')); + $backend1 = new Backend\Mock(); + $backend2 = new Backend\Mock(); + $backend2->fail = true; + + $plugin = new Plugin(); + $plugin->addBackend($backend1); + $plugin->addBackend($backend2); + + $fakeServer->addPlugin($plugin); + $fakeServer->emit('beforeMethod', [new HTTP\Request(), new HTTP\Response()]); + + $this->assertEquals('principals/admin', $plugin->getCurrentPrincipal()); + + } + + /** + * @depends testInit + * @expectedException Sabre\DAV\Exception + */ + function testNoAuthBackend() { + + $fakeServer = new DAV\Server(new DAV\SimpleCollection('bla')); + + $plugin = new Plugin(); + $fakeServer->addPlugin($plugin); + $fakeServer->emit('beforeMethod', [new HTTP\Request(), new HTTP\Response()]); + + } + /** + * @depends testInit + * @expectedException Sabre\DAV\Exception + */ + function testInvalidCheckResponse() { + + $fakeServer = new DAV\Server(new DAV\SimpleCollection('bla')); + $backend = new Backend\Mock(); + $backend->invalidCheckResponse = true; + + $plugin = new Plugin($backend); + $fakeServer->addPlugin($plugin); + $fakeServer->emit('beforeMethod', [new HTTP\Request(), new HTTP\Response()]); + + } + + /** + * @depends testAuthenticate + */ + function testGetCurrentPrincipal() { + + $fakeServer = new DAV\Server(new DAV\SimpleCollection('bla')); + $plugin = new Plugin(new Backend\Mock()); + $fakeServer->addPlugin($plugin); + $fakeServer->emit('beforeMethod', [new HTTP\Request(), new HTTP\Response()]); + $this->assertEquals('principals/admin', $plugin->getCurrentPrincipal()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/BasicNodeTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/BasicNodeTest.php new file mode 100644 index 00000000000..ec104ec805e --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/BasicNodeTest.php @@ -0,0 +1,235 @@ +put('hi'); + + } + + /** + * @expectedException Sabre\DAV\Exception\Forbidden + */ + function testGet() { + + $file = new FileMock(); + $file->get(); + + } + + function testGetSize() { + + $file = new FileMock(); + $this->assertEquals(0, $file->getSize()); + + } + + + function testGetETag() { + + $file = new FileMock(); + $this->assertNull($file->getETag()); + + } + + function testGetContentType() { + + $file = new FileMock(); + $this->assertNull($file->getContentType()); + + } + + /** + * @expectedException Sabre\DAV\Exception\Forbidden + */ + function testDelete() { + + $file = new FileMock(); + $file->delete(); + + } + + /** + * @expectedException Sabre\DAV\Exception\Forbidden + */ + function testSetName() { + + $file = new FileMock(); + $file->setName('hi'); + + } + + function testGetLastModified() { + + $file = new FileMock(); + // checking if lastmod is within the range of a few seconds + $lastMod = $file->getLastModified(); + $compareTime = ($lastMod + 1) - time(); + $this->assertTrue($compareTime < 3); + + } + + function testGetChild() { + + $dir = new DirectoryMock(); + $file = $dir->getChild('mockfile'); + $this->assertTrue($file instanceof FileMock); + + } + + function testChildExists() { + + $dir = new DirectoryMock(); + $this->assertTrue($dir->childExists('mockfile')); + + } + + function testChildExistsFalse() { + + $dir = new DirectoryMock(); + $this->assertFalse($dir->childExists('mockfile2')); + + } + + /** + * @expectedException Sabre\DAV\Exception\NotFound + */ + function testGetChild404() { + + $dir = new DirectoryMock(); + $file = $dir->getChild('blabla'); + + } + + /** + * @expectedException Sabre\DAV\Exception\Forbidden + */ + function testCreateFile() { + + $dir = new DirectoryMock(); + $dir->createFile('hello', 'data'); + + } + + /** + * @expectedException Sabre\DAV\Exception\Forbidden + */ + function testCreateDirectory() { + + $dir = new DirectoryMock(); + $dir->createDirectory('hello'); + + } + + function testSimpleDirectoryConstruct() { + + $dir = new SimpleCollection('simpledir', []); + $this->assertInstanceOf('Sabre\DAV\SimpleCollection', $dir); + + } + + /** + * @depends testSimpleDirectoryConstruct + */ + function testSimpleDirectoryConstructChild() { + + $file = new FileMock(); + $dir = new SimpleCollection('simpledir', [$file]); + $file2 = $dir->getChild('mockfile'); + + $this->assertEquals($file, $file2); + + } + + /** + * @expectedException Sabre\DAV\Exception + * @depends testSimpleDirectoryConstruct + */ + function testSimpleDirectoryBadParam() { + + $dir = new SimpleCollection('simpledir', ['string shouldn\'t be here']); + + } + + /** + * @depends testSimpleDirectoryConstruct + */ + function testSimpleDirectoryAddChild() { + + $file = new FileMock(); + $dir = new SimpleCollection('simpledir'); + $dir->addChild($file); + $file2 = $dir->getChild('mockfile'); + + $this->assertEquals($file, $file2); + + } + + /** + * @depends testSimpleDirectoryConstruct + * @depends testSimpleDirectoryAddChild + */ + function testSimpleDirectoryGetChildren() { + + $file = new FileMock(); + $dir = new SimpleCollection('simpledir'); + $dir->addChild($file); + + $this->assertEquals([$file], $dir->getChildren()); + + } + + /* + * @depends testSimpleDirectoryConstruct + */ + function testSimpleDirectoryGetName() { + + $dir = new SimpleCollection('simpledir'); + $this->assertEquals('simpledir', $dir->getName()); + + } + + /** + * @depends testSimpleDirectoryConstruct + * @expectedException Sabre\DAV\Exception\NotFound + */ + function testSimpleDirectoryGetChild404() { + + $dir = new SimpleCollection('simpledir'); + $dir->getChild('blabla'); + + } +} + +class DirectoryMock extends Collection { + + function getName() { + + return 'mockdir'; + + } + + function getChildren() { + + return [new FileMock()]; + + } + +} + +class FileMock extends File { + + function getName() { + + return 'mockfile'; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Browser/GuessContentTypeTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Browser/GuessContentTypeTest.php new file mode 100644 index 00000000000..54a3053ec9f --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Browser/GuessContentTypeTest.php @@ -0,0 +1,70 @@ +server->getPropertiesForPath('/somefile.jpg', $properties); + $this->assertArrayHasKey(0, $result); + $this->assertArrayHasKey(404, $result[0]); + $this->assertArrayHasKey('{DAV:}getcontenttype', $result[0][404]); + + } + + /** + * @depends testGetProperties + */ + function testGetPropertiesPluginEnabled() { + + $this->server->addPlugin(new GuessContentType()); + $properties = [ + '{DAV:}getcontenttype', + ]; + $result = $this->server->getPropertiesForPath('/somefile.jpg', $properties); + $this->assertArrayHasKey(0, $result); + $this->assertArrayHasKey(200, $result[0], 'We received: ' . print_r($result, true)); + $this->assertArrayHasKey('{DAV:}getcontenttype', $result[0][200]); + $this->assertEquals('image/jpeg', $result[0][200]['{DAV:}getcontenttype']); + + } + + /** + * @depends testGetPropertiesPluginEnabled + */ + function testGetPropertiesUnknown() { + + $this->server->addPlugin(new GuessContentType()); + $properties = [ + '{DAV:}getcontenttype', + ]; + $result = $this->server->getPropertiesForPath('/somefile.hoi', $properties); + $this->assertArrayHasKey(0, $result); + $this->assertArrayHasKey(200, $result[0]); + $this->assertArrayHasKey('{DAV:}getcontenttype', $result[0][200]); + $this->assertEquals('application/octet-stream', $result[0][200]['{DAV:}getcontenttype']); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Browser/MapGetToPropFindTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Browser/MapGetToPropFindTest.php new file mode 100644 index 00000000000..33c4ede96b4 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Browser/MapGetToPropFindTest.php @@ -0,0 +1,44 @@ +server->addPlugin(new MapGetToPropFind()); + + } + + function testCollectionGet() { + + $serverVars = [ + 'REQUEST_URI' => '/', + 'REQUEST_METHOD' => 'GET', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody(''); + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals(207, $this->response->status, 'Incorrect status response received. Full response body: ' . $this->response->body); + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + 'DAV' => ['1, 3, extended-mkcol'], + 'Vary' => ['Brief,Prefer'], + ], + $this->response->getHeaders() + ); + + } + + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Browser/PluginTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Browser/PluginTest.php new file mode 100644 index 00000000000..f20c50f8637 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Browser/PluginTest.php @@ -0,0 +1,186 @@ +server->addPlugin($this->plugin = new Plugin()); + $this->server->tree->getNodeForPath('')->createDirectory('dir2'); + + } + + function testCollectionGet() { + + $request = new HTTP\Request('GET', '/dir'); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(200, $this->response->getStatus(), "Incorrect status received. Full response body: " . $this->response->getBodyAsString()); + $this->assertEquals( + [ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Type' => ['text/html; charset=utf-8'], + 'Content-Security-Policy' => ["default-src 'none'; img-src 'self'; style-src 'self'; font-src 'self';"] + ], + $this->response->getHeaders() + ); + + $body = $this->response->getBodyAsString(); + $this->assertTrue(strpos($body, 'dir') !== false, $body); + $this->assertTrue(strpos($body, '<a href="/dir/child.txt">') !== false); + + } + + /** + * Adding the If-None-Match should have 0 effect, but it threw an error. + */ + function testCollectionGetIfNoneMatch() { + + $request = new HTTP\Request('GET', '/dir'); + $request->setHeader('If-None-Match', '"foo-bar"'); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(200, $this->response->getStatus(), "Incorrect status received. Full response body: " . $this->response->getBodyAsString()); + $this->assertEquals( + [ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Type' => ['text/html; charset=utf-8'], + 'Content-Security-Policy' => ["default-src 'none'; img-src 'self'; style-src 'self'; font-src 'self';"] + ], + $this->response->getHeaders() + ); + + $body = $this->response->getBodyAsString(); + $this->assertTrue(strpos($body, '<title>dir') !== false, $body); + $this->assertTrue(strpos($body, '<a href="/dir/child.txt">') !== false); + + } + function testCollectionGetRoot() { + + $request = new HTTP\Request('GET', '/'); + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals(200, $this->response->status, "Incorrect status received. Full response body: " . $this->response->getBodyAsString()); + $this->assertEquals( + [ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Type' => ['text/html; charset=utf-8'], + 'Content-Security-Policy' => ["default-src 'none'; img-src 'self'; style-src 'self'; font-src 'self';"] + ], + $this->response->getHeaders() + ); + + $body = $this->response->getBodyAsString(); + $this->assertTrue(strpos($body, '<title>/') !== false, $body); + $this->assertTrue(strpos($body, '<a href="/dir/">') !== false); + $this->assertTrue(strpos($body, '<span class="btn disabled">') !== false); + + } + + function testGETPassthru() { + + $request = new HTTP\Request('GET', '/random'); + $response = new HTTP\Response(); + $this->assertNull( + $this->plugin->httpGet($request, $response) + ); + + } + + function testPostOtherContentType() { + + $request = new HTTP\Request('POST', '/', ['Content-Type' => 'text/xml']); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(501, $this->response->status); + + } + + function testPostNoSabreAction() { + + $request = new HTTP\Request('POST', '/', ['Content-Type' => 'application/x-www-form-urlencoded']); + $request->setPostData([]); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(501, $this->response->status); + + } + + function testPostMkCol() { + + $serverVars = [ + 'REQUEST_URI' => '/', + 'REQUEST_METHOD' => 'POST', + 'CONTENT_TYPE' => 'application/x-www-form-urlencoded', + ]; + $postVars = [ + 'sabreAction' => 'mkcol', + 'name' => 'new_collection', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setPostData($postVars); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(302, $this->response->status); + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Location' => ['/'], + ], $this->response->getHeaders()); + + $this->assertTrue(is_dir(SABRE_TEMPDIR . '/new_collection')); + + } + + function testGetAsset() { + + $request = new HTTP\Request('GET', '/?sabreAction=asset&assetName=favicon.ico'); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(200, $this->response->getStatus(), 'Error: ' . $this->response->body); + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Type' => ['image/vnd.microsoft.icon'], + 'Content-Length' => ['4286'], + 'Cache-Control' => ['public, max-age=1209600'], + 'Content-Security-Policy' => ["default-src 'none'; img-src 'self'; style-src 'self'; font-src 'self';"] + ], $this->response->getHeaders()); + + } + + function testGetAsset404() { + + $request = new HTTP\Request('GET', '/?sabreAction=asset&assetName=flavicon.ico'); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(404, $this->response->getStatus(), 'Error: ' . $this->response->body); + + } + + function testGetAssetEscapeBasePath() { + + $request = new HTTP\Request('GET', '/?sabreAction=asset&assetName=./../assets/favicon.ico'); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(404, $this->response->getStatus(), 'Error: ' . $this->response->body); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Browser/PropFindAllTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Browser/PropFindAllTest.php new file mode 100644 index 00000000000..08c2080bbab --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Browser/PropFindAllTest.php @@ -0,0 +1,70 @@ +<?php + +namespace Sabre\DAV\Browser; + +class PropFindAllTest extends \PHPUnit_Framework_TestCase { + + function testHandleSimple() { + + $pf = new PropFindAll('foo'); + $pf->handle('{DAV:}displayname', 'foo'); + + $this->assertEquals(200, $pf->getStatus('{DAV:}displayname')); + $this->assertEquals('foo', $pf->get('{DAV:}displayname')); + + + } + + function testHandleCallBack() { + + $pf = new PropFindAll('foo'); + $pf->handle('{DAV:}displayname', function() { return 'foo'; }); + + $this->assertEquals(200, $pf->getStatus('{DAV:}displayname')); + $this->assertEquals('foo', $pf->get('{DAV:}displayname')); + + } + + function testSet() { + + $pf = new PropFindAll('foo'); + $pf->set('{DAV:}displayname', 'foo'); + + $this->assertEquals(200, $pf->getStatus('{DAV:}displayname')); + $this->assertEquals('foo', $pf->get('{DAV:}displayname')); + + } + + function testSetNull() { + + $pf = new PropFindAll('foo'); + $pf->set('{DAV:}displayname', null); + + $this->assertEquals(404, $pf->getStatus('{DAV:}displayname')); + $this->assertEquals(null, $pf->get('{DAV:}displayname')); + + } + + function testGet404Properties() { + + $pf = new PropFindAll('foo'); + $pf->set('{DAV:}displayname', null); + $this->assertEquals( + ['{DAV:}displayname'], + $pf->get404Properties() + ); + + } + + function testGet404PropertiesNothing() { + + $pf = new PropFindAll('foo'); + $pf->set('{DAV:}displayname', 'foo'); + $this->assertEquals( + ['{http://sabredav.org/ns}idk'], + $pf->get404Properties() + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ClientMock.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ClientMock.php new file mode 100644 index 00000000000..5a48b063ce8 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ClientMock.php @@ -0,0 +1,34 @@ +<?php + +namespace Sabre\DAV; + +use Sabre\HTTP\RequestInterface; + +class ClientMock extends Client { + + public $request; + public $response; + + public $url; + public $curlSettings; + + /** + * Just making this method public + * + * @param string $url + * @return string + */ + function getAbsoluteUrl($url) { + + return parent::getAbsoluteUrl($url); + + } + + function doRequest(RequestInterface $request) { + + $this->request = $request; + return $this->response; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ClientTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ClientTest.php new file mode 100644 index 00000000000..687f61e2fd2 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ClientTest.php @@ -0,0 +1,306 @@ +<?php + +namespace Sabre\DAV; + +use Sabre\HTTP\Request; +use Sabre\HTTP\Response; + +require_once 'Sabre/DAV/ClientMock.php'; + +class ClientTest extends \PHPUnit_Framework_TestCase { + + function setUp() { + + if (!function_exists('curl_init')) { + $this->markTestSkipped('CURL must be installed to test the client'); + } + + } + + function testConstruct() { + + $client = new ClientMock([ + 'baseUri' => '/', + ]); + $this->assertInstanceOf('Sabre\DAV\ClientMock', $client); + + } + + /** + * @expectedException InvalidArgumentException + */ + function testConstructNoBaseUri() { + + $client = new ClientMock([]); + + } + + function testAuth() { + + $client = new ClientMock([ + 'baseUri' => '/', + 'userName' => 'foo', + 'password' => 'bar', + ]); + + $this->assertEquals("foo:bar", $client->curlSettings[CURLOPT_USERPWD]); + $this->assertEquals(CURLAUTH_BASIC | CURLAUTH_DIGEST, $client->curlSettings[CURLOPT_HTTPAUTH]); + + } + + function testBasicAuth() { + + $client = new ClientMock([ + 'baseUri' => '/', + 'userName' => 'foo', + 'password' => 'bar', + 'authType' => Client::AUTH_BASIC + ]); + + $this->assertEquals("foo:bar", $client->curlSettings[CURLOPT_USERPWD]); + $this->assertEquals(CURLAUTH_BASIC, $client->curlSettings[CURLOPT_HTTPAUTH]); + + } + + function testDigestAuth() { + + $client = new ClientMock([ + 'baseUri' => '/', + 'userName' => 'foo', + 'password' => 'bar', + 'authType' => Client::AUTH_DIGEST + ]); + + $this->assertEquals("foo:bar", $client->curlSettings[CURLOPT_USERPWD]); + $this->assertEquals(CURLAUTH_DIGEST, $client->curlSettings[CURLOPT_HTTPAUTH]); + + } + + function testNTLMAuth() { + + $client = new ClientMock([ + 'baseUri' => '/', + 'userName' => 'foo', + 'password' => 'bar', + 'authType' => Client::AUTH_NTLM + ]); + + $this->assertEquals("foo:bar", $client->curlSettings[CURLOPT_USERPWD]); + $this->assertEquals(CURLAUTH_NTLM, $client->curlSettings[CURLOPT_HTTPAUTH]); + + } + + function testProxy() { + + $client = new ClientMock([ + 'baseUri' => '/', + 'proxy' => 'localhost:8888', + ]); + + $this->assertEquals("localhost:8888", $client->curlSettings[CURLOPT_PROXY]); + + } + + function testEncoding() { + + $client = new ClientMock([ + 'baseUri' => '/', + 'encoding' => Client::ENCODING_IDENTITY | Client::ENCODING_GZIP | Client::ENCODING_DEFLATE, + ]); + + $this->assertEquals("identity,deflate,gzip", $client->curlSettings[CURLOPT_ENCODING]); + + } + + function testPropFind() { + + $client = new ClientMock([ + 'baseUri' => '/', + ]); + + $responseBody = <<<XML +<?xml version="1.0"?> +<multistatus xmlns="DAV:"> + <response> + <href>/foo</href> + <propstat> + <prop> + <displayname>bar</displayname> + </prop> + <status>HTTP/1.1 200 OK</status> + </propstat> + </response> +</multistatus> +XML; + + $client->response = new Response(207, [], $responseBody); + $result = $client->propFind('foo', ['{DAV:}displayname', '{urn:zim}gir']); + + $this->assertEquals(['{DAV:}displayname' => 'bar'], $result); + + $request = $client->request; + $this->assertEquals('PROPFIND', $request->getMethod()); + $this->assertEquals('/foo', $request->getUrl()); + $this->assertEquals([ + 'Depth' => ['0'], + 'Content-Type' => ['application/xml'], + ], $request->getHeaders()); + + } + + /** + * @expectedException \Sabre\HTTP\ClientHttpException + */ + function testPropFindError() { + + $client = new ClientMock([ + 'baseUri' => '/', + ]); + + $client->response = new Response(405, []); + $client->propFind('foo', ['{DAV:}displayname', '{urn:zim}gir']); + + } + + function testPropFindDepth1() { + + $client = new ClientMock([ + 'baseUri' => '/', + ]); + + $responseBody = <<<XML +<?xml version="1.0"?> +<multistatus xmlns="DAV:"> + <response> + <href>/foo</href> + <propstat> + <prop> + <displayname>bar</displayname> + </prop> + <status>HTTP/1.1 200 OK</status> + </propstat> + </response> +</multistatus> +XML; + + $client->response = new Response(207, [], $responseBody); + $result = $client->propFind('foo', ['{DAV:}displayname', '{urn:zim}gir'], 1); + + $this->assertEquals([ + '/foo' => [ + '{DAV:}displayname' => 'bar' + ], + ], $result); + + $request = $client->request; + $this->assertEquals('PROPFIND', $request->getMethod()); + $this->assertEquals('/foo', $request->getUrl()); + $this->assertEquals([ + 'Depth' => ['1'], + 'Content-Type' => ['application/xml'], + ], $request->getHeaders()); + + } + + function testPropPatch() { + + $client = new ClientMock([ + 'baseUri' => '/', + ]); + + $responseBody = <<<XML +<?xml version="1.0"?> +<multistatus xmlns="DAV:"> + <response> + <href>/foo</href> + <propstat> + <prop> + <displayname>bar</displayname> + </prop> + <status>HTTP/1.1 200 OK</status> + </propstat> + </response> +</multistatus> +XML; + + $client->response = new Response(207, [], $responseBody); + $result = $client->propPatch('foo', ['{DAV:}displayname' => 'hi', '{urn:zim}gir' => null]); + $this->assertTrue($result); + $request = $client->request; + $this->assertEquals('PROPPATCH', $request->getMethod()); + $this->assertEquals('/foo', $request->getUrl()); + $this->assertEquals([ + 'Content-Type' => ['application/xml'], + ], $request->getHeaders()); + + } + + /** + * @depends testPropPatch + * @expectedException \Sabre\HTTP\ClientHttpException + */ + function testPropPatchHTTPError() { + + $client = new ClientMock([ + 'baseUri' => '/', + ]); + + $client->response = new Response(403, [], ''); + $client->propPatch('foo', ['{DAV:}displayname' => 'hi', '{urn:zim}gir' => null]); + + } + + /** + * @depends testPropPatch + * @expectedException Sabre\HTTP\ClientException + */ + function testPropPatchMultiStatusError() { + + $client = new ClientMock([ + 'baseUri' => '/', + ]); + + $responseBody = <<<XML +<?xml version="1.0"?> +<multistatus xmlns="DAV:"> +<response> + <href>/foo</href> + <propstat> + <prop> + <displayname /> + </prop> + <status>HTTP/1.1 403 Forbidden</status> + </propstat> +</response> +</multistatus> +XML; + + $client->response = new Response(207, [], $responseBody); + $client->propPatch('foo', ['{DAV:}displayname' => 'hi', '{urn:zim}gir' => null]); + + } + + function testOPTIONS() { + + $client = new ClientMock([ + 'baseUri' => '/', + ]); + + $client->response = new Response(207, [ + 'DAV' => 'calendar-access, extended-mkcol', + ]); + $result = $client->options(); + + $this->assertEquals( + ['calendar-access', 'extended-mkcol'], + $result + ); + + $request = $client->request; + $this->assertEquals('OPTIONS', $request->getMethod()); + $this->assertEquals('/', $request->getUrl()); + $this->assertEquals([ + ], $request->getHeaders()); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/CorePluginTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/CorePluginTest.php new file mode 100644 index 00000000000..5c6f07ae80a --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/CorePluginTest.php @@ -0,0 +1,14 @@ +<?php + +namespace Sabre\DAV; + +class CorePluginTest extends \PHPUnit_Framework_TestCase { + + function testGetInfo() { + + $corePlugin = new CorePlugin(); + $this->assertEquals('core', $corePlugin->getPluginInfo()['name']); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/DbTestHelperTrait.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/DbTestHelperTrait.php new file mode 100644 index 00000000000..63e35fb88ee --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/DbTestHelperTrait.php @@ -0,0 +1,143 @@ +<?php + +namespace Sabre\DAV; + +use PDO; +use PDOException; + +class DbCache { + + static $cache = []; + +} + +trait DbTestHelperTrait { + + /** + * Should be "mysql", "pgsql", "sqlite". + */ + public $driver = null; + + /** + * Returns a fully configured PDO object. + * + * @return PDO + */ + function getDb() { + + if (!$this->driver) { + throw new \Exception('You must set the $driver public property'); + } + + if (array_key_exists($this->driver, DbCache::$cache)) { + $pdo = DbCache::$cache[$this->driver]; + if ($pdo === null) { + $this->markTestSkipped($this->driver . ' was not enabled, not correctly configured or of the wrong version'); + } + return $pdo; + } + + try { + + switch ($this->driver) { + + case 'mysql' : + $pdo = new PDO(SABRE_MYSQLDSN, SABRE_MYSQLUSER, SABRE_MYSQLPASS); + break; + case 'sqlite' : + $pdo = new \PDO('sqlite:' . SABRE_TEMPDIR . '/testdb'); + break; + case 'pgsql' : + $pdo = new \PDO(SABRE_PGSQLDSN); + $version = $pdo->query('SELECT VERSION()')->fetchColumn(); + preg_match('|([0-9\.]){5,}|', $version, $matches); + $version = $matches[0]; + if (version_compare($version, '9.5.0', '<')) { + DbCache::$cache[$this->driver] = null; + $this->markTestSkipped('We require at least Postgres 9.5. This server is running ' . $version); + } + break; + + + + } + $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + + } catch (PDOException $e) { + + $this->markTestSkipped($this->driver . ' was not enabled or not correctly configured. Error message: ' . $e->getMessage()); + + } + + DbCache::$cache[$this->driver] = $pdo; + return $pdo; + + } + + /** + * Alias for getDb + * + * @return PDO + */ + function getPDO() { + + return $this->getDb(); + + } + + /** + * Uses .sql files from the examples directory to initialize the database. + * + * @param string $schemaName + * @return void + */ + function createSchema($schemaName) { + + $db = $this->getDb(); + + $queries = file_get_contents( + __DIR__ . '/../../../examples/sql/' . $this->driver . '.' . $schemaName . '.sql' + ); + + foreach (explode(';', $queries) as $query) { + + if (trim($query) === '') { + continue; + } + + $db->exec($query); + + } + + } + + /** + * Drops tables, if they exist + * + * @param string|string[] $tableNames + * @return void + */ + function dropTables($tableNames) { + + $tableNames = (array)$tableNames; + $db = $this->getDb(); + foreach ($tableNames as $tableName) { + $db->exec('DROP TABLE IF EXISTS ' . $tableName); + } + + + } + + function tearDown() { + + switch ($this->driver) { + + case 'sqlite' : + // Recreating sqlite, just in case + unset(DbCache::$cache[$this->driver]); + unlink(SABRE_TEMPDIR . '/testdb'); + } + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Exception/LockedTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Exception/LockedTest.php new file mode 100644 index 00000000000..174a561b537 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Exception/LockedTest.php @@ -0,0 +1,67 @@ +<?php + +namespace Sabre\DAV\Exception; + +use DOMDocument; +use Sabre\DAV; + +class LockedTest extends \PHPUnit_Framework_TestCase { + + function testSerialize() { + + $dom = new DOMDocument('1.0'); + $dom->formatOutput = true; + $root = $dom->createElement('d:root'); + + $dom->appendChild($root); + $root->setAttribute('xmlns:d', 'DAV:'); + + $lockInfo = new DAV\Locks\LockInfo(); + $lockInfo->uri = '/foo'; + $locked = new Locked($lockInfo); + + $locked->serialize(new DAV\Server(), $root); + + $output = $dom->saveXML(); + + $expected = '<?xml version="1.0"?> +<d:root xmlns:d="DAV:"> + <d:lock-token-submitted xmlns:d="DAV:"> + <d:href>/foo</d:href> + </d:lock-token-submitted> +</d:root> +'; + + $this->assertEquals($expected, $output); + + } + + function testSerializeAmpersand() { + + $dom = new DOMDocument('1.0'); + $dom->formatOutput = true; + $root = $dom->createElement('d:root'); + + $dom->appendChild($root); + $root->setAttribute('xmlns:d', 'DAV:'); + + $lockInfo = new DAV\Locks\LockInfo(); + $lockInfo->uri = '/foo&bar'; + $locked = new Locked($lockInfo); + + $locked->serialize(new DAV\Server(), $root); + + $output = $dom->saveXML(); + + $expected = '<?xml version="1.0"?> +<d:root xmlns:d="DAV:"> + <d:lock-token-submitted xmlns:d="DAV:"> + <d:href>/foo&bar</d:href> + </d:lock-token-submitted> +</d:root> +'; + + $this->assertEquals($expected, $output); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Exception/PaymentRequiredTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Exception/PaymentRequiredTest.php new file mode 100644 index 00000000000..7142937b4c0 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Exception/PaymentRequiredTest.php @@ -0,0 +1,14 @@ +<?php + +namespace Sabre\DAV\Exception; + +class PaymentRequiredTest extends \PHPUnit_Framework_TestCase { + + function testGetHTTPCode() { + + $ex = new PaymentRequired(); + $this->assertEquals(402, $ex->getHTTPCode()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Exception/ServiceUnavailableTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Exception/ServiceUnavailableTest.php new file mode 100644 index 00000000000..1cc691e6d38 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Exception/ServiceUnavailableTest.php @@ -0,0 +1,14 @@ +<?php + +namespace Sabre\DAV\Exception; + +class ServiceUnavailableTest extends \PHPUnit_Framework_TestCase { + + function testGetHTTPCode() { + + $ex = new ServiceUnavailable(); + $this->assertEquals(503, $ex->getHTTPCode()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Exception/TooManyMatchesTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Exception/TooManyMatchesTest.php new file mode 100644 index 00000000000..0f58e8726be --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Exception/TooManyMatchesTest.php @@ -0,0 +1,35 @@ +<?php + +namespace Sabre\DAV\Exception; + +use DOMDocument; +use Sabre\DAV; + +class TooManyMatchesTest extends \PHPUnit_Framework_TestCase { + + function testSerialize() { + + $dom = new DOMDocument('1.0'); + $dom->formatOutput = true; + $root = $dom->createElement('d:root'); + + $dom->appendChild($root); + $root->setAttribute('xmlns:d', 'DAV:'); + + $locked = new TooManyMatches(); + + $locked->serialize(new DAV\Server(), $root); + + $output = $dom->saveXML(); + + $expected = '<?xml version="1.0"?> +<d:root xmlns:d="DAV:"> + <d:number-of-matches-within-limits xmlns:d="DAV:"/> +</d:root> +'; + + $this->assertEquals($expected, $output); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ExceptionTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ExceptionTest.php new file mode 100644 index 00000000000..0eb4f3dd878 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ExceptionTest.php @@ -0,0 +1,30 @@ +<?php + +namespace Sabre\DAV; + +class ExceptionTest extends \PHPUnit_Framework_TestCase { + + function testStatus() { + + $e = new Exception(); + $this->assertEquals(500, $e->getHTTPCode()); + + } + + function testExceptionStatuses() { + + $c = [ + 'Sabre\\DAV\\Exception\\NotAuthenticated' => 401, + 'Sabre\\DAV\\Exception\\InsufficientStorage' => 507, + ]; + + foreach ($c as $class => $status) { + + $obj = new $class(); + $this->assertEquals($status, $obj->getHTTPCode()); + + } + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/FSExt/DirectoryTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/FSExt/DirectoryTest.php new file mode 100644 index 00000000000..097ebd26b92 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/FSExt/DirectoryTest.php @@ -0,0 +1,30 @@ +<?php + +namespace Sabre\DAV\FSExt; + +class DirectoryTest extends \PHPUnit_Framework_TestCase { + + function create() { + + return new Directory(SABRE_TEMPDIR); + + } + + function testCreate() { + + $dir = $this->create(); + $this->assertEquals(basename(SABRE_TEMPDIR), $dir->getName()); + + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + */ + function testChildExistDot() { + + $dir = $this->create(); + $dir->childExists('..'); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/FSExt/FileTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/FSExt/FileTest.php new file mode 100644 index 00000000000..f5d65a44f96 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/FSExt/FileTest.php @@ -0,0 +1,110 @@ +<?php + +namespace Sabre\DAV\FSExt; + +require_once 'Sabre/TestUtil.php'; + +class FileTest extends \PHPUnit_Framework_TestCase { + + function setUp() { + + file_put_contents(SABRE_TEMPDIR . '/file.txt', 'Contents'); + + } + + function tearDown() { + + \Sabre\TestUtil::clearTempDir(); + + } + + function testPut() { + + $filename = SABRE_TEMPDIR . '/file.txt'; + $file = new File($filename); + $result = $file->put('New contents'); + + $this->assertEquals('New contents', file_get_contents(SABRE_TEMPDIR . '/file.txt')); + $this->assertEquals( + '"' . + sha1( + fileinode($filename) . + filesize($filename) . + filemtime($filename) + ) . '"', + $result + ); + + } + + function testRange() { + + $file = new File(SABRE_TEMPDIR . '/file.txt'); + $file->put('0000000'); + $file->patch('111', 2, 3); + + $this->assertEquals('0001110', file_get_contents(SABRE_TEMPDIR . '/file.txt')); + + } + + function testRangeStream() { + + $stream = fopen('php://memory', 'r+'); + fwrite($stream, "222"); + rewind($stream); + + $file = new File(SABRE_TEMPDIR . '/file.txt'); + $file->put('0000000'); + $file->patch($stream, 2, 3); + + $this->assertEquals('0002220', file_get_contents(SABRE_TEMPDIR . '/file.txt')); + + } + + + function testGet() { + + $file = new File(SABRE_TEMPDIR . '/file.txt'); + $this->assertEquals('Contents', stream_get_contents($file->get())); + + } + + function testDelete() { + + $file = new File(SABRE_TEMPDIR . '/file.txt'); + $file->delete(); + + $this->assertFalse(file_exists(SABRE_TEMPDIR . '/file.txt')); + + } + + function testGetETag() { + + $filename = SABRE_TEMPDIR . '/file.txt'; + $file = new File($filename); + $this->assertEquals( + '"' . + sha1( + fileinode($filename) . + filesize($filename) . + filemtime($filename) + ) . '"', + $file->getETag() + ); + } + + function testGetContentType() { + + $file = new File(SABRE_TEMPDIR . '/file.txt'); + $this->assertNull($file->getContentType()); + + } + + function testGetSize() { + + $file = new File(SABRE_TEMPDIR . '/file.txt'); + $this->assertEquals(8, $file->getSize()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/FSExt/ServerTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/FSExt/ServerTest.php new file mode 100644 index 00000000000..20fca490a35 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/FSExt/ServerTest.php @@ -0,0 +1,246 @@ +<?php + +namespace Sabre\DAV\FSExt; + +use Sabre\DAV; +use Sabre\HTTP; + +require_once 'Sabre/DAV/AbstractServer.php'; + +class ServerTest extends DAV\AbstractServer{ + + protected function getRootNode() { + + return new Directory($this->tempDir); + + } + + function testGet() { + + $request = new HTTP\Request('GET', '/test.txt'); + $filename = $this->tempDir . '/test.txt'; + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(200, $this->response->getStatus(), 'Invalid status code received.'); + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Type' => ['application/octet-stream'], + 'Content-Length' => [13], + 'Last-Modified' => [HTTP\Util::toHTTPDate(new \DateTime('@' . filemtime($filename)))], + 'ETag' => ['"' . sha1(fileinode($filename) . filesize($filename) . filemtime($filename)) . '"'], + ], + $this->response->getHeaders() + ); + + + $this->assertEquals('Test contents', stream_get_contents($this->response->body)); + + } + + function testHEAD() { + + $request = new HTTP\Request('HEAD', '/test.txt'); + $filename = $this->tempDir . '/test.txt'; + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Type' => ['application/octet-stream'], + 'Content-Length' => [13], + 'Last-Modified' => [HTTP\Util::toHTTPDate(new \DateTime('@' . filemtime($this->tempDir . '/test.txt')))], + 'ETag' => ['"' . sha1(fileinode($filename) . filesize($filename) . filemtime($filename)) . '"'], + ], + $this->response->getHeaders() + ); + + $this->assertEquals(200, $this->response->status); + $this->assertEquals('', $this->response->body); + + } + + function testPut() { + + $request = new HTTP\Request('PUT', '/testput.txt'); + $filename = $this->tempDir . '/testput.txt'; + $request->setBody('Testing new file'); + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Length' => ['0'], + 'ETag' => ['"' . sha1(fileinode($filename) . filesize($filename) . filemtime($filename)) . '"'], + ], $this->response->getHeaders()); + + $this->assertEquals(201, $this->response->status); + $this->assertEquals('', $this->response->body); + $this->assertEquals('Testing new file', file_get_contents($filename)); + + } + + function testPutAlreadyExists() { + + $request = new HTTP\Request('PUT', '/test.txt', ['If-None-Match' => '*']); + $request->setBody('Testing new file'); + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + ], $this->response->getHeaders()); + + $this->assertEquals(412, $this->response->status); + $this->assertNotEquals('Testing new file', file_get_contents($this->tempDir . '/test.txt')); + + } + + function testMkcol() { + + $request = new HTTP\Request('MKCOL', '/testcol'); + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Length' => ['0'], + ], $this->response->getHeaders()); + + $this->assertEquals(201, $this->response->status); + $this->assertEquals('', $this->response->body); + $this->assertTrue(is_dir($this->tempDir . '/testcol')); + + } + + function testPutUpdate() { + + $request = new HTTP\Request('PUT', '/test.txt'); + $request->setBody('Testing updated file'); + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals('0', $this->response->getHeader('Content-Length')); + + $this->assertEquals(204, $this->response->status); + $this->assertEquals('', $this->response->body); + $this->assertEquals('Testing updated file', file_get_contents($this->tempDir . '/test.txt')); + + } + + function testDelete() { + + $request = new HTTP\Request('DELETE', '/test.txt'); + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Length' => ['0'], + ], $this->response->getHeaders()); + + $this->assertEquals(204, $this->response->status); + $this->assertEquals('', $this->response->body); + $this->assertFalse(file_exists($this->tempDir . '/test.txt')); + + } + + function testDeleteDirectory() { + + mkdir($this->tempDir . '/testcol'); + file_put_contents($this->tempDir . '/testcol/test.txt', 'Hi! I\'m a file with a short lifespan'); + + $request = new HTTP\Request('DELETE', '/testcol'); + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Length' => ['0'], + ], $this->response->getHeaders()); + $this->assertEquals(204, $this->response->status); + $this->assertEquals('', $this->response->body); + $this->assertFalse(file_exists($this->tempDir . '/testcol')); + + } + + function testOptions() { + + $request = new HTTP\Request('OPTIONS', '/'); + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals([ + 'DAV' => ['1, 3, extended-mkcol'], + 'MS-Author-Via' => ['DAV'], + 'Allow' => ['OPTIONS, GET, HEAD, DELETE, PROPFIND, PUT, PROPPATCH, COPY, MOVE, REPORT'], + 'Accept-Ranges' => ['bytes'], + 'Content-Length' => ['0'], + 'X-Sabre-Version' => [DAV\Version::VERSION], + ], $this->response->getHeaders()); + + $this->assertEquals(200, $this->response->status); + $this->assertEquals('', $this->response->body); + + } + + function testMove() { + + mkdir($this->tempDir . '/testcol'); + + $request = new HTTP\Request('MOVE', '/test.txt', ['Destination' => '/testcol/test2.txt']); + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals(201, $this->response->status); + $this->assertEquals('', $this->response->body); + + $this->assertEquals([ + 'Content-Length' => ['0'], + 'X-Sabre-Version' => [DAV\Version::VERSION], + ], $this->response->getHeaders()); + + $this->assertTrue( + is_file($this->tempDir . '/testcol/test2.txt') + ); + + + } + + /** + * This test checks if it's possible to move a non-FSExt collection into a + * FSExt collection. + * + * The moveInto function *should* ignore the object and let sabredav itself + * execute the slow move. + */ + function testMoveOtherObject() { + + mkdir($this->tempDir . '/tree1'); + mkdir($this->tempDir . '/tree2'); + + $tree = new DAV\Tree(new DAV\SimpleCollection('root', [ + new DAV\FS\Directory($this->tempDir . '/tree1'), + new DAV\FSExt\Directory($this->tempDir . '/tree2'), + ])); + $this->server->tree = $tree; + + $request = new HTTP\Request('MOVE', '/tree1', ['Destination' => '/tree2/tree1']); + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals(201, $this->response->status); + $this->assertEquals('', $this->response->body); + + $this->assertEquals([ + 'Content-Length' => ['0'], + 'X-Sabre-Version' => [DAV\Version::VERSION], + ], $this->response->getHeaders()); + + $this->assertTrue( + is_dir($this->tempDir . '/tree2/tree1') + ); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/GetIfConditionsTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/GetIfConditionsTest.php new file mode 100644 index 00000000000..d41abc00a2b --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/GetIfConditionsTest.php @@ -0,0 +1,337 @@ +<?php + +namespace Sabre\DAV; + +use Sabre\HTTP; + +require_once 'Sabre/HTTP/ResponseMock.php'; +require_once 'Sabre/DAV/AbstractServer.php'; + +class GetIfConditionsTest extends AbstractServer { + + function testNoConditions() { + + $request = new HTTP\Request(); + + $conditions = $this->server->getIfConditions($request); + $this->assertEquals([], $conditions); + + } + + function testLockToken() { + + $request = new HTTP\Request('GET', '/path/', ['If' => '(<opaquelocktoken:token1>)']); + $conditions = $this->server->getIfConditions($request); + + $compare = [ + + [ + 'uri' => 'path', + 'tokens' => [ + [ + 'negate' => false, + 'token' => 'opaquelocktoken:token1', + 'etag' => '', + ], + ], + + ], + + ]; + + $this->assertEquals($compare, $conditions); + + } + + function testNotLockToken() { + + $serverVars = [ + 'HTTP_IF' => '(Not <opaquelocktoken:token1>)', + 'REQUEST_URI' => '/bla' + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $conditions = $this->server->getIfConditions($request); + + $compare = [ + + [ + 'uri' => 'bla', + 'tokens' => [ + [ + 'negate' => true, + 'token' => 'opaquelocktoken:token1', + 'etag' => '', + ], + ], + + ], + + ]; + $this->assertEquals($compare, $conditions); + + } + + function testLockTokenUrl() { + + $serverVars = [ + 'HTTP_IF' => '<http://www.example.com/> (<opaquelocktoken:token1>)', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $conditions = $this->server->getIfConditions($request); + + $compare = [ + + [ + 'uri' => '', + 'tokens' => [ + [ + 'negate' => false, + 'token' => 'opaquelocktoken:token1', + 'etag' => '', + ], + ], + + ], + + ]; + $this->assertEquals($compare, $conditions); + + } + + function test2LockTokens() { + + $serverVars = [ + 'HTTP_IF' => '(<opaquelocktoken:token1>) (Not <opaquelocktoken:token2>)', + 'REQUEST_URI' => '/bla', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $conditions = $this->server->getIfConditions($request); + + $compare = [ + + [ + 'uri' => 'bla', + 'tokens' => [ + [ + 'negate' => false, + 'token' => 'opaquelocktoken:token1', + 'etag' => '', + ], + [ + 'negate' => true, + 'token' => 'opaquelocktoken:token2', + 'etag' => '', + ], + ], + + ], + + ]; + $this->assertEquals($compare, $conditions); + + } + + function test2UriLockTokens() { + + $serverVars = [ + 'HTTP_IF' => '<http://www.example.org/node1> (<opaquelocktoken:token1>) <http://www.example.org/node2> (Not <opaquelocktoken:token2>)', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $conditions = $this->server->getIfConditions($request); + + $compare = [ + + [ + 'uri' => 'node1', + 'tokens' => [ + [ + 'negate' => false, + 'token' => 'opaquelocktoken:token1', + 'etag' => '', + ], + ], + ], + [ + 'uri' => 'node2', + 'tokens' => [ + [ + 'negate' => true, + 'token' => 'opaquelocktoken:token2', + 'etag' => '', + ], + ], + + ], + + ]; + $this->assertEquals($compare, $conditions); + + } + + function test2UriMultiLockTokens() { + + $serverVars = [ + 'HTTP_IF' => '<http://www.example.org/node1> (<opaquelocktoken:token1>) (<opaquelocktoken:token2>) <http://www.example.org/node2> (Not <opaquelocktoken:token3>)', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $conditions = $this->server->getIfConditions($request); + + $compare = [ + + [ + 'uri' => 'node1', + 'tokens' => [ + [ + 'negate' => false, + 'token' => 'opaquelocktoken:token1', + 'etag' => '', + ], + [ + 'negate' => false, + 'token' => 'opaquelocktoken:token2', + 'etag' => '', + ], + ], + ], + [ + 'uri' => 'node2', + 'tokens' => [ + [ + 'negate' => true, + 'token' => 'opaquelocktoken:token3', + 'etag' => '', + ], + ], + + ], + + ]; + $this->assertEquals($compare, $conditions); + + } + + function testEtag() { + + $serverVars = [ + 'HTTP_IF' => '(["etag1"])', + 'REQUEST_URI' => '/foo', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $conditions = $this->server->getIfConditions($request); + + $compare = [ + + [ + 'uri' => 'foo', + 'tokens' => [ + [ + 'negate' => false, + 'token' => '', + 'etag' => '"etag1"', + ], + ], + ], + + ]; + $this->assertEquals($compare, $conditions); + + } + + function test2Etags() { + + $serverVars = [ + 'HTTP_IF' => '<http://www.example.org/> (["etag1"]) (["etag2"])', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $conditions = $this->server->getIfConditions($request); + + $compare = [ + + [ + 'uri' => '', + 'tokens' => [ + [ + 'negate' => false, + 'token' => '', + 'etag' => '"etag1"', + ], + [ + 'negate' => false, + 'token' => '', + 'etag' => '"etag2"', + ], + ], + ], + + ]; + $this->assertEquals($compare, $conditions); + + } + + function testComplexIf() { + + $serverVars = [ + 'HTTP_IF' => '<http://www.example.org/node1> (<opaquelocktoken:token1> ["etag1"]) ' . + '(Not <opaquelocktoken:token2>) (["etag2"]) <http://www.example.org/node2> ' . + '(<opaquelocktoken:token3>) (Not <opaquelocktoken:token4>) (["etag3"])', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $conditions = $this->server->getIfConditions($request); + + $compare = [ + + [ + 'uri' => 'node1', + 'tokens' => [ + [ + 'negate' => false, + 'token' => 'opaquelocktoken:token1', + 'etag' => '"etag1"', + ], + [ + 'negate' => true, + 'token' => 'opaquelocktoken:token2', + 'etag' => '', + ], + [ + 'negate' => false, + 'token' => '', + 'etag' => '"etag2"', + ], + ], + ], + [ + 'uri' => 'node2', + 'tokens' => [ + [ + 'negate' => false, + 'token' => 'opaquelocktoken:token3', + 'etag' => '', + ], + [ + 'negate' => true, + 'token' => 'opaquelocktoken:token4', + 'etag' => '', + ], + [ + 'negate' => false, + 'token' => '', + 'etag' => '"etag3"', + ], + ], + ], + + ]; + $this->assertEquals($compare, $conditions); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/HTTPPreferParsingTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/HTTPPreferParsingTest.php new file mode 100644 index 00000000000..cd8bee9686d --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/HTTPPreferParsingTest.php @@ -0,0 +1,188 @@ +<?php + +namespace Sabre\DAV; + +use Sabre\HTTP; + +class HTTPPreferParsingTest extends \Sabre\DAVServerTest { + + function testParseSimple() { + + $httpRequest = HTTP\Sapi::createFromServerArray([ + 'HTTP_PREFER' => 'return-asynch', + ]); + + $server = new Server(); + $server->httpRequest = $httpRequest; + + $this->assertEquals([ + 'respond-async' => true, + 'return' => null, + 'handling' => null, + 'wait' => null, + ], $server->getHTTPPrefer()); + + } + + function testParseValue() { + + $httpRequest = HTTP\Sapi::createFromServerArray([ + 'HTTP_PREFER' => 'wait=10', + ]); + + $server = new Server(); + $server->httpRequest = $httpRequest; + + $this->assertEquals([ + 'respond-async' => false, + 'return' => null, + 'handling' => null, + 'wait' => '10', + ], $server->getHTTPPrefer()); + + } + + function testParseMultiple() { + + $httpRequest = HTTP\Sapi::createFromServerArray([ + 'HTTP_PREFER' => 'return-minimal, strict,lenient', + ]); + + $server = new Server(); + $server->httpRequest = $httpRequest; + + $this->assertEquals([ + 'respond-async' => false, + 'return' => 'minimal', + 'handling' => 'lenient', + 'wait' => null, + ], $server->getHTTPPrefer()); + + } + + function testParseWeirdValue() { + + $httpRequest = HTTP\Sapi::createFromServerArray([ + 'HTTP_PREFER' => 'BOOOH', + ]); + + $server = new Server(); + $server->httpRequest = $httpRequest; + + $this->assertEquals([ + 'respond-async' => false, + 'return' => null, + 'handling' => null, + 'wait' => null, + 'boooh' => true, + ], $server->getHTTPPrefer()); + + } + + function testBrief() { + + $httpRequest = HTTP\Sapi::createFromServerArray([ + 'HTTP_BRIEF' => 't', + ]); + + $server = new Server(); + $server->httpRequest = $httpRequest; + + $this->assertEquals([ + 'respond-async' => false, + 'return' => 'minimal', + 'handling' => null, + 'wait' => null, + ], $server->getHTTPPrefer()); + + } + + /** + * propfindMinimal + * + * @return void + */ + function testpropfindMinimal() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'PROPFIND', + 'REQUEST_URI' => '/', + 'HTTP_PREFER' => 'return-minimal', + ]); + $request->setBody(<<<BLA +<?xml version="1.0"?> +<d:propfind xmlns:d="DAV:"> + <d:prop> + <d:something /> + <d:resourcetype /> + </d:prop> +</d:propfind> +BLA + ); + + $response = $this->request($request); + + $body = $response->getBodyAsString(); + + $this->assertEquals(207, $response->getStatus(), $body); + + $this->assertTrue(strpos($body, 'resourcetype') !== false, $body); + $this->assertTrue(strpos($body, 'something') === false, $body); + + } + + function testproppatchMinimal() { + + $request = new HTTP\Request('PROPPATCH', '/', ['Prefer' => 'return-minimal']); + $request->setBody(<<<BLA +<?xml version="1.0"?> +<d:propertyupdate xmlns:d="DAV:"> + <d:set> + <d:prop> + <d:something>nope!</d:something> + </d:prop> + </d:set> +</d:propertyupdate> +BLA + ); + + $this->server->on('propPatch', function($path, PropPatch $propPatch) { + + $propPatch->handle('{DAV:}something', function($props) { + return true; + }); + + }); + + $response = $this->request($request); + + $this->assertEquals(0, strlen($response->body), 'Expected empty body: ' . $response->body); + $this->assertEquals(204, $response->status); + + } + + function testproppatchMinimalError() { + + $request = new HTTP\Request('PROPPATCH', '/', ['Prefer' => 'return-minimal']); + $request->setBody(<<<BLA +<?xml version="1.0"?> +<d:propertyupdate xmlns:d="DAV:"> + <d:set> + <d:prop> + <d:something>nope!</d:something> + </d:prop> + </d:set> +</d:propertyupdate> +BLA + ); + + $response = $this->request($request); + + $body = $response->getBodyAsString(); + + $this->assertEquals(207, $response->status); + $this->assertTrue(strpos($body, 'something') !== false); + $this->assertTrue(strpos($body, '403 Forbidden') !== false, $body); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/HttpCopyTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/HttpCopyTest.php new file mode 100644 index 00000000000..b5e64369ef9 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/HttpCopyTest.php @@ -0,0 +1,199 @@ +<?php + +namespace Sabre\DAV; + +use Sabre\DAVServerTest; +use Sabre\HTTP; + +/** + * Tests related to the COPY request. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class HttpCopyTest extends DAVServerTest { + + /** + * Sets up the DAV tree. + * + * @return void + */ + function setUpTree() { + + $this->tree = new Mock\Collection('root', [ + 'file1' => 'content1', + 'file2' => 'content2', + 'coll1' => [ + 'file3' => 'content3', + 'file4' => 'content4', + ] + ]); + + } + + function testCopyFile() { + + $request = new HTTP\Request('COPY', '/file1', [ + 'Destination' => '/file5' + ]); + $response = $this->request($request); + $this->assertEquals(201, $response->getStatus()); + $this->assertEquals('content1', $this->tree->getChild('file5')->get()); + + } + + function testCopyFileToSelf() { + + $request = new HTTP\Request('COPY', '/file1', [ + 'Destination' => '/file1' + ]); + $response = $this->request($request); + $this->assertEquals(403, $response->getStatus()); + + } + + function testCopyFileToExisting() { + + $request = new HTTP\Request('COPY', '/file1', [ + 'Destination' => '/file2' + ]); + $response = $this->request($request); + $this->assertEquals(204, $response->getStatus()); + $this->assertEquals('content1', $this->tree->getChild('file2')->get()); + + } + + function testCopyFileToExistingOverwriteT() { + + $request = new HTTP\Request('COPY', '/file1', [ + 'Destination' => '/file2', + 'Overwrite' => 'T', + ]); + $response = $this->request($request); + $this->assertEquals(204, $response->getStatus()); + $this->assertEquals('content1', $this->tree->getChild('file2')->get()); + + } + + function testCopyFileToExistingOverwriteBadValue() { + + $request = new HTTP\Request('COPY', '/file1', [ + 'Destination' => '/file2', + 'Overwrite' => 'B', + ]); + $response = $this->request($request); + $this->assertEquals(400, $response->getStatus()); + + } + + function testCopyFileNonExistantParent() { + + $request = new HTTP\Request('COPY', '/file1', [ + 'Destination' => '/notfound/file2', + ]); + $response = $this->request($request); + $this->assertEquals(409, $response->getStatus()); + + } + + function testCopyFileToExistingOverwriteF() { + + $request = new HTTP\Request('COPY', '/file1', [ + 'Destination' => '/file2', + 'Overwrite' => 'F', + ]); + $response = $this->request($request); + $this->assertEquals(412, $response->getStatus()); + $this->assertEquals('content2', $this->tree->getChild('file2')->get()); + + } + + function testCopyFileToExistinBlockedCreateDestination() { + + $this->server->on('beforeBind', function($path) { + + if ($path === 'file2') { + return false; + } + + }); + $request = new HTTP\Request('COPY', '/file1', [ + 'Destination' => '/file2', + 'Overwrite' => 'T', + ]); + $response = $this->request($request); + + // This checks if the destination file is intact. + $this->assertEquals('content2', $this->tree->getChild('file2')->get()); + + } + + function testCopyColl() { + + $request = new HTTP\Request('COPY', '/coll1', [ + 'Destination' => '/coll2' + ]); + $response = $this->request($request); + $this->assertEquals(201, $response->getStatus()); + $this->assertEquals('content3', $this->tree->getChild('coll2')->getChild('file3')->get()); + + } + + function testCopyCollToSelf() { + + $request = new HTTP\Request('COPY', '/coll1', [ + 'Destination' => '/coll1' + ]); + $response = $this->request($request); + $this->assertEquals(403, $response->getStatus()); + + } + + function testCopyCollToExisting() { + + $request = new HTTP\Request('COPY', '/coll1', [ + 'Destination' => '/file2' + ]); + $response = $this->request($request); + $this->assertEquals(204, $response->getStatus()); + $this->assertEquals('content3', $this->tree->getChild('file2')->getChild('file3')->get()); + + } + + function testCopyCollToExistingOverwriteT() { + + $request = new HTTP\Request('COPY', '/coll1', [ + 'Destination' => '/file2', + 'Overwrite' => 'T', + ]); + $response = $this->request($request); + $this->assertEquals(204, $response->getStatus()); + $this->assertEquals('content3', $this->tree->getChild('file2')->getChild('file3')->get()); + + } + + function testCopyCollToExistingOverwriteF() { + + $request = new HTTP\Request('COPY', '/coll1', [ + 'Destination' => '/file2', + 'Overwrite' => 'F', + ]); + $response = $this->request($request); + $this->assertEquals(412, $response->getStatus()); + $this->assertEquals('content2', $this->tree->getChild('file2')->get()); + + } + + function testCopyCollIntoSubtree() { + + $request = new HTTP\Request('COPY', '/coll1', [ + 'Destination' => '/coll1/subcol', + ]); + $response = $this->request($request); + $this->assertEquals(409, $response->getStatus()); + + } + + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/HttpDeleteTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/HttpDeleteTest.php new file mode 100644 index 00000000000..bd1b3315057 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/HttpDeleteTest.php @@ -0,0 +1,137 @@ +<?php + +namespace Sabre\DAV; + +use Sabre\DAVServerTest; +use Sabre\HTTP; + +/** + * Tests related to the PUT request. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class HttpDeleteTest extends DAVServerTest { + + /** + * Sets up the DAV tree. + * + * @return void + */ + function setUpTree() { + + $this->tree = new Mock\Collection('root', [ + 'file1' => 'foo', + 'dir' => [ + 'subfile' => 'bar', + 'subfile2' => 'baz', + ], + ]); + + } + + /** + * A successful DELETE + */ + function testDelete() { + + $request = new HTTP\Request('DELETE', '/file1'); + + $response = $this->request($request); + + $this->assertEquals( + 204, + $response->getStatus(), + "Incorrect status code. Response body: " . $response->getBodyAsString() + ); + + $this->assertEquals( + [ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Length' => ['0'], + ], + $response->getHeaders() + ); + + } + + /** + * Deleting a Directory + */ + function testDeleteDirectory() { + + $request = new HTTP\Request('DELETE', '/dir'); + + $response = $this->request($request); + + $this->assertEquals( + 204, + $response->getStatus(), + "Incorrect status code. Response body: " . $response->getBodyAsString() + ); + + $this->assertEquals( + [ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Length' => ['0'], + ], + $response->getHeaders() + ); + + } + + /** + * DELETE on a node that does not exist + */ + function testDeleteNotFound() { + + $request = new HTTP\Request('DELETE', '/file2'); + $response = $this->request($request); + + $this->assertEquals( + 404, + $response->getStatus(), + "Incorrect status code. Response body: " . $response->getBodyAsString() + ); + + } + + /** + * DELETE with preconditions + */ + function testDeletePreconditions() { + + $request = new HTTP\Request('DELETE', '/file1', [ + 'If-Match' => '"' . md5('foo') . '"', + ]); + + $response = $this->request($request); + + $this->assertEquals( + 204, + $response->getStatus(), + "Incorrect status code. Response body: " . $response->getBodyAsString() + ); + + } + + /** + * DELETE with incorrect preconditions + */ + function testDeletePreconditionsFailed() { + + $request = new HTTP\Request('DELETE', '/file1', [ + 'If-Match' => '"' . md5('bar') . '"', + ]); + + $response = $this->request($request); + + $this->assertEquals( + 412, + $response->getStatus(), + "Incorrect status code. Response body: " . $response->getBodyAsString() + ); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/HttpGetTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/HttpGetTest.php new file mode 100644 index 00000000000..1eefba70657 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/HttpGetTest.php @@ -0,0 +1,158 @@ +<?php + +namespace Sabre\DAV; + +use Sabre\DAVServerTest; +use Sabre\HTTP; + +/** + * Tests related to the GET request. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class HttpGetTest extends DAVServerTest { + + /** + * Sets up the DAV tree. + * + * @return void + */ + function setUpTree() { + + $this->tree = new Mock\Collection('root', [ + 'file1' => 'foo', + new Mock\Collection('dir', []), + new Mock\StreamingFile('streaming', 'stream') + ]); + + } + + function testGet() { + + $request = new HTTP\Request('GET', '/file1'); + $response = $this->request($request); + + $this->assertEquals(200, $response->getStatus()); + + // Removing Last-Modified because it keeps changing. + $response->removeHeader('Last-Modified'); + + $this->assertEquals( + [ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/octet-stream'], + 'Content-Length' => [3], + 'ETag' => ['"' . md5('foo') . '"'], + ], + $response->getHeaders() + ); + + $this->assertEquals('foo', $response->getBodyAsString()); + + } + + function testGetHttp10() { + + $request = new HTTP\Request('GET', '/file1'); + $request->setHttpVersion('1.0'); + $response = $this->request($request); + + $this->assertEquals(200, $response->getStatus()); + + // Removing Last-Modified because it keeps changing. + $response->removeHeader('Last-Modified'); + + $this->assertEquals( + [ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/octet-stream'], + 'Content-Length' => [3], + 'ETag' => ['"' . md5('foo') . '"'], + ], + $response->getHeaders() + ); + + $this->assertEquals('1.0', $response->getHttpVersion()); + + $this->assertEquals('foo', $response->getBodyAsString()); + + } + + function testGet404() { + + $request = new HTTP\Request('GET', '/notfound'); + $response = $this->request($request); + + $this->assertEquals(404, $response->getStatus()); + + } + + function testGet404_aswell() { + + $request = new HTTP\Request('GET', '/file1/subfile'); + $response = $this->request($request); + + $this->assertEquals(404, $response->getStatus()); + + } + + /** + * We automatically normalize double slashes. + */ + function testGetDoubleSlash() { + + $request = new HTTP\Request('GET', '//file1'); + $response = $this->request($request); + + $this->assertEquals(200, $response->getStatus()); + + // Removing Last-Modified because it keeps changing. + $response->removeHeader('Last-Modified'); + + $this->assertEquals( + [ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/octet-stream'], + 'Content-Length' => [3], + 'ETag' => ['"' . md5('foo') . '"'], + ], + $response->getHeaders() + ); + + $this->assertEquals('foo', $response->getBodyAsString()); + + } + + function testGetCollection() { + + $request = new HTTP\Request('GET', '/dir'); + $response = $this->request($request); + + $this->assertEquals(501, $response->getStatus()); + + } + + function testGetStreaming() { + + $request = new HTTP\Request('GET', '/streaming'); + $response = $this->request($request); + + $this->assertEquals(200, $response->getStatus()); + + // Removing Last-Modified because it keeps changing. + $response->removeHeader('Last-Modified'); + + $this->assertEquals( + [ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/octet-stream'], + ], + $response->getHeaders() + ); + + $this->assertEquals('stream', $response->getBodyAsString()); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/HttpHeadTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/HttpHeadTest.php new file mode 100644 index 00000000000..2cd1c5ea3c1 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/HttpHeadTest.php @@ -0,0 +1,97 @@ +<?php + +namespace Sabre\DAV; + +use Sabre\DAVServerTest; +use Sabre\HTTP; + +/** + * Tests related to the HEAD request. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class HttpHeadTest extends DAVServerTest { + + /** + * Sets up the DAV tree. + * + * @return void + */ + function setUpTree() { + + $this->tree = new Mock\Collection('root', [ + 'file1' => 'foo', + new Mock\Collection('dir', []), + new Mock\StreamingFile('streaming', 'stream') + ]); + + } + + function testHEAD() { + + $request = new HTTP\Request('HEAD', '//file1'); + $response = $this->request($request); + + $this->assertEquals(200, $response->getStatus()); + + // Removing Last-Modified because it keeps changing. + $response->removeHeader('Last-Modified'); + + $this->assertEquals( + [ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/octet-stream'], + 'Content-Length' => [3], + 'ETag' => ['"' . md5('foo') . '"'], + ], + $response->getHeaders() + ); + + $this->assertEquals('', $response->getBodyAsString()); + + } + + /** + * According to the specs, HEAD should behave identical to GET. But, broken + * clients needs HEAD requests on collections to respond with a 200, so + * that's what we do. + */ + function testHEADCollection() { + + $request = new HTTP\Request('HEAD', '/dir'); + $response = $this->request($request); + + $this->assertEquals(200, $response->getStatus()); + + } + + /** + * HEAD automatically internally maps to GET via a sub-request. + * The Auth plugin must not be triggered twice for these, so we'll + * test for that. + */ + function testDoubleAuth() { + + $count = 0; + + $authBackend = new Auth\Backend\BasicCallBack(function($userName, $password) use (&$count) { + $count++; + return true; + }); + $this->server->addPlugin( + new Auth\Plugin( + $authBackend + ) + ); + $request = new HTTP\Request('HEAD', '/file1', ['Authorization' => 'Basic ' . base64_encode('user:pass')]); + $response = $this->request($request); + + $this->assertEquals(200, $response->getStatus()); + + $this->assertEquals(1, $count, 'Auth was triggered twice :('); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/HttpMoveTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/HttpMoveTest.php new file mode 100644 index 00000000000..52f7c674ee9 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/HttpMoveTest.php @@ -0,0 +1,119 @@ +<?php + +namespace Sabre\DAV; + +use Sabre\DAVServerTest; +use Sabre\HTTP; + +/** + * Tests related to the MOVE request. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class HttpMoveTest extends DAVServerTest { + + /** + * Sets up the DAV tree. + * + * @return void + */ + function setUpTree() { + + $this->tree = new Mock\Collection('root', [ + 'file1' => 'content1', + 'file2' => 'content2', + ]); + + } + + function testMoveToSelf() { + + $request = new HTTP\Request('MOVE', '/file1', [ + 'Destination' => '/file1' + ]); + $response = $this->request($request); + $this->assertEquals(403, $response->getStatus()); + $this->assertEquals('content1', $this->tree->getChild('file1')->get()); + + } + + function testMove() { + + $request = new HTTP\Request('MOVE', '/file1', [ + 'Destination' => '/file3' + ]); + $response = $this->request($request); + $this->assertEquals(201, $response->getStatus(), print_r($response, true)); + $this->assertEquals('content1', $this->tree->getChild('file3')->get()); + $this->assertFalse($this->tree->childExists('file1')); + + } + + function testMoveToExisting() { + + $request = new HTTP\Request('MOVE', '/file1', [ + 'Destination' => '/file2' + ]); + $response = $this->request($request); + $this->assertEquals(204, $response->getStatus(), print_r($response, true)); + $this->assertEquals('content1', $this->tree->getChild('file2')->get()); + $this->assertFalse($this->tree->childExists('file1')); + + } + + function testMoveToExistingOverwriteT() { + + $request = new HTTP\Request('MOVE', '/file1', [ + 'Destination' => '/file2', + 'Overwrite' => 'T', + ]); + $response = $this->request($request); + $this->assertEquals(204, $response->getStatus(), print_r($response, true)); + $this->assertEquals('content1', $this->tree->getChild('file2')->get()); + $this->assertFalse($this->tree->childExists('file1')); + + } + + function testMoveToExistingOverwriteF() { + + $request = new HTTP\Request('MOVE', '/file1', [ + 'Destination' => '/file2', + 'Overwrite' => 'F', + ]); + $response = $this->request($request); + $this->assertEquals(412, $response->getStatus(), print_r($response, true)); + $this->assertEquals('content1', $this->tree->getChild('file1')->get()); + $this->assertEquals('content2', $this->tree->getChild('file2')->get()); + $this->assertTrue($this->tree->childExists('file1')); + $this->assertTrue($this->tree->childExists('file2')); + + } + + /** + * If we MOVE to an existing file, but a plugin prevents the original from + * being deleted, we need to make sure that the server does not delete + * the destination. + */ + function testMoveToExistingBlockedDeleteSource() { + + $this->server->on('beforeUnbind', function($path) { + + if ($path === 'file1') { + throw new \Sabre\DAV\Exception\Forbidden('uh oh'); + } + + }); + $request = new HTTP\Request('MOVE', '/file1', [ + 'Destination' => '/file2' + ]); + $response = $this->request($request); + $this->assertEquals(403, $response->getStatus(), print_r($response, true)); + $this->assertEquals('content1', $this->tree->getChild('file1')->get()); + $this->assertEquals('content2', $this->tree->getChild('file2')->get()); + $this->assertTrue($this->tree->childExists('file1')); + $this->assertTrue($this->tree->childExists('file2')); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/HttpPutTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/HttpPutTest.php new file mode 100644 index 00000000000..86480b1c22a --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/HttpPutTest.php @@ -0,0 +1,349 @@ +<?php + +namespace Sabre\DAV; + +use Sabre\DAVServerTest; +use Sabre\HTTP; + +/** + * Tests related to the PUT request. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class HttpPutTest extends DAVServerTest { + + /** + * Sets up the DAV tree. + * + * @return void + */ + function setUpTree() { + + $this->tree = new Mock\Collection('root', [ + 'file1' => 'foo', + ]); + + } + + /** + * A successful PUT of a new file. + */ + function testPut() { + + $request = new HTTP\Request('PUT', '/file2', [], 'hello'); + + $response = $this->request($request); + + $this->assertEquals(201, $response->getStatus(), 'Incorrect status code received. Full response body:' . $response->getBodyAsString()); + + $this->assertEquals( + 'hello', + $this->server->tree->getNodeForPath('file2')->get() + ); + + $this->assertEquals( + [ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Length' => ['0'], + 'ETag' => ['"' . md5('hello') . '"'] + ], + $response->getHeaders() + ); + + } + + /** + * A successful PUT on an existing file. + * + * @depends testPut + */ + function testPutExisting() { + + $request = new HTTP\Request('PUT', '/file1', [], 'bar'); + + $response = $this->request($request); + + $this->assertEquals(204, $response->getStatus()); + + $this->assertEquals( + 'bar', + $this->server->tree->getNodeForPath('file1')->get() + ); + + $this->assertEquals( + [ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Length' => ['0'], + 'ETag' => ['"' . md5('bar') . '"'] + ], + $response->getHeaders() + ); + + } + + /** + * PUT on existing file with If-Match: * + * + * @depends testPutExisting + */ + function testPutExistingIfMatchStar() { + + $request = new HTTP\Request( + 'PUT', + '/file1', + ['If-Match' => '*'], + 'hello' + ); + + $response = $this->request($request); + + $this->assertEquals(204, $response->getStatus()); + + $this->assertEquals( + 'hello', + $this->server->tree->getNodeForPath('file1')->get() + ); + + $this->assertEquals( + [ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Length' => ['0'], + 'ETag' => ['"' . md5('hello') . '"'] + ], + $response->getHeaders() + ); + + } + + /** + * PUT on existing file with If-Match: with a correct etag + * + * @depends testPutExisting + */ + function testPutExistingIfMatchCorrect() { + + $request = new HTTP\Request( + 'PUT', + '/file1', + ['If-Match' => '"' . md5('foo') . '"'], + 'hello' + ); + + $response = $this->request($request); + + $this->assertEquals(204, $response->status); + + $this->assertEquals( + 'hello', + $this->server->tree->getNodeForPath('file1')->get() + ); + + $this->assertEquals( + [ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Length' => ['0'], + 'ETag' => ['"' . md5('hello') . '"'], + ], + $response->getHeaders() + ); + + } + + /** + * PUT with Content-Range should be rejected. + * + * @depends testPut + */ + function testPutContentRange() { + + $request = new HTTP\Request( + 'PUT', + '/file2', + ['Content-Range' => 'bytes/100-200'], + 'hello' + ); + + $response = $this->request($request); + $this->assertEquals(400, $response->getStatus()); + + } + + /** + * PUT on non-existing file with If-None-Match: * should work. + * + * @depends testPut + */ + function testPutIfNoneMatchStar() { + + $request = new HTTP\Request( + 'PUT', + '/file2', + ['If-None-Match' => '*'], + 'hello' + ); + + $response = $this->request($request); + + $this->assertEquals(201, $response->getStatus()); + + $this->assertEquals( + 'hello', + $this->server->tree->getNodeForPath('file2')->get() + ); + + $this->assertEquals( + [ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Length' => ['0'], + 'ETag' => ['"' . md5('hello') . '"'] + ], + $response->getHeaders() + ); + + } + + /** + * PUT on non-existing file with If-Match: * should fail. + * + * @depends testPut + */ + function testPutIfMatchStar() { + + $request = new HTTP\Request( + 'PUT', + '/file2', + ['If-Match' => '*'], + 'hello' + ); + + $response = $this->request($request); + + $this->assertEquals(412, $response->getStatus()); + + } + + /** + * PUT on existing file with If-None-Match: * should fail. + * + * @depends testPut + */ + function testPutExistingIfNoneMatchStar() { + + $request = new HTTP\Request( + 'PUT', + '/file1', + ['If-None-Match' => '*'], + 'hello' + ); + $request->setBody('hello'); + + $response = $this->request($request); + + $this->assertEquals(412, $response->getStatus()); + + } + + /** + * PUT thats created in a non-collection should be rejected. + * + * @depends testPut + */ + function testPutNoParent() { + + $request = new HTTP\Request( + 'PUT', + '/file1/file2', + [], + 'hello' + ); + + $response = $this->request($request); + $this->assertEquals(409, $response->getStatus()); + + } + + /** + * Finder may sometimes make a request, which gets its content-body + * stripped. We can't always prevent this from happening, but in some cases + * we can detected this and return an error instead. + * + * @depends testPut + */ + function testFinderPutSuccess() { + + $request = new HTTP\Request( + 'PUT', + '/file2', + ['X-Expected-Entity-Length' => '5'], + 'hello' + ); + $response = $this->request($request); + + $this->assertEquals(201, $response->getStatus()); + + $this->assertEquals( + 'hello', + $this->server->tree->getNodeForPath('file2')->get() + ); + + $this->assertEquals( + [ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Length' => ['0'], + 'ETag' => ['"' . md5('hello') . '"'], + ], + $response->getHeaders() + ); + + } + + /** + * Same as the last one, but in this case we're mimicing a failed request. + * + * @depends testFinderPutSuccess + */ + function testFinderPutFail() { + + $request = new HTTP\Request( + 'PUT', + '/file2', + ['X-Expected-Entity-Length' => '5'], + '' + ); + + $response = $this->request($request); + + $this->assertEquals(403, $response->getStatus()); + + } + + /** + * Plugins can intercept PUT. We need to make sure that works. + * + * @depends testPut + */ + function testPutIntercept() { + + $this->server->on('beforeBind', function($uri) { + $this->server->httpResponse->setStatus(418); + return false; + }); + + $request = new HTTP\Request('PUT', '/file2', [], 'hello'); + $response = $this->request($request); + + $this->assertEquals(418, $response->getStatus(), 'Incorrect status code received. Full response body: ' . $response->getBodyAsString()); + + $this->assertFalse( + $this->server->tree->nodeExists('file2') + ); + + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + ], $response->getHeaders()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Issue33Test.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Issue33Test.php new file mode 100644 index 00000000000..ba2cf3dc152 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Issue33Test.php @@ -0,0 +1,106 @@ +<?php + +namespace Sabre\DAV; + +use Sabre\HTTP; + +require_once 'Sabre/TestUtil.php'; + +class Issue33Test extends \PHPUnit_Framework_TestCase { + + function setUp() { + + \Sabre\TestUtil::clearTempDir(); + + } + + function testCopyMoveInfo() { + + $bar = new SimpleCollection('bar'); + $root = new SimpleCollection('webdav', [$bar]); + + $server = new Server($root); + $server->setBaseUri('/webdav/'); + + $serverVars = [ + 'REQUEST_URI' => '/webdav/bar', + 'HTTP_DESTINATION' => 'http://dev2.tribalos.com/webdav/%C3%A0fo%C3%B3', + 'HTTP_OVERWRITE' => 'F', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + + $server->httpRequest = $request; + + $info = $server->getCopyAndMoveInfo($request); + + $this->assertEquals('%C3%A0fo%C3%B3', urlencode($info['destination'])); + $this->assertFalse($info['destinationExists']); + $this->assertFalse($info['destinationNode']); + + } + + function testTreeMove() { + + mkdir(SABRE_TEMPDIR . '/issue33'); + $dir = new FS\Directory(SABRE_TEMPDIR . '/issue33'); + + $dir->createDirectory('bar'); + + $tree = new Tree($dir); + $tree->move('bar', urldecode('%C3%A0fo%C3%B3')); + + $node = $tree->getNodeForPath(urldecode('%C3%A0fo%C3%B3')); + $this->assertEquals(urldecode('%C3%A0fo%C3%B3'), $node->getName()); + + } + + function testDirName() { + + $dirname1 = 'bar'; + $dirname2 = urlencode('%C3%A0fo%C3%B3'); + + $this->assertTrue(dirname($dirname1) == dirname($dirname2)); + + } + + /** + * @depends testTreeMove + * @depends testCopyMoveInfo + */ + function testEverything() { + + // Request object + $serverVars = [ + 'REQUEST_METHOD' => 'MOVE', + 'REQUEST_URI' => '/webdav/bar', + 'HTTP_DESTINATION' => 'http://dev2.tribalos.com/webdav/%C3%A0fo%C3%B3', + 'HTTP_OVERWRITE' => 'F', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody(''); + + $response = new HTTP\ResponseMock(); + + // Server setup + mkdir(SABRE_TEMPDIR . '/issue33'); + $dir = new FS\Directory(SABRE_TEMPDIR . '/issue33'); + + $dir->createDirectory('bar'); + + $tree = new Tree($dir); + + $server = new Server($tree); + $server->setBaseUri('/webdav/'); + + $server->httpRequest = $request; + $server->httpResponse = $response; + $server->sapi = new HTTP\SapiMock(); + $server->exec(); + + $this->assertTrue(file_exists(SABRE_TEMPDIR . '/issue33/' . urldecode('%C3%A0fo%C3%B3'))); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Backend/AbstractTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Backend/AbstractTest.php new file mode 100644 index 00000000000..bbde69097a5 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Backend/AbstractTest.php @@ -0,0 +1,196 @@ +<?php + +namespace Sabre\DAV\Locks\Backend; + +use Sabre\DAV; + +abstract class AbstractTest extends \PHPUnit_Framework_TestCase { + + /** + * @abstract + * @return AbstractBackend + */ + abstract function getBackend(); + + function testSetup() { + + $backend = $this->getBackend(); + $this->assertInstanceOf('Sabre\\DAV\\Locks\\Backend\\AbstractBackend', $backend); + + } + + /** + * @depends testSetup + */ + function testGetLocks() { + + $backend = $this->getBackend(); + + $lock = new DAV\Locks\LockInfo(); + $lock->owner = 'Sinterklaas'; + $lock->timeout = 60; + $lock->created = time(); + $lock->token = 'MY-UNIQUE-TOKEN'; + $lock->uri = 'someuri'; + + $this->assertTrue($backend->lock('someuri', $lock)); + + $locks = $backend->getLocks('someuri', false); + + $this->assertEquals(1, count($locks)); + $this->assertEquals('Sinterklaas', $locks[0]->owner); + $this->assertEquals('someuri', $locks[0]->uri); + + } + + /** + * @depends testGetLocks + */ + function testGetLocksParent() { + + $backend = $this->getBackend(); + + $lock = new DAV\Locks\LockInfo(); + $lock->owner = 'Sinterklaas'; + $lock->timeout = 60; + $lock->created = time(); + $lock->depth = DAV\Server::DEPTH_INFINITY; + $lock->token = 'MY-UNIQUE-TOKEN'; + + $this->assertTrue($backend->lock('someuri', $lock)); + + $locks = $backend->getLocks('someuri/child', false); + + $this->assertEquals(1, count($locks)); + $this->assertEquals('Sinterklaas', $locks[0]->owner); + $this->assertEquals('someuri', $locks[0]->uri); + + } + + + /** + * @depends testGetLocks + */ + function testGetLocksParentDepth0() { + + $backend = $this->getBackend(); + + $lock = new DAV\Locks\LockInfo(); + $lock->owner = 'Sinterklaas'; + $lock->timeout = 60; + $lock->created = time(); + $lock->depth = 0; + $lock->token = 'MY-UNIQUE-TOKEN'; + + $this->assertTrue($backend->lock('someuri', $lock)); + + $locks = $backend->getLocks('someuri/child', false); + + $this->assertEquals(0, count($locks)); + + } + + function testGetLocksChildren() { + + $backend = $this->getBackend(); + + $lock = new DAV\Locks\LockInfo(); + $lock->owner = 'Sinterklaas'; + $lock->timeout = 60; + $lock->created = time(); + $lock->depth = 0; + $lock->token = 'MY-UNIQUE-TOKEN'; + + $this->assertTrue($backend->lock('someuri/child', $lock)); + + $locks = $backend->getLocks('someuri/child', false); + $this->assertEquals(1, count($locks)); + + $locks = $backend->getLocks('someuri', false); + $this->assertEquals(0, count($locks)); + + $locks = $backend->getLocks('someuri', true); + $this->assertEquals(1, count($locks)); + + } + + /** + * @depends testGetLocks + */ + function testLockRefresh() { + + $backend = $this->getBackend(); + + $lock = new DAV\Locks\LockInfo(); + $lock->owner = 'Sinterklaas'; + $lock->timeout = 60; + $lock->created = time(); + $lock->token = 'MY-UNIQUE-TOKEN'; + + $this->assertTrue($backend->lock('someuri', $lock)); + /* Second time */ + + $lock->owner = 'Santa Clause'; + $this->assertTrue($backend->lock('someuri', $lock)); + + $locks = $backend->getLocks('someuri', false); + + $this->assertEquals(1, count($locks)); + + $this->assertEquals('Santa Clause', $locks[0]->owner); + $this->assertEquals('someuri', $locks[0]->uri); + + } + + /** + * @depends testGetLocks + */ + function testUnlock() { + + $backend = $this->getBackend(); + + $lock = new DAV\Locks\LockInfo(); + $lock->owner = 'Sinterklaas'; + $lock->timeout = 60; + $lock->created = time(); + $lock->token = 'MY-UNIQUE-TOKEN'; + + $this->assertTrue($backend->lock('someuri', $lock)); + + $locks = $backend->getLocks('someuri', false); + $this->assertEquals(1, count($locks)); + + $this->assertTrue($backend->unlock('someuri', $lock)); + + $locks = $backend->getLocks('someuri', false); + $this->assertEquals(0, count($locks)); + + } + + /** + * @depends testUnlock + */ + function testUnlockUnknownToken() { + + $backend = $this->getBackend(); + + $lock = new DAV\Locks\LockInfo(); + $lock->owner = 'Sinterklaas'; + $lock->timeout = 60; + $lock->created = time(); + $lock->token = 'MY-UNIQUE-TOKEN'; + + $this->assertTrue($backend->lock('someuri', $lock)); + + $locks = $backend->getLocks('someuri', false); + $this->assertEquals(1, count($locks)); + + $lock->token = 'SOME-OTHER-TOKEN'; + $this->assertFalse($backend->unlock('someuri', $lock)); + + $locks = $backend->getLocks('someuri', false); + $this->assertEquals(1, count($locks)); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Backend/FileTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Backend/FileTest.php new file mode 100644 index 00000000000..537996f3bbf --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Backend/FileTest.php @@ -0,0 +1,24 @@ +<?php + +namespace Sabre\DAV\Locks\Backend; + +require_once 'Sabre/TestUtil.php'; + +class FileTest extends AbstractTest { + + function getBackend() { + + \Sabre\TestUtil::clearTempDir(); + $backend = new File(SABRE_TEMPDIR . '/lockdb'); + return $backend; + + } + + + function tearDown() { + + \Sabre\TestUtil::clearTempDir(); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Backend/Mock.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Backend/Mock.php new file mode 100644 index 00000000000..dd475807178 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Backend/Mock.php @@ -0,0 +1,139 @@ +<?php + +namespace Sabre\DAV\Locks\Backend; + +use Sabre\DAV\Locks\LockInfo; + +/** + * Locks Mock backend. + * + * This backend stores lock information in memory. Mainly useful for testing. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Mock extends AbstractBackend { + + /** + * 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 + */ + function getLocks($uri, $returnChildLocks) { + + $newLocks = []; + + $locks = $this->getData(); + + foreach ($locks as $lock) { + + if ($lock->uri === $uri || + //deep locks on parents + ($lock->depth != 0 && strpos($uri, $lock->uri . '/') === 0) || + + // locks on children + ($returnChildLocks && (strpos($lock->uri, $uri . '/') === 0))) { + + $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 + * @param LockInfo $lockInfo + * @return bool + */ + 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 + * @param LockInfo $lockInfo + * @return bool + */ + 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; + + } + + protected $data = []; + + /** + * Loads the lockdata from the filesystem. + * + * @return array + */ + protected function getData() { + + return $this->data; + + } + + /** + * Saves the lockdata + * + * @param array $newData + * @return void + */ + protected function putData(array $newData) { + + $this->data = $newData; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOMySQLTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOMySQLTest.php new file mode 100644 index 00000000000..0ba02fc8b53 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOMySQLTest.php @@ -0,0 +1,9 @@ +<?php + +namespace Sabre\DAV\Locks\Backend; + +class PDOMySQLTest extends PDOTest { + + public $driver = 'mysql'; + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOPgSqlTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOPgSqlTest.php new file mode 100644 index 00000000000..39ee56419a2 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOPgSqlTest.php @@ -0,0 +1,9 @@ +<?php + +namespace Sabre\DAV\Locks\Backend; + +class PDOPgSqlTest extends PDOTest { + + public $driver = 'pgsql'; + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOSqliteTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOSqliteTest.php new file mode 100644 index 00000000000..4b126dcf359 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOSqliteTest.php @@ -0,0 +1,9 @@ +<?php + +namespace Sabre\DAV\Locks\Backend; + +class PDOSqliteTest extends PDOTest { + + public $driver = 'sqlite'; + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOTest.php new file mode 100644 index 00000000000..a27eae93cef --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Backend/PDOTest.php @@ -0,0 +1,20 @@ +<?php + +namespace Sabre\DAV\Locks\Backend; + +abstract class PDOTest extends AbstractTest { + + use \Sabre\DAV\DbTestHelperTrait; + + function getBackend() { + + $this->dropTables('locks'); + $this->createSchema('locks'); + + $pdo = $this->getPDO(); + + return new PDO($pdo); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/MSWordTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/MSWordTest.php new file mode 100644 index 00000000000..1111db5b5c2 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/MSWordTest.php @@ -0,0 +1,124 @@ +<?php + +namespace Sabre\DAV\Locks; + +use Sabre\DAV; +use Sabre\HTTP; + +require_once 'Sabre/HTTP/ResponseMock.php'; +require_once 'Sabre/TestUtil.php'; + +class MSWordTest extends \PHPUnit_Framework_TestCase { + + function tearDown() { + + \Sabre\TestUtil::clearTempDir(); + + } + + function testLockEtc() { + + mkdir(SABRE_TEMPDIR . '/mstest'); + $tree = new DAV\FS\Directory(SABRE_TEMPDIR . '/mstest'); + + $server = new DAV\Server($tree); + $server->debugExceptions = true; + $locksBackend = new Backend\File(SABRE_TEMPDIR . '/locksdb'); + $locksPlugin = new Plugin($locksBackend); + $server->addPlugin($locksPlugin); + + $response1 = new HTTP\ResponseMock(); + + $server->httpRequest = $this->getLockRequest(); + $server->httpResponse = $response1; + $server->sapi = new HTTP\SapiMock(); + $server->exec(); + + $this->assertEquals(201, $server->httpResponse->getStatus(), 'Full response body:' . $response1->getBodyAsString()); + $this->assertTrue(!!$server->httpResponse->getHeaders('Lock-Token')); + $lockToken = $server->httpResponse->getHeader('Lock-Token'); + + //sleep(10); + + $response2 = new HTTP\ResponseMock(); + + $server->httpRequest = $this->getLockRequest2(); + $server->httpResponse = $response2; + $server->exec(); + + $this->assertEquals(201, $server->httpResponse->status); + $this->assertTrue(!!$server->httpResponse->getHeaders('Lock-Token')); + + //sleep(10); + + $response3 = new HTTP\ResponseMock(); + $server->httpRequest = $this->getPutRequest($lockToken); + $server->httpResponse = $response3; + $server->exec(); + + $this->assertEquals(204, $server->httpResponse->status); + + } + + function getLockRequest() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'LOCK', + 'HTTP_CONTENT_TYPE' => 'application/xml', + 'HTTP_TIMEOUT' => 'Second-3600', + 'REQUEST_URI' => '/Nouveau%20Microsoft%20Office%20Excel%20Worksheet.xlsx', + ]); + + $request->setBody('<D:lockinfo xmlns:D="DAV:"> + <D:lockscope> + <D:exclusive /> + </D:lockscope> + <D:locktype> + <D:write /> + </D:locktype> + <D:owner> + <D:href>PC-Vista\User</D:href> + </D:owner> +</D:lockinfo>'); + + return $request; + + } + function getLockRequest2() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'LOCK', + 'HTTP_CONTENT_TYPE' => 'application/xml', + 'HTTP_TIMEOUT' => 'Second-3600', + 'REQUEST_URI' => '/~$Nouveau%20Microsoft%20Office%20Excel%20Worksheet.xlsx', + ]); + + $request->setBody('<D:lockinfo xmlns:D="DAV:"> + <D:lockscope> + <D:exclusive /> + </D:lockscope> + <D:locktype> + <D:write /> + </D:locktype> + <D:owner> + <D:href>PC-Vista\User</D:href> + </D:owner> +</D:lockinfo>'); + + return $request; + + } + + function getPutRequest($lockToken) { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'PUT', + 'REQUEST_URI' => '/Nouveau%20Microsoft%20Office%20Excel%20Worksheet.xlsx', + 'HTTP_IF' => 'If: (' . $lockToken . ')', + ]); + $request->setBody('FAKE BODY'); + return $request; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Plugin2Test.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Plugin2Test.php new file mode 100644 index 00000000000..7af49079574 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/Plugin2Test.php @@ -0,0 +1,69 @@ +<?php + +namespace Sabre\DAV\Locks; + +use Sabre\HTTP\Request; + +class Plugin2Test extends \Sabre\DAVServerTest { + + public $setupLocks = true; + + function setUpTree() { + + $this->tree = new \Sabre\DAV\FS\Directory(SABRE_TEMPDIR); + + } + + function tearDown() { + + \Sabre\TestUtil::clearTempDir(); + + } + + /** + * This test first creates a file with LOCK and then deletes it. + * + * After deleting the file, the lock should no longer be in the lock + * backend. + * + * Reported in ticket #487 + */ + function testUnlockAfterDelete() { + + $body = '<?xml version="1.0"?> +<D:lockinfo xmlns:D="DAV:"> + <D:lockscope><D:exclusive/></D:lockscope> + <D:locktype><D:write/></D:locktype> +</D:lockinfo>'; + + $request = new Request( + 'LOCK', + '/file.txt', + [], + $body + ); + $response = $this->request($request); + $this->assertEquals(201, $response->getStatus(), $response->getBodyAsString()); + + $this->assertEquals( + 1, + count($this->locksBackend->getLocks('file.txt', true)) + ); + + $request = new Request( + 'DELETE', + '/file.txt', + [ + 'If' => '(' . $response->getHeader('Lock-Token') . ')', + ] + ); + $response = $this->request($request); + $this->assertEquals(204, $response->getStatus(), $response->getBodyAsString()); + + $this->assertEquals( + 0, + count($this->locksBackend->getLocks('file.txt', true)) + ); + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/PluginTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/PluginTest.php new file mode 100644 index 00000000000..dbbf6757aad --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Locks/PluginTest.php @@ -0,0 +1,1003 @@ +<?php + +namespace Sabre\DAV\Locks; + +use Sabre\DAV; +use Sabre\HTTP; + +require_once 'Sabre/DAV/AbstractServer.php'; + +class PluginTest extends DAV\AbstractServer { + + /** + * @var Plugin + */ + protected $locksPlugin; + + function setUp() { + + parent::setUp(); + $locksBackend = new Backend\File(SABRE_TEMPDIR . '/locksdb'); + $locksPlugin = new Plugin($locksBackend); + $this->server->addPlugin($locksPlugin); + $this->locksPlugin = $locksPlugin; + + } + + function testGetInfo() { + + $this->assertArrayHasKey( + 'name', + $this->locksPlugin->getPluginInfo() + ); + + } + + function testGetFeatures() { + + $this->assertEquals([2], $this->locksPlugin->getFeatures()); + + } + + function testGetHTTPMethods() { + + $this->assertEquals(['LOCK', 'UNLOCK'], $this->locksPlugin->getHTTPMethods('')); + + } + + function testLockNoBody() { + + $request = new HTTP\Request('LOCK', '/test.txt'); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + ], + $this->response->getHeaders() + ); + + $this->assertEquals(400, $this->response->status); + + } + + function testLock() { + + $request = new HTTP\Request('LOCK', '/test.txt'); + $request->setBody('<?xml version="1.0"?> +<D:lockinfo xmlns:D="DAV:"> + <D:lockscope><D:exclusive/></D:lockscope> + <D:locktype><D:write/></D:locktype> + <D:owner> + <D:href>http://example.org/~ejw/contact.html</D:href> + </D:owner> +</D:lockinfo>'); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + $this->assertTrue(preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')) === 1, 'We did not get a valid Locktoken back (' . $this->response->getHeader('Lock-Token') . ')'); + + $this->assertEquals(200, $this->response->status, 'Got an incorrect status back. Response body: ' . $this->response->body); + + $body = preg_replace("/xmlns(:[A-Za-z0-9_])?=(\"|\')DAV:(\"|\')/", "xmlns\\1=\"urn:DAV\"", $this->response->body); + $xml = simplexml_load_string($body); + $xml->registerXPathNamespace('d', 'urn:DAV'); + + $elements = [ + '/d:prop', + '/d:prop/d:lockdiscovery', + '/d:prop/d:lockdiscovery/d:activelock', + '/d:prop/d:lockdiscovery/d:activelock/d:locktype', + '/d:prop/d:lockdiscovery/d:activelock/d:lockroot', + '/d:prop/d:lockdiscovery/d:activelock/d:lockroot/d:href', + '/d:prop/d:lockdiscovery/d:activelock/d:locktype/d:write', + '/d:prop/d:lockdiscovery/d:activelock/d:lockscope', + '/d:prop/d:lockdiscovery/d:activelock/d:lockscope/d:exclusive', + '/d:prop/d:lockdiscovery/d:activelock/d:depth', + '/d:prop/d:lockdiscovery/d:activelock/d:owner', + '/d:prop/d:lockdiscovery/d:activelock/d:timeout', + '/d:prop/d:lockdiscovery/d:activelock/d:locktoken', + '/d:prop/d:lockdiscovery/d:activelock/d:locktoken/d:href', + ]; + + foreach ($elements as $elem) { + $data = $xml->xpath($elem); + $this->assertEquals(1, count($data), 'We expected 1 match for the xpath expression "' . $elem . '". ' . count($data) . ' were found. Full response body: ' . $this->response->body); + } + + $depth = $xml->xpath('/d:prop/d:lockdiscovery/d:activelock/d:depth'); + $this->assertEquals('infinity', (string)$depth[0]); + + $token = $xml->xpath('/d:prop/d:lockdiscovery/d:activelock/d:locktoken/d:href'); + $this->assertEquals($this->response->getHeader('Lock-Token'), '<' . (string)$token[0] . '>', 'Token in response body didn\'t match token in response header.'); + + } + + /** + * @depends testLock + */ + function testDoubleLock() { + + $request = new HTTP\Request('LOCK', '/test.txt'); + $request->setBody('<?xml version="1.0"?> +<D:lockinfo xmlns:D="DAV:"> + <D:lockscope><D:exclusive/></D:lockscope> + <D:locktype><D:write/></D:locktype> + <D:owner> + <D:href>http://example.org/~ejw/contact.html</D:href> + </D:owner> +</D:lockinfo>'); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->response = new HTTP\ResponseMock(); + $this->server->httpResponse = $this->response; + + $this->server->exec(); + + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + + $this->assertEquals(423, $this->response->status, 'Full response: ' . $this->response->body); + + } + + /** + * @depends testLock + */ + function testLockRefresh() { + + $request = new HTTP\Request('LOCK', '/test.txt'); + $request->setBody('<?xml version="1.0"?> +<D:lockinfo xmlns:D="DAV:"> + <D:lockscope><D:exclusive/></D:lockscope> + <D:locktype><D:write/></D:locktype> + <D:owner> + <D:href>http://example.org/~ejw/contact.html</D:href> + </D:owner> +</D:lockinfo>'); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $lockToken = $this->response->getHeader('Lock-Token'); + + $this->response = new HTTP\ResponseMock(); + $this->server->httpResponse = $this->response; + + $request = new HTTP\Request('LOCK', '/test.txt', ['If' => '(' . $lockToken . ')']); + $request->setBody(''); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + + $this->assertEquals(200, $this->response->status, 'We received an incorrect status code. Full response body: ' . $this->response->getBody()); + + } + + /** + * @depends testLock + */ + function testLockRefreshBadToken() { + + $request = new HTTP\Request('LOCK', '/test.txt'); + $request->setBody('<?xml version="1.0"?> +<D:lockinfo xmlns:D="DAV:"> + <D:lockscope><D:exclusive/></D:lockscope> + <D:locktype><D:write/></D:locktype> + <D:owner> + <D:href>http://example.org/~ejw/contact.html</D:href> + </D:owner> +</D:lockinfo>'); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $lockToken = $this->response->getHeader('Lock-Token'); + + $this->response = new HTTP\ResponseMock(); + $this->server->httpResponse = $this->response; + + $request = new HTTP\Request('LOCK', '/test.txt', ['If' => '(' . $lockToken . 'foobar) (<opaquelocktoken:anotherbadtoken>)']); + $request->setBody(''); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + + $this->assertEquals(423, $this->response->getStatus(), 'We received an incorrect status code. Full response body: ' . $this->response->getBody()); + + } + + /** + * @depends testLock + */ + function testLockNoFile() { + + $request = new HTTP\Request('LOCK', '/notfound.txt'); + $request->setBody('<?xml version="1.0"?> +<D:lockinfo xmlns:D="DAV:"> + <D:lockscope><D:exclusive/></D:lockscope> + <D:locktype><D:write/></D:locktype> + <D:owner> + <D:href>http://example.org/~ejw/contact.html</D:href> + </D:owner> +</D:lockinfo>'); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + $this->assertTrue(preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')) === 1, 'We did not get a valid Locktoken back (' . $this->response->getHeader('Lock-Token') . ')'); + + $this->assertEquals(201, $this->response->status); + + } + + /** + * @depends testLock + */ + function testUnlockNoToken() { + + $request = new HTTP\Request('UNLOCK', '/test.txt'); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + ], + $this->response->getHeaders() + ); + + $this->assertEquals(400, $this->response->status); + + } + + /** + * @depends testLock + */ + function testUnlockBadToken() { + + $request = new HTTP\Request('UNLOCK', '/test.txt', ['Lock-Token' => '<opaquelocktoken:blablabla>']); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + ], + $this->response->getHeaders() + ); + + $this->assertEquals(409, $this->response->status, 'Got an incorrect status code. Full response body: ' . $this->response->body); + + } + + /** + * @depends testLock + */ + function testLockPutNoToken() { + + $request = new HTTP\Request('LOCK', '/test.txt'); + $request->setBody('<?xml version="1.0"?> +<D:lockinfo xmlns:D="DAV:"> + <D:lockscope><D:exclusive/></D:lockscope> + <D:locktype><D:write/></D:locktype> + <D:owner> + <D:href>http://example.org/~ejw/contact.html</D:href> + </D:owner> +</D:lockinfo>'); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + $this->assertTrue(preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')) === 1, 'We did not get a valid Locktoken back (' . $this->response->getHeader('Lock-Token') . ')'); + + $this->assertEquals(200, $this->response->status); + + $request = new HTTP\Request('PUT', '/test.txt'); + $request->setBody('newbody'); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + $this->assertTrue(preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')) === 1, 'We did not get a valid Locktoken back (' . $this->response->getHeader('Lock-Token') . ')'); + + $this->assertEquals(423, $this->response->status); + + } + + /** + * @depends testLock + */ + function testUnlock() { + + $request = new HTTP\Request('LOCK', '/test.txt'); + $this->server->httpRequest = $request; + + $request->setBody('<?xml version="1.0"?> +<D:lockinfo xmlns:D="DAV:"> + <D:lockscope><D:exclusive/></D:lockscope> + <D:locktype><D:write/></D:locktype> + <D:owner> + <D:href>http://example.org/~ejw/contact.html</D:href> + </D:owner> +</D:lockinfo>'); + + $this->server->invokeMethod($request, $this->server->httpResponse); + $lockToken = $this->server->httpResponse->getHeader('Lock-Token'); + + $request = new HTTP\Request('UNLOCK', '/test.txt', ['Lock-Token' => $lockToken]); + $this->server->httpRequest = $request; + $this->server->httpResponse = new HTTP\ResponseMock(); + $this->server->invokeMethod($request, $this->server->httpResponse); + + $this->assertEquals(204, $this->server->httpResponse->status, 'Got an incorrect status code. Full response body: ' . $this->response->body); + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Length' => ['0'], + ], + $this->server->httpResponse->getHeaders() + ); + + + } + + /** + * @depends testLock + */ + function testUnlockWindowsBug() { + + $request = new HTTP\Request('LOCK', '/test.txt'); + $this->server->httpRequest = $request; + + $request->setBody('<?xml version="1.0"?> +<D:lockinfo xmlns:D="DAV:"> + <D:lockscope><D:exclusive/></D:lockscope> + <D:locktype><D:write/></D:locktype> + <D:owner> + <D:href>http://example.org/~ejw/contact.html</D:href> + </D:owner> +</D:lockinfo>'); + + $this->server->invokeMethod($request, $this->server->httpResponse); + $lockToken = $this->server->httpResponse->getHeader('Lock-Token'); + + // See Issue 123 + $lockToken = trim($lockToken, '<>'); + + $request = new HTTP\Request('UNLOCK', '/test.txt', ['Lock-Token' => $lockToken]); + $this->server->httpRequest = $request; + $this->server->httpResponse = new HTTP\ResponseMock(); + $this->server->invokeMethod($request, $this->server->httpResponse); + + $this->assertEquals(204, $this->server->httpResponse->status, 'Got an incorrect status code. Full response body: ' . $this->response->body); + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Length' => ['0'], + ], + $this->server->httpResponse->getHeaders() + ); + + + } + + /** + * @depends testLock + */ + function testLockRetainOwner() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_URI' => '/test.txt', + 'REQUEST_METHOD' => 'LOCK', + ]); + $this->server->httpRequest = $request; + + $request->setBody('<?xml version="1.0"?> +<D:lockinfo xmlns:D="DAV:"> + <D:lockscope><D:exclusive/></D:lockscope> + <D:locktype><D:write/></D:locktype> + <D:owner>Evert</D:owner> +</D:lockinfo>'); + + $this->server->invokeMethod($request, $this->server->httpResponse); + $lockToken = $this->server->httpResponse->getHeader('Lock-Token'); + + $locks = $this->locksPlugin->getLocks('test.txt'); + $this->assertEquals(1, count($locks)); + $this->assertEquals('Evert', $locks[0]->owner); + + + } + + /** + * @depends testLock + */ + function testLockPutBadToken() { + + $serverVars = [ + 'REQUEST_URI' => '/test.txt', + 'REQUEST_METHOD' => 'LOCK', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody('<?xml version="1.0"?> +<D:lockinfo xmlns:D="DAV:"> + <D:lockscope><D:exclusive/></D:lockscope> + <D:locktype><D:write/></D:locktype> + <D:owner> + <D:href>http://example.org/~ejw/contact.html</D:href> + </D:owner> +</D:lockinfo>'); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + $this->assertTrue(preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')) === 1, 'We did not get a valid Locktoken back (' . $this->response->getHeader('Lock-Token') . ')'); + + $this->assertEquals(200, $this->response->status); + + $serverVars = [ + 'REQUEST_URI' => '/test.txt', + 'REQUEST_METHOD' => 'PUT', + 'HTTP_IF' => '(<opaquelocktoken:token1>)', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody('newbody'); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + $this->assertTrue(preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')) === 1, 'We did not get a valid Locktoken back (' . $this->response->getHeader('Lock-Token') . ')'); + + // $this->assertEquals('412 Precondition failed',$this->response->status); + $this->assertEquals(423, $this->response->status); + + } + + /** + * @depends testLock + */ + function testLockDeleteParent() { + + $serverVars = [ + 'REQUEST_URI' => '/dir/child.txt', + 'REQUEST_METHOD' => 'LOCK', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody('<?xml version="1.0"?> +<D:lockinfo xmlns:D="DAV:"> + <D:lockscope><D:exclusive/></D:lockscope> + <D:locktype><D:write/></D:locktype> + <D:owner> + <D:href>http://example.org/~ejw/contact.html</D:href> + </D:owner> +</D:lockinfo>'); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + $this->assertTrue(preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')) === 1, 'We did not get a valid Locktoken back (' . $this->response->getHeader('Lock-Token') . ')'); + + $this->assertEquals(200, $this->response->status); + + $serverVars = [ + 'REQUEST_URI' => '/dir', + 'REQUEST_METHOD' => 'DELETE', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(423, $this->response->status); + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + + } + /** + * @depends testLock + */ + function testLockDeleteSucceed() { + + $serverVars = [ + 'REQUEST_URI' => '/dir/child.txt', + 'REQUEST_METHOD' => 'LOCK', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody('<?xml version="1.0"?> +<D:lockinfo xmlns:D="DAV:"> + <D:lockscope><D:exclusive/></D:lockscope> + <D:locktype><D:write/></D:locktype> + <D:owner> + <D:href>http://example.org/~ejw/contact.html</D:href> + </D:owner> +</D:lockinfo>'); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + $this->assertTrue(preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')) === 1, 'We did not get a valid Locktoken back (' . $this->response->getHeader('Lock-Token') . ')'); + + $this->assertEquals(200, $this->response->status); + + $serverVars = [ + 'REQUEST_URI' => '/dir/child.txt', + 'REQUEST_METHOD' => 'DELETE', + 'HTTP_IF' => '(' . $this->response->getHeader('Lock-Token') . ')', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(204, $this->response->status); + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + + } + + /** + * @depends testLock + */ + function testLockCopyLockSource() { + + $serverVars = [ + 'REQUEST_URI' => '/dir/child.txt', + 'REQUEST_METHOD' => 'LOCK', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody('<?xml version="1.0"?> +<D:lockinfo xmlns:D="DAV:"> + <D:lockscope><D:exclusive/></D:lockscope> + <D:locktype><D:write/></D:locktype> + <D:owner> + <D:href>http://example.org/~ejw/contact.html</D:href> + </D:owner> +</D:lockinfo>'); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + $this->assertTrue(preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')) === 1, 'We did not get a valid Locktoken back (' . $this->response->getHeader('Lock-Token') . ')'); + + $this->assertEquals(200, $this->response->status); + + $serverVars = [ + 'REQUEST_URI' => '/dir/child.txt', + 'REQUEST_METHOD' => 'COPY', + 'HTTP_DESTINATION' => '/dir/child2.txt', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(201, $this->response->status, 'Copy must succeed if only the source is locked, but not the destination'); + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + + } + /** + * @depends testLock + */ + function testLockCopyLockDestination() { + + $serverVars = [ + 'REQUEST_URI' => '/dir/child2.txt', + 'REQUEST_METHOD' => 'LOCK', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody('<?xml version="1.0"?> +<D:lockinfo xmlns:D="DAV:"> + <D:lockscope><D:exclusive/></D:lockscope> + <D:locktype><D:write/></D:locktype> + <D:owner> + <D:href>http://example.org/~ejw/contact.html</D:href> + </D:owner> +</D:lockinfo>'); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + $this->assertTrue(preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')) === 1, 'We did not get a valid Locktoken back (' . $this->response->getHeader('Lock-Token') . ')'); + + $this->assertEquals(201, $this->response->status); + + $serverVars = [ + 'REQUEST_URI' => '/dir/child.txt', + 'REQUEST_METHOD' => 'COPY', + 'HTTP_DESTINATION' => '/dir/child2.txt', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(423, $this->response->status, 'Copy must succeed if only the source is locked, but not the destination'); + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + + } + + /** + * @depends testLock + */ + function testLockMoveLockSourceLocked() { + + $serverVars = [ + 'REQUEST_URI' => '/dir/child.txt', + 'REQUEST_METHOD' => 'LOCK', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody('<?xml version="1.0"?> +<D:lockinfo xmlns:D="DAV:"> + <D:lockscope><D:exclusive/></D:lockscope> + <D:locktype><D:write/></D:locktype> + <D:owner> + <D:href>http://example.org/~ejw/contact.html</D:href> + </D:owner> +</D:lockinfo>'); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + $this->assertTrue(preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')) === 1, 'We did not get a valid Locktoken back (' . $this->response->getHeader('Lock-Token') . ')'); + + $this->assertEquals(200, $this->response->status); + + $serverVars = [ + 'REQUEST_URI' => '/dir/child.txt', + 'REQUEST_METHOD' => 'MOVE', + 'HTTP_DESTINATION' => '/dir/child2.txt', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(423, $this->response->status, 'Copy must succeed if only the source is locked, but not the destination'); + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + + } + + /** + * @depends testLock + */ + function testLockMoveLockSourceSucceed() { + + $serverVars = [ + 'REQUEST_URI' => '/dir/child.txt', + 'REQUEST_METHOD' => 'LOCK', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody('<?xml version="1.0"?> +<D:lockinfo xmlns:D="DAV:"> + <D:lockscope><D:exclusive/></D:lockscope> + <D:locktype><D:write/></D:locktype> + <D:owner> + <D:href>http://example.org/~ejw/contact.html</D:href> + </D:owner> +</D:lockinfo>'); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + $this->assertTrue(preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')) === 1, 'We did not get a valid Locktoken back (' . $this->response->getHeader('Lock-Token') . ')'); + + $this->assertEquals(200, $this->response->status); + + $serverVars = [ + 'REQUEST_URI' => '/dir/child.txt', + 'REQUEST_METHOD' => 'MOVE', + 'HTTP_DESTINATION' => '/dir/child2.txt', + 'HTTP_IF' => '(' . $this->response->getHeader('Lock-Token') . ')', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(201, $this->response->status, 'A valid lock-token was provided for the source, so this MOVE operation must succeed. Full response body: ' . $this->response->body); + + } + + /** + * @depends testLock + */ + function testLockMoveLockDestination() { + + $serverVars = [ + 'REQUEST_URI' => '/dir/child2.txt', + 'REQUEST_METHOD' => 'LOCK', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody('<?xml version="1.0"?> +<D:lockinfo xmlns:D="DAV:"> + <D:lockscope><D:exclusive/></D:lockscope> + <D:locktype><D:write/></D:locktype> + <D:owner> + <D:href>http://example.org/~ejw/contact.html</D:href> + </D:owner> +</D:lockinfo>'); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + $this->assertTrue(preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')) === 1, 'We did not get a valid Locktoken back (' . $this->response->getHeader('Lock-Token') . ')'); + + $this->assertEquals(201, $this->response->status); + + $serverVars = [ + 'REQUEST_URI' => '/dir/child.txt', + 'REQUEST_METHOD' => 'MOVE', + 'HTTP_DESTINATION' => '/dir/child2.txt', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(423, $this->response->status, 'Copy must succeed if only the source is locked, but not the destination'); + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + + } + /** + * @depends testLock + */ + function testLockMoveLockParent() { + + $serverVars = [ + 'REQUEST_URI' => '/dir', + 'REQUEST_METHOD' => 'LOCK', + 'HTTP_DEPTH' => 'infinite', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody('<?xml version="1.0"?> +<D:lockinfo xmlns:D="DAV:"> + <D:lockscope><D:exclusive/></D:lockscope> + <D:locktype><D:write/></D:locktype> + <D:owner> + <D:href>http://example.org/~ejw/contact.html</D:href> + </D:owner> +</D:lockinfo>'); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + $this->assertTrue(preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')) === 1, 'We did not get a valid Locktoken back (' . $this->response->getHeader('Lock-Token') . ')'); + + $this->assertEquals(200, $this->response->status); + + $serverVars = [ + 'REQUEST_URI' => '/dir/child.txt', + 'REQUEST_METHOD' => 'MOVE', + 'HTTP_DESTINATION' => '/dir/child2.txt', + 'HTTP_IF' => '</dir> (' . $this->response->getHeader('Lock-Token') . ')', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(201, $this->response->status, 'We locked the parent of both the source and destination, but the move didn\'t succeed.'); + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + + } + + /** + * @depends testLock + */ + function testLockPutGoodToken() { + + $serverVars = [ + 'REQUEST_URI' => '/test.txt', + 'REQUEST_METHOD' => 'LOCK', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody('<?xml version="1.0"?> +<D:lockinfo xmlns:D="DAV:"> + <D:lockscope><D:exclusive/></D:lockscope> + <D:locktype><D:write/></D:locktype> + <D:owner> + <D:href>http://example.org/~ejw/contact.html</D:href> + </D:owner> +</D:lockinfo>'); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + $this->assertTrue(preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')) === 1, 'We did not get a valid Locktoken back (' . $this->response->getHeader('Lock-Token') . ')'); + + $this->assertEquals(200, $this->response->status); + + $serverVars = [ + 'REQUEST_URI' => '/test.txt', + 'REQUEST_METHOD' => 'PUT', + 'HTTP_IF' => '(' . $this->response->getHeader('Lock-Token') . ')', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody('newbody'); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + $this->assertTrue(preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')) === 1, 'We did not get a valid Locktoken back (' . $this->response->getHeader('Lock-Token') . ')'); + + $this->assertEquals(204, $this->response->status); + + } + + /** + * @depends testLock + */ + function testLockPutUnrelatedToken() { + + $request = new HTTP\Request('LOCK', '/unrelated.txt'); + $request->setBody('<?xml version="1.0"?> +<D:lockinfo xmlns:D="DAV:"> + <D:lockscope><D:exclusive/></D:lockscope> + <D:locktype><D:write/></D:locktype> + <D:owner> + <D:href>http://example.org/~ejw/contact.html</D:href> + </D:owner> +</D:lockinfo>'); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + $this->assertTrue(preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')) === 1, 'We did not get a valid Locktoken back (' . $this->response->getHeader('Lock-Token') . ')'); + + $this->assertEquals(201, $this->response->getStatus()); + + $request = new HTTP\Request( + 'PUT', + '/test.txt', + ['If' => '</unrelated.txt> (' . $this->response->getHeader('Lock-Token') . ')'] + ); + $request->setBody('newbody'); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + $this->assertTrue(preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')) === 1, 'We did not get a valid Locktoken back (' . $this->response->getHeader('Lock-Token') . ')'); + + $this->assertEquals(204, $this->response->status); + + } + + function testPutWithIncorrectETag() { + + $serverVars = [ + 'REQUEST_URI' => '/test.txt', + 'REQUEST_METHOD' => 'PUT', + 'HTTP_IF' => '(["etag1"])', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody('newbody'); + $this->server->httpRequest = $request; + $this->server->exec(); + $this->assertEquals(412, $this->response->status); + + } + + /** + * @depends testPutWithIncorrectETag + */ + function testPutWithCorrectETag() { + + // We need an ETag-enabled file node. + $tree = new DAV\Tree(new DAV\FSExt\Directory(SABRE_TEMPDIR)); + $this->server->tree = $tree; + + $filename = SABRE_TEMPDIR . '/test.txt'; + $etag = sha1( + fileinode($filename) . + filesize($filename) . + filemtime($filename) + ); + $serverVars = [ + 'REQUEST_URI' => '/test.txt', + 'REQUEST_METHOD' => 'PUT', + 'HTTP_IF' => '(["' . $etag . '"])', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody('newbody'); + $this->server->httpRequest = $request; + $this->server->exec(); + $this->assertEquals(204, $this->response->status, 'Incorrect status received. Full response body:' . $this->response->body); + + } + + function testDeleteWithETagOnCollection() { + + $serverVars = [ + 'REQUEST_URI' => '/dir', + 'REQUEST_METHOD' => 'DELETE', + 'HTTP_IF' => '(["etag1"])', + ]; + $request = HTTP\Sapi::createFromServerArray($serverVars); + + $this->server->httpRequest = $request; + $this->server->exec(); + $this->assertEquals(412, $this->response->status); + + } + + function testGetTimeoutHeader() { + + $request = HTTP\Sapi::createFromServerArray([ + 'HTTP_TIMEOUT' => 'second-100', + ]); + + $this->server->httpRequest = $request; + $this->assertEquals(100, $this->locksPlugin->getTimeoutHeader()); + + } + + function testGetTimeoutHeaderTwoItems() { + + $request = HTTP\Sapi::createFromServerArray([ + 'HTTP_TIMEOUT' => 'second-5, infinite', + ]); + + $this->server->httpRequest = $request; + $this->assertEquals(5, $this->locksPlugin->getTimeoutHeader()); + + } + + function testGetTimeoutHeaderInfinite() { + + $request = HTTP\Sapi::createFromServerArray([ + 'HTTP_TIMEOUT' => 'infinite, second-5', + ]); + + $this->server->httpRequest = $request; + $this->assertEquals(LockInfo::TIMEOUT_INFINITE, $this->locksPlugin->getTimeoutHeader()); + + } + + /** + * @expectedException Sabre\DAV\Exception\BadRequest + */ + function testGetTimeoutHeaderInvalid() { + + $request = HTTP\Sapi::createFromServerArray([ + 'HTTP_TIMEOUT' => 'yourmom', + ]); + + $this->server->httpRequest = $request; + $this->locksPlugin->getTimeoutHeader(); + + } + + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Mock/Collection.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Mock/Collection.php new file mode 100644 index 00000000000..fded5e474a6 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Mock/Collection.php @@ -0,0 +1,168 @@ +<?php + +namespace Sabre\DAV\Mock; + +use Sabre\DAV; + +/** + * Mock Collection. + * + * This collection quickly allows you to create trees of nodes. + * Children are specified as an array. + * + * Every key a filename, every array value is either: + * * an array, for a sub-collection + * * a string, for a file + * * An instance of \Sabre\DAV\INode. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Collection extends DAV\Collection { + + protected $name; + protected $children; + protected $parent; + + /** + * Creates the object + * + * @param string $name + * @param array $children + * @param Collection $parent + * @return void + */ + function __construct($name, array $children = [], Collection $parent = null) { + + $this->name = $name; + foreach ($children as $key => $value) { + if (is_string($value)) { + $this->children[] = new File($key, $value, $this); + } elseif (is_array($value)) { + $this->children[] = new self($key, $value, $this); + } elseif ($value instanceof \Sabre\DAV\INode) { + $this->children[] = $value; + } else { + throw new \InvalidArgumentException('Unknown value passed in $children'); + } + } + $this->parent = $parent; + + } + + /** + * Returns the name of the node. + * + * This is used to generate the url. + * + * @return string + */ + function getName() { + + return $this->name; + + } + + /** + * 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 null|string + */ + function createFile($name, $data = null) { + + if (is_resource($data)) { + $data = stream_get_contents($data); + } + $this->children[] = new File($name, $data, $this); + return '"' . md5($data) . '"'; + + } + + /** + * Creates a new subdirectory + * + * @param string $name + * @return void + */ + function createDirectory($name) { + + $this->children[] = new self($name); + + } + + /** + * Returns an array with all the child nodes + * + * @return \Sabre\DAV\INode[] + */ + function getChildren() { + + return $this->children; + + } + + /** + * Adds an already existing node to this collection. + * + * @param \Sabre\DAV\INode $node + */ + function addNode(\Sabre\DAV\INode $node) { + + $this->children[] = $node; + + } + + /** + * Removes a childnode from this node. + * + * @param string $name + * @return void + */ + function deleteChild($name) { + + foreach ($this->children as $key => $value) { + + if ($value->getName() == $name) { + unset($this->children[$key]); + return; + } + + } + + } + + /** + * Deletes this collection and all its children,. + * + * @return void + */ + function delete() { + + foreach ($this->getChildren() as $child) { + $this->deleteChild($child->getName()); + } + $this->parent->deleteChild($this->getName()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Mock/File.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Mock/File.php new file mode 100644 index 00000000000..a624b6b6bf1 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Mock/File.php @@ -0,0 +1,163 @@ +<?php + +namespace Sabre\DAV\Mock; + +use Sabre\DAV; + +/** + * Mock File + * + * See the Collection in this directory for more details. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class File extends DAV\File { + + protected $name; + protected $contents; + protected $parent; + protected $lastModified; + + /** + * Creates the object + * + * @param string $name + * @param resource $contents + * @param Collection $parent + * @param int $lastModified + * @return void + */ + function __construct($name, $contents, Collection $parent = null, $lastModified = -1) { + + $this->name = $name; + $this->put($contents); + $this->parent = $parent; + + if ($lastModified === -1) { + $lastModified = time(); + } + + $this->lastModified = $lastModified; + + } + + /** + * Returns the name of the node. + * + * This is used to generate the url. + * + * @return string + */ + function getName() { + + return $this->name; + + } + + /** + * Changes the name of the node. + * + * @param string $name + * @return void + */ + function setName($name) { + + $this->name = $name; + + } + + /** + * Updates the data + * + * The data argument is a readable stream resource. + * + * 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. + * + * If you don't plan to store the file byte-by-byte, and you return a + * different object on a subsequent GET you are strongly recommended to not + * return an ETag, and just return null. + * + * @param resource $data + * @return string|null + */ + function put($data) { + + if (is_resource($data)) { + $data = stream_get_contents($data); + } + $this->contents = $data; + return '"' . md5($data) . '"'; + + } + + /** + * Returns the data + * + * This method may either return a string or a readable stream resource + * + * @return mixed + */ + function get() { + + return $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. + * + * Return null if the ETag can not effectively be determined + * + * @return void + */ + function getETag() { + + return '"' . md5($this->contents) . '"'; + + } + + /** + * Returns the size of the node, in bytes + * + * @return int + */ + function getSize() { + + return strlen($this->contents); + + } + + /** + * Delete the node + * + * @return void + */ + function delete() { + + $this->parent->deleteChild($this->name); + + } + + /** + * Returns the last modification time as a unix timestamp. + * If the information is not available, return null. + * + * @return int + */ + function getLastModified() { + + return $this->lastModified; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Mock/PropertiesCollection.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Mock/PropertiesCollection.php new file mode 100644 index 00000000000..af3fd2d3f2f --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Mock/PropertiesCollection.php @@ -0,0 +1,94 @@ +<?php + +namespace Sabre\DAV\Mock; + +use Sabre\DAV\IProperties; +use Sabre\DAV\PropPatch; + +/** + * A node specifically for testing property-related operations + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class PropertiesCollection extends Collection implements IProperties { + + public $failMode = false; + + public $properties; + + /** + * Creates the object + * + * @param string $name + * @param array $children + * @param array $properties + * @return void + */ + function __construct($name, array $children, array $properties = []) { + + parent::__construct($name, $children, null); + $this->properties = $properties; + + } + + /** + * 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. + * + * @param PropPatch $proppatch + * @return bool|array + */ + function propPatch(PropPatch $proppatch) { + + $proppatch->handleRemaining(function($updateProperties) { + + switch ($this->failMode) { + case 'updatepropsfalse' : return false; + case 'updatepropsarray' : + $r = []; + foreach ($updateProperties as $k => $v) $r[$k] = 402; + return $r; + case 'updatepropsobj' : + return new \STDClass(); + } + + }); + + } + + /** + * 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 $requestedProperties + * @return array + */ + function getProperties($requestedProperties) { + + $returnedProperties = []; + foreach ($requestedProperties as $requestedProperty) { + if (isset($this->properties[$requestedProperty])) { + $returnedProperties[$requestedProperty] = + $this->properties[$requestedProperty]; + } + } + return $returnedProperties; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Mock/SharedNode.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Mock/SharedNode.php new file mode 100644 index 00000000000..503d6407095 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Mock/SharedNode.php @@ -0,0 +1,125 @@ +<?php + +namespace Sabre\DAV\Mock; + +use Sabre\DAV\Sharing\ISharedNode; +use Sabre\DAV\Sharing\Sharee; + +class SharedNode extends \Sabre\DAV\Node implements ISharedNode { + + protected $name; + protected $access; + protected $invites = []; + + function __construct($name, $access) { + + $this->name = $name; + $this->access = $access; + + } + + function getName() { + + return $this->name; + + } + + /** + * Returns the 'access level' for the instance of this shared resource. + * + * The value should be one of the Sabre\DAV\Sharing\Plugin::ACCESS_ + * constants. + * + * @return int + */ + function getShareAccess() { + + return $this->access; + + } + + /** + * 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 + */ + function getShareResourceUri() { + + return 'urn:example:bar'; + + } + + /** + * Updates the list of sharees. + * + * Every item must be a Sharee object. + * + * @param Sharee[] $sharees + * @return void + */ + function updateInvites(array $sharees) { + + foreach ($sharees as $sharee) { + + if ($sharee->access === \Sabre\DAV\Sharing\Plugin::ACCESS_NOACCESS) { + // Removal + foreach ($this->invites as $k => $invitee) { + + if ($invitee->href = $sharee->href) { + unset($this->invites[$k]); + } + + } + + } else { + foreach ($this->invites as $k => $invitee) { + + if ($invitee->href = $sharee->href) { + if (!$sharee->inviteStatus) { + $sharee->inviteStatus = $invitee->inviteStatus; + } + // Overwriting an existing invitee + $this->invites[$k] = $sharee; + continue 2; + } + + } + if (!$sharee->inviteStatus) { + $sharee->inviteStatus = \Sabre\DAV\Sharing\Plugin::INVITE_NORESPONSE; + } + // Adding a new invitee + $this->invites[] = $sharee; + } + + } + + } + + /** + * 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[] + */ + function getInvites() { + + return $this->invites; + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Mock/StreamingFile.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Mock/StreamingFile.php new file mode 100644 index 00000000000..d60a49d092e --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Mock/StreamingFile.php @@ -0,0 +1,102 @@ +<?php + +namespace Sabre\DAV\Mock; + +/** + * Mock Streaming File File + * + * Works similar to the mock file, but this one works with streams and has no + * content-length or etags. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class StreamingFile extends File { + + protected $size; + + /** + * Updates the data + * + * The data argument is a readable stream resource. + * + * 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. + * + * If you don't plan to store the file byte-by-byte, and you return a + * different object on a subsequent GET you are strongly recommended to not + * return an ETag, and just return null. + * + * @param resource $data + * @return string|null + */ + function put($data) { + + if (is_string($data)) { + $stream = fopen('php://memory', 'r+'); + fwrite($stream, $data); + rewind($stream); + $data = $stream; + } + $this->contents = $data; + + } + + /** + * Returns the data + * + * This method may either return a string or a readable stream resource + * + * @return mixed + */ + function get() { + + return $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. + * + * Return null if the ETag can not effectively be determined + * + * @return void + */ + function getETag() { + + return null; + + } + + /** + * Returns the size of the node, in bytes + * + * @return int + */ + function getSize() { + + return $this->size; + + } + + /** + * Allows testing scripts to set the resource's file size. + * + * @param int $size + * @return void + */ + function setSize($size) { + + $this->size = $size; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/MockLogger.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/MockLogger.php new file mode 100644 index 00000000000..03332569300 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/MockLogger.php @@ -0,0 +1,36 @@ +<?php + +namespace Sabre\DAV; + +use Psr\Log\AbstractLogger; + +/** + * The MockLogger is a simple PSR-3 implementation that we can use to test + * whether things get logged correctly. + * + * @copyright Copyright (C) fruux GmbH. (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class MockLogger extends AbstractLogger { + + public $logs = []; + + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + * @return null + */ + function log($level, $message, array $context = []) { + + $this->logs[] = [ + $level, + $message, + $context + ]; + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Mount/PluginTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Mount/PluginTest.php new file mode 100644 index 00000000000..3213fcb1b45 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Mount/PluginTest.php @@ -0,0 +1,58 @@ +<?php + +namespace Sabre\DAV\Mount; + +use Sabre\DAV; +use Sabre\HTTP; + +require_once 'Sabre/DAV/AbstractServer.php'; + +class PluginTest extends DAV\AbstractServer { + + function setUp() { + + parent::setUp(); + $this->server->addPlugin(new Plugin()); + + } + + function testPassThrough() { + + $serverVars = [ + 'REQUEST_URI' => '/', + 'REQUEST_METHOD' => 'GET', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals(501, $this->response->status, 'We expected GET to not be implemented for Directories. Response body: ' . $this->response->body); + + } + + function testMountResponse() { + + $serverVars = [ + 'REQUEST_URI' => '/?mount', + 'REQUEST_METHOD' => 'GET', + 'QUERY_STRING' => 'mount', + 'HTTP_HOST' => 'example.org', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals(200, $this->response->status); + + $xml = simplexml_load_string($this->response->body); + $this->assertInstanceOf('SimpleXMLElement', $xml, 'Response was not a valid xml document. The list of errors:' . print_r(libxml_get_errors(), true) . '. xml body: ' . $this->response->body . '. What type we got: ' . gettype($xml) . ' class, if object: ' . get_class($xml)); + + $xml->registerXPathNamespace('dm', 'http://purl.org/NET/webdav/mount'); + $url = $xml->xpath('//dm:url'); + $this->assertEquals('http://example.org/', (string)$url[0]); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ObjectTreeTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ObjectTreeTest.php new file mode 100644 index 00000000000..15289ce528c --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ObjectTreeTest.php @@ -0,0 +1,100 @@ +<?php + +namespace Sabre\DAV; + +require_once 'Sabre/TestUtil.php'; + +class ObjectTreeTest extends \PHPUnit_Framework_TestCase { + + protected $tree; + + function setup() { + + \Sabre\TestUtil::clearTempDir(); + mkdir(SABRE_TEMPDIR . '/root'); + mkdir(SABRE_TEMPDIR . '/root/subdir'); + file_put_contents(SABRE_TEMPDIR . '/root/file.txt', 'contents'); + file_put_contents(SABRE_TEMPDIR . '/root/subdir/subfile.txt', 'subcontents'); + $rootNode = new FSExt\Directory(SABRE_TEMPDIR . '/root'); + $this->tree = new Tree($rootNode); + + } + + function teardown() { + + \Sabre\TestUtil::clearTempDir(); + + } + + function testGetRootNode() { + + $root = $this->tree->getNodeForPath(''); + $this->assertInstanceOf('Sabre\\DAV\\FSExt\\Directory', $root); + + } + + function testGetSubDir() { + + $root = $this->tree->getNodeForPath('subdir'); + $this->assertInstanceOf('Sabre\\DAV\\FSExt\\Directory', $root); + + } + + function testCopyFile() { + + $this->tree->copy('file.txt', 'file2.txt'); + $this->assertTrue(file_exists(SABRE_TEMPDIR . '/root/file2.txt')); + $this->assertEquals('contents', file_get_contents(SABRE_TEMPDIR . '/root/file2.txt')); + + } + + /** + * @depends testCopyFile + */ + function testCopyDirectory() { + + $this->tree->copy('subdir', 'subdir2'); + $this->assertTrue(file_exists(SABRE_TEMPDIR . '/root/subdir2')); + $this->assertTrue(file_exists(SABRE_TEMPDIR . '/root/subdir2/subfile.txt')); + $this->assertEquals('subcontents', file_get_contents(SABRE_TEMPDIR . '/root/subdir2/subfile.txt')); + + } + + /** + * @depends testCopyFile + */ + function testMoveFile() { + + $this->tree->move('file.txt', 'file2.txt'); + $this->assertTrue(file_exists(SABRE_TEMPDIR . '/root/file2.txt')); + $this->assertFalse(file_exists(SABRE_TEMPDIR . '/root/file.txt')); + $this->assertEquals('contents', file_get_contents(SABRE_TEMPDIR . '/root/file2.txt')); + + } + + /** + * @depends testMoveFile + */ + function testMoveFileNewParent() { + + $this->tree->move('file.txt', 'subdir/file2.txt'); + $this->assertTrue(file_exists(SABRE_TEMPDIR . '/root/subdir/file2.txt')); + $this->assertFalse(file_exists(SABRE_TEMPDIR . '/root/file.txt')); + $this->assertEquals('contents', file_get_contents(SABRE_TEMPDIR . '/root/subdir/file2.txt')); + + } + + /** + * @depends testCopyDirectory + */ + function testMoveDirectory() { + + $this->tree->move('subdir', 'subdir2'); + $this->assertTrue(file_exists(SABRE_TEMPDIR . '/root/subdir2')); + $this->assertTrue(file_exists(SABRE_TEMPDIR . '/root/subdir2/subfile.txt')); + $this->assertFalse(file_exists(SABRE_TEMPDIR . '/root/subdir')); + $this->assertEquals('subcontents', file_get_contents(SABRE_TEMPDIR . '/root/subdir2/subfile.txt')); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PSR3Test.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PSR3Test.php new file mode 100644 index 00000000000..d30fde128a7 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PSR3Test.php @@ -0,0 +1,87 @@ +<?php + +namespace Sabre\DAV; + +class PSR3Test extends \PHPUnit_Framework_TestCase { + + function testIsLoggerAware() { + + $server = new Server(); + $this->assertInstanceOf( + 'Psr\Log\LoggerAwareInterface', + $server + ); + + } + + function testGetNullLoggerByDefault() { + + $server = new Server(); + $this->assertInstanceOf( + 'Psr\Log\NullLogger', + $server->getLogger() + ); + + } + + function testSetLogger() { + + $server = new Server(); + $logger = new MockLogger(); + + $server->setLogger($logger); + + $this->assertEquals( + $logger, + $server->getLogger() + ); + + } + + /** + * Start the server, trigger an exception and see if the logger captured + * it. + */ + function testLogException() { + + $server = new Server(); + $logger = new MockLogger(); + + $server->setLogger($logger); + + // Creating a fake environment to execute http requests in. + $request = new \Sabre\HTTP\Request( + 'GET', + '/not-found', + [] + ); + $response = new \Sabre\HTTP\Response(); + + $server->httpRequest = $request; + $server->httpResponse = $response; + $server->sapi = new \Sabre\HTTP\SapiMock(); + + // Executing the request. + $server->exec(); + + // The request should have triggered a 404 status. + $this->assertEquals(404, $response->getStatus()); + + // We should also see this in the PSR-3 log. + $this->assertEquals(1, count($logger->logs)); + + $logItem = $logger->logs[0]; + + $this->assertEquals( + \Psr\Log\LogLevel::INFO, + $logItem[0] + ); + + $this->assertInstanceOf( + 'Exception', + $logItem[2]['exception'] + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PartialUpdate/FileMock.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PartialUpdate/FileMock.php new file mode 100644 index 00000000000..eff1e7d6738 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PartialUpdate/FileMock.php @@ -0,0 +1,122 @@ +<?php + +namespace Sabre\DAV\PartialUpdate; + +use Sabre\DAV; + +class FileMock implements IPatchSupport { + + protected $data = ''; + + function put($str) { + + if (is_resource($str)) { + $str = stream_get_contents($str); + } + $this->data = $str; + + } + + /** + * 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 + * * 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 + */ + function patch($data, $rangeType, $offset = null) { + + if (is_resource($data)) { + $data = stream_get_contents($data); + } + + switch ($rangeType) { + + case 1 : + $this->data .= $data; + break; + case 3 : + // Turn the offset into an offset-offset. + $offset = strlen($this->data) - $offset; + // No break is intentional + case 2 : + $this->data = + substr($this->data, 0, $offset) . + $data . + substr($this->data, $offset + strlen($data)); + break; + + } + + } + + function get() { + + return $this->data; + + } + + function getContentType() { + + return 'text/plain'; + + } + + function getSize() { + + return strlen($this->data); + + } + + function getETag() { + + return '"' . $this->data . '"'; + + } + + function delete() { + + throw new DAV\Exception\MethodNotAllowed(); + + } + + function setName($name) { + + throw new DAV\Exception\MethodNotAllowed(); + + } + + function getName() { + + return 'partial'; + + } + + function getLastModified() { + + return null; + + } + + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PartialUpdate/PluginTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PartialUpdate/PluginTest.php new file mode 100644 index 00000000000..5bd696416bb --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PartialUpdate/PluginTest.php @@ -0,0 +1,135 @@ +<?php + +namespace Sabre\DAV\PartialUpdate; + +use Sabre\DAV; +use Sabre\HTTP; + +require_once 'Sabre/DAV/PartialUpdate/FileMock.php'; + +class PluginTest extends \Sabre\DAVServerTest { + + protected $node; + protected $plugin; + + function setUp() { + + $this->node = new FileMock(); + $this->tree[] = $this->node; + + parent::setUp(); + + $this->plugin = new Plugin(); + $this->server->addPlugin($this->plugin); + + + + } + + function testInit() { + + $this->assertEquals('partialupdate', $this->plugin->getPluginName()); + $this->assertEquals(['sabredav-partialupdate'], $this->plugin->getFeatures()); + $this->assertEquals([ + 'PATCH' + ], $this->plugin->getHTTPMethods('partial')); + $this->assertEquals([ + ], $this->plugin->getHTTPMethods('')); + + } + + function testPatchNoRange() { + + $this->node->put('aaaaaaaa'); + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'PATCH', + 'REQUEST_URI' => '/partial', + ]); + $response = $this->request($request); + + $this->assertEquals(400, $response->status, 'Full response body:' . $response->body); + + } + + function testPatchNotSupported() { + + $this->node->put('aaaaaaaa'); + $request = new HTTP\Request('PATCH', '/', ['X-Update-Range' => '3-4']); + $request->setBody( + 'bbb' + ); + $response = $this->request($request); + + $this->assertEquals(405, $response->status, 'Full response body:' . $response->body); + + } + + function testPatchNoContentType() { + + $this->node->put('aaaaaaaa'); + $request = new HTTP\Request('PATCH', '/partial', ['X-Update-Range' => 'bytes=3-4']); + $request->setBody( + 'bbb' + ); + $response = $this->request($request); + + $this->assertEquals(415, $response->status, 'Full response body:' . $response->body); + + } + + function testPatchBadRange() { + + $this->node->put('aaaaaaaa'); + $request = new HTTP\Request('PATCH', '/partial', ['X-Update-Range' => 'bytes=3-4', 'Content-Type' => 'application/x-sabredav-partialupdate', 'Content-Length' => '3']); + $request->setBody( + 'bbb' + ); + $response = $this->request($request); + + $this->assertEquals(416, $response->status, 'Full response body:' . $response->body); + + } + + function testPatchNoLength() { + + $this->node->put('aaaaaaaa'); + $request = new HTTP\Request('PATCH', '/partial', ['X-Update-Range' => 'bytes=3-5', 'Content-Type' => 'application/x-sabredav-partialupdate']); + $request->setBody( + 'bbb' + ); + $response = $this->request($request); + + $this->assertEquals(411, $response->status, 'Full response body:' . $response->body); + + } + + function testPatchSuccess() { + + $this->node->put('aaaaaaaa'); + $request = new HTTP\Request('PATCH', '/partial', ['X-Update-Range' => 'bytes=3-5', 'Content-Type' => 'application/x-sabredav-partialupdate', 'Content-Length' => 3]); + $request->setBody( + 'bbb' + ); + $response = $this->request($request); + + $this->assertEquals(204, $response->status, 'Full response body:' . $response->body); + $this->assertEquals('aaabbbaa', $this->node->get()); + + } + + function testPatchNoEndRange() { + + $this->node->put('aaaaa'); + $request = new HTTP\Request('PATCH', '/partial', ['X-Update-Range' => 'bytes=3-', 'Content-Type' => 'application/x-sabredav-partialupdate', 'Content-Length' => '3']); + $request->setBody( + 'bbb' + ); + + $response = $this->request($request); + + $this->assertEquals(204, $response->getStatus(), 'Full response body:' . $response->getBodyAsString()); + $this->assertEquals('aaabbb', $this->node->get()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PartialUpdate/SpecificationTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PartialUpdate/SpecificationTest.php new file mode 100644 index 00000000000..2c627417330 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PartialUpdate/SpecificationTest.php @@ -0,0 +1,94 @@ +<?php + +namespace Sabre\DAV\PartialUpdate; + +use Sabre\DAV\FSExt\File; +use Sabre\DAV\Server; +use Sabre\HTTP; + +/** + * This test is an end-to-end sabredav test that goes through all + * the cases in the specification. + * + * See: http://sabre.io/dav/http-patch/ + */ +class SpecificationTest extends \PHPUnit_Framework_TestCase { + + protected $server; + + function setUp() { + + $tree = [ + new File(SABRE_TEMPDIR . '/foobar.txt') + ]; + $server = new Server($tree); + $server->debugExceptions = true; + $server->addPlugin(new Plugin()); + + $tree[0]->put('1234567890'); + + $this->server = $server; + + } + + function tearDown() { + + \Sabre\TestUtil::clearTempDir(); + + } + + /** + * @param string $headerValue + * @param string $httpStatus + * @param string $endResult + * @param int $contentLength + * + * @dataProvider data + */ + function testUpdateRange($headerValue, $httpStatus, $endResult, $contentLength = 4) { + + $headers = [ + 'Content-Type' => 'application/x-sabredav-partialupdate', + 'X-Update-Range' => $headerValue, + ]; + + if ($contentLength) { + $headers['Content-Length'] = (string)$contentLength; + } + + $request = new HTTP\Request('PATCH', '/foobar.txt', $headers, '----'); + + $request->setBody('----'); + $this->server->httpRequest = $request; + $this->server->httpResponse = new HTTP\ResponseMock(); + $this->server->sapi = new HTTP\SapiMock(); + $this->server->exec(); + + $this->assertEquals($httpStatus, $this->server->httpResponse->status, 'Incorrect http status received: ' . $this->server->httpResponse->body); + if (!is_null($endResult)) { + $this->assertEquals($endResult, file_get_contents(SABRE_TEMPDIR . '/foobar.txt')); + } + + } + + function data() { + + return [ + // Problems + ['foo', 400, null], + ['bytes=0-3', 411, null, 0], + ['bytes=4-1', 416, null], + + ['bytes=0-3', 204, '----567890'], + ['bytes=1-4', 204, '1----67890'], + ['bytes=0-', 204, '----567890'], + ['bytes=-4', 204, '123456----'], + ['bytes=-2', 204, '12345678----'], + ['bytes=2-', 204, '12----7890'], + ['append', 204, '1234567890----'], + + ]; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropFindTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropFindTest.php new file mode 100644 index 00000000000..ec1d616cbb9 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropFindTest.php @@ -0,0 +1,76 @@ +<?php + +namespace Sabre\DAV; + +class PropFindTest extends \PHPUnit_Framework_TestCase { + + function testHandle() { + + $propFind = new PropFind('foo', ['{DAV:}displayname']); + $propFind->handle('{DAV:}displayname', 'foobar'); + + $this->assertEquals([ + 200 => ['{DAV:}displayname' => 'foobar'], + 404 => [], + ], $propFind->getResultForMultiStatus()); + + } + + function testHandleCallBack() { + + $propFind = new PropFind('foo', ['{DAV:}displayname']); + $propFind->handle('{DAV:}displayname', function() { return 'foobar'; }); + + $this->assertEquals([ + 200 => ['{DAV:}displayname' => 'foobar'], + 404 => [], + ], $propFind->getResultForMultiStatus()); + + } + + function testAllPropDefaults() { + + $propFind = new PropFind('foo', ['{DAV:}displayname'], 0, PropFind::ALLPROPS); + + $this->assertEquals([ + 200 => [], + ], $propFind->getResultForMultiStatus()); + + } + + function testSet() { + + $propFind = new PropFind('foo', ['{DAV:}displayname']); + $propFind->set('{DAV:}displayname', 'bar'); + + $this->assertEquals([ + 200 => ['{DAV:}displayname' => 'bar'], + 404 => [], + ], $propFind->getResultForMultiStatus()); + + } + + function testSetAllpropCustom() { + + $propFind = new PropFind('foo', ['{DAV:}displayname'], 0, PropFind::ALLPROPS); + $propFind->set('{DAV:}customproperty', 'bar'); + + $this->assertEquals([ + 200 => ['{DAV:}customproperty' => 'bar'], + ], $propFind->getResultForMultiStatus()); + + } + + function testSetUnset() { + + $propFind = new PropFind('foo', ['{DAV:}displayname']); + $propFind->set('{DAV:}displayname', 'bar'); + $propFind->set('{DAV:}displayname', null); + + $this->assertEquals([ + 200 => [], + 404 => ['{DAV:}displayname' => null], + ], $propFind->getResultForMultiStatus()); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropPatchTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropPatchTest.php new file mode 100644 index 00000000000..72dbf5345b5 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropPatchTest.php @@ -0,0 +1,351 @@ +<?php + +namespace Sabre\DAV; + +class PropPatchTest extends \PHPUnit_Framework_TestCase { + + protected $propPatch; + + function setUp() { + + $this->propPatch = new PropPatch([ + '{DAV:}displayname' => 'foo', + ]); + $this->assertEquals(['{DAV:}displayname' => 'foo'], $this->propPatch->getMutations()); + + } + + function testHandleSingleSuccess() { + + $hasRan = false; + + $this->propPatch->handle('{DAV:}displayname', function($value) use (&$hasRan) { + $hasRan = true; + $this->assertEquals('foo', $value); + return true; + }); + + $this->assertTrue($this->propPatch->commit()); + $result = $this->propPatch->getResult(); + $this->assertEquals(['{DAV:}displayname' => 200], $result); + + $this->assertTrue($hasRan); + + } + + function testHandleSingleFail() { + + $hasRan = false; + + $this->propPatch->handle('{DAV:}displayname', function($value) use (&$hasRan) { + $hasRan = true; + $this->assertEquals('foo', $value); + return false; + }); + + $this->assertFalse($this->propPatch->commit()); + $result = $this->propPatch->getResult(); + $this->assertEquals(['{DAV:}displayname' => 403], $result); + + $this->assertTrue($hasRan); + + } + + function testHandleSingleCustomResult() { + + $hasRan = false; + + $this->propPatch->handle('{DAV:}displayname', function($value) use (&$hasRan) { + $hasRan = true; + $this->assertEquals('foo', $value); + return 201; + }); + + $this->assertTrue($this->propPatch->commit()); + $result = $this->propPatch->getResult(); + $this->assertEquals(['{DAV:}displayname' => 201], $result); + + $this->assertTrue($hasRan); + + } + + function testHandleSingleDeleteSuccess() { + + $hasRan = false; + + $this->propPatch = new PropPatch(['{DAV:}displayname' => null]); + $this->propPatch->handle('{DAV:}displayname', function($value) use (&$hasRan) { + $hasRan = true; + $this->assertNull($value); + return true; + }); + + $this->assertTrue($this->propPatch->commit()); + $result = $this->propPatch->getResult(); + $this->assertEquals(['{DAV:}displayname' => 204], $result); + + $this->assertTrue($hasRan); + + } + + + function testHandleNothing() { + + $hasRan = false; + + $this->propPatch->handle('{DAV:}foobar', function($value) use (&$hasRan) { + $hasRan = true; + }); + + $this->assertFalse($hasRan); + + } + + /** + * @depends testHandleSingleSuccess + */ + function testHandleRemaining() { + + $hasRan = false; + + $this->propPatch->handleRemaining(function($mutations) use (&$hasRan) { + $hasRan = true; + $this->assertEquals(['{DAV:}displayname' => 'foo'], $mutations); + return true; + }); + + $this->assertTrue($this->propPatch->commit()); + $result = $this->propPatch->getResult(); + $this->assertEquals(['{DAV:}displayname' => 200], $result); + + $this->assertTrue($hasRan); + + } + function testHandleRemainingNothingToDo() { + + $hasRan = false; + + $this->propPatch->handle('{DAV:}displayname', function() {}); + $this->propPatch->handleRemaining(function($mutations) use (&$hasRan) { + $hasRan = true; + }); + + $this->assertFalse($hasRan); + + } + + function testSetResultCode() { + + $this->propPatch->setResultCode('{DAV:}displayname', 201); + $this->assertTrue($this->propPatch->commit()); + $result = $this->propPatch->getResult(); + $this->assertEquals(['{DAV:}displayname' => 201], $result); + + } + + function testSetResultCodeFail() { + + $this->propPatch->setResultCode('{DAV:}displayname', 402); + $this->assertFalse($this->propPatch->commit()); + $result = $this->propPatch->getResult(); + $this->assertEquals(['{DAV:}displayname' => 402], $result); + + } + + function testSetRemainingResultCode() { + + $this->propPatch->setRemainingResultCode(204); + $this->assertTrue($this->propPatch->commit()); + $result = $this->propPatch->getResult(); + $this->assertEquals(['{DAV:}displayname' => 204], $result); + + } + + function testCommitNoHandler() { + + $this->assertFalse($this->propPatch->commit()); + $result = $this->propPatch->getResult(); + $this->assertEquals(['{DAV:}displayname' => 403], $result); + + } + + function testHandlerNotCalled() { + + $hasRan = false; + + $this->propPatch->setResultCode('{DAV:}displayname', 402); + $this->propPatch->handle('{DAV:}displayname', function($value) use (&$hasRan) { + $hasRan = true; + }); + + $this->propPatch->commit(); + + // The handler is not supposed to have ran + $this->assertFalse($hasRan); + + } + + function testDependencyFail() { + + $propPatch = new PropPatch([ + '{DAV:}a' => 'foo', + '{DAV:}b' => 'bar', + ]); + + $calledA = false; + $calledB = false; + + $propPatch->handle('{DAV:}a', function() use (&$calledA) { + $calledA = true; + return false; + }); + $propPatch->handle('{DAV:}b', function() use (&$calledB) { + $calledB = true; + return false; + }); + + $result = $propPatch->commit(); + $this->assertTrue($calledA); + $this->assertFalse($calledB); + + $this->assertFalse($result); + + $this->assertEquals([ + '{DAV:}a' => 403, + '{DAV:}b' => 424, + ], $propPatch->getResult()); + + } + + /** + * @expectedException \UnexpectedValueException + */ + function testHandleSingleBrokenResult() { + + $propPatch = new PropPatch([ + '{DAV:}a' => 'foo', + ]); + + $propPatch->handle('{DAV:}a', function() { + return []; + }); + $propPatch->commit(); + + } + + function testHandleMultiValueSuccess() { + + $propPatch = new PropPatch([ + '{DAV:}a' => 'foo', + '{DAV:}b' => 'bar', + '{DAV:}c' => null, + ]); + + $calledA = false; + + $propPatch->handle(['{DAV:}a', '{DAV:}b', '{DAV:}c'], function($properties) use (&$calledA) { + $calledA = true; + $this->assertEquals([ + '{DAV:}a' => 'foo', + '{DAV:}b' => 'bar', + '{DAV:}c' => null, + ], $properties); + return true; + }); + $result = $propPatch->commit(); + $this->assertTrue($calledA); + $this->assertTrue($result); + + $this->assertEquals([ + '{DAV:}a' => 200, + '{DAV:}b' => 200, + '{DAV:}c' => 204, + ], $propPatch->getResult()); + + } + + + function testHandleMultiValueFail() { + + $propPatch = new PropPatch([ + '{DAV:}a' => 'foo', + '{DAV:}b' => 'bar', + '{DAV:}c' => null, + ]); + + $calledA = false; + + $propPatch->handle(['{DAV:}a', '{DAV:}b', '{DAV:}c'], function($properties) use (&$calledA) { + $calledA = true; + $this->assertEquals([ + '{DAV:}a' => 'foo', + '{DAV:}b' => 'bar', + '{DAV:}c' => null, + ], $properties); + return false; + }); + $result = $propPatch->commit(); + $this->assertTrue($calledA); + $this->assertFalse($result); + + $this->assertEquals([ + '{DAV:}a' => 403, + '{DAV:}b' => 403, + '{DAV:}c' => 403, + ], $propPatch->getResult()); + + } + + function testHandleMultiValueCustomResult() { + + $propPatch = new PropPatch([ + '{DAV:}a' => 'foo', + '{DAV:}b' => 'bar', + '{DAV:}c' => null, + ]); + + $calledA = false; + + $propPatch->handle(['{DAV:}a', '{DAV:}b', '{DAV:}c'], function($properties) use (&$calledA) { + $calledA = true; + $this->assertEquals([ + '{DAV:}a' => 'foo', + '{DAV:}b' => 'bar', + '{DAV:}c' => null, + ], $properties); + + return [ + '{DAV:}a' => 201, + '{DAV:}b' => 204, + ]; + }); + $result = $propPatch->commit(); + $this->assertTrue($calledA); + $this->assertFalse($result); + + $this->assertEquals([ + '{DAV:}a' => 201, + '{DAV:}b' => 204, + '{DAV:}c' => 500, + ], $propPatch->getResult()); + + } + + /** + * @expectedException \UnexpectedValueException + */ + function testHandleMultiValueBroken() { + + $propPatch = new PropPatch([ + '{DAV:}a' => 'foo', + '{DAV:}b' => 'bar', + '{DAV:}c' => null, + ]); + + $propPatch->handle(['{DAV:}a', '{DAV:}b', '{DAV:}c'], function($properties) { + return 'hi'; + }); + $propPatch->commit(); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/AbstractPDOTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/AbstractPDOTest.php new file mode 100644 index 00000000000..a2b9987b7e5 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/AbstractPDOTest.php @@ -0,0 +1,193 @@ +<?php + +namespace Sabre\DAV\PropertyStorage\Backend; + +use Sabre\DAV\PropFind; +use Sabre\DAV\PropPatch; +use Sabre\DAV\Xml\Property\Complex; +use Sabre\DAV\Xml\Property\Href; + +abstract class AbstractPDOTest extends \PHPUnit_Framework_TestCase { + + use \Sabre\DAV\DbTestHelperTrait; + + function getBackend() { + + $this->dropTables('propertystorage'); + $this->createSchema('propertystorage'); + + $pdo = $this->getPDO(); + + $pdo->exec("INSERT INTO propertystorage (path, name, valuetype, value) VALUES ('dir', '{DAV:}displayname', 1, 'Directory')"); + + return new PDO($this->getPDO()); + + } + + function testPropFind() { + + $backend = $this->getBackend(); + + $propFind = new PropFind('dir', ['{DAV:}displayname']); + $backend->propFind('dir', $propFind); + + $this->assertEquals('Directory', $propFind->get('{DAV:}displayname')); + + } + + function testPropFindNothingToDo() { + + $backend = $this->getBackend(); + + $propFind = new PropFind('dir', ['{DAV:}displayname']); + $propFind->set('{DAV:}displayname', 'foo'); + $backend->propFind('dir', $propFind); + + $this->assertEquals('foo', $propFind->get('{DAV:}displayname')); + + } + + /** + * @depends testPropFind + */ + function testPropPatchUpdate() { + + $backend = $this->getBackend(); + + $propPatch = new PropPatch(['{DAV:}displayname' => 'bar']); + $backend->propPatch('dir', $propPatch); + $propPatch->commit(); + + $propFind = new PropFind('dir', ['{DAV:}displayname']); + $backend->propFind('dir', $propFind); + + $this->assertEquals('bar', $propFind->get('{DAV:}displayname')); + + } + + /** + * @depends testPropPatchUpdate + */ + function testPropPatchComplex() { + + $backend = $this->getBackend(); + + $complex = new Complex('<foo xmlns="DAV:">somevalue</foo>'); + + $propPatch = new PropPatch(['{DAV:}complex' => $complex]); + $backend->propPatch('dir', $propPatch); + $propPatch->commit(); + + $propFind = new PropFind('dir', ['{DAV:}complex']); + $backend->propFind('dir', $propFind); + + $this->assertEquals($complex, $propFind->get('{DAV:}complex')); + + } + + + /** + * @depends testPropPatchComplex + */ + function testPropPatchCustom() { + + $backend = $this->getBackend(); + + $custom = new Href('/foo/bar/'); + + $propPatch = new PropPatch(['{DAV:}custom' => $custom]); + $backend->propPatch('dir', $propPatch); + $propPatch->commit(); + + $propFind = new PropFind('dir', ['{DAV:}custom']); + $backend->propFind('dir', $propFind); + + $this->assertEquals($custom, $propFind->get('{DAV:}custom')); + + } + + /** + * @depends testPropFind + */ + function testPropPatchRemove() { + + $backend = $this->getBackend(); + + $propPatch = new PropPatch(['{DAV:}displayname' => null]); + $backend->propPatch('dir', $propPatch); + $propPatch->commit(); + + $propFind = new PropFind('dir', ['{DAV:}displayname']); + $backend->propFind('dir', $propFind); + + $this->assertEquals(null, $propFind->get('{DAV:}displayname')); + + } + + /** + * @depends testPropFind + */ + function testDelete() { + + $backend = $this->getBackend(); + $backend->delete('dir'); + + $propFind = new PropFind('dir', ['{DAV:}displayname']); + $backend->propFind('dir', $propFind); + + $this->assertEquals(null, $propFind->get('{DAV:}displayname')); + + } + + /** + * @depends testPropFind + */ + function testMove() { + + $backend = $this->getBackend(); + // Creating a new child property. + $propPatch = new PropPatch(['{DAV:}displayname' => 'child']); + $backend->propPatch('dir/child', $propPatch); + $propPatch->commit(); + + $backend->move('dir', 'dir2'); + + // Old 'dir' + $propFind = new PropFind('dir', ['{DAV:}displayname']); + $backend->propFind('dir', $propFind); + $this->assertEquals(null, $propFind->get('{DAV:}displayname')); + + // Old 'dir/child' + $propFind = new PropFind('dir/child', ['{DAV:}displayname']); + $backend->propFind('dir/child', $propFind); + $this->assertEquals(null, $propFind->get('{DAV:}displayname')); + + // New 'dir2' + $propFind = new PropFind('dir2', ['{DAV:}displayname']); + $backend->propFind('dir2', $propFind); + $this->assertEquals('Directory', $propFind->get('{DAV:}displayname')); + + // New 'dir2/child' + $propFind = new PropFind('dir2/child', ['{DAV:}displayname']); + $backend->propFind('dir2/child', $propFind); + $this->assertEquals('child', $propFind->get('{DAV:}displayname')); + } + + /** + * @depends testPropFind + */ + function testDeepDelete() { + + $backend = $this->getBackend(); + $propPatch = new PropPatch(['{DAV:}displayname' => 'child']); + $backend->propPatch('dir/child', $propPatch); + $propPatch->commit(); + $backend->delete('dir'); + + $propFind = new PropFind('dir/child', ['{DAV:}displayname']); + $backend->propFind('dir/child', $propFind); + + $this->assertEquals(null, $propFind->get('{DAV:}displayname')); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/Mock.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/Mock.php new file mode 100644 index 00000000000..cf4c88fb85f --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/Mock.php @@ -0,0 +1,117 @@ +<?php + +namespace Sabre\DAV\PropertyStorage\Backend; + +use Sabre\DAV\PropFind; +use Sabre\DAV\PropPatch; + +class Mock implements BackendInterface { + + public $data = []; + + /** + * 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. + * + * @param string $path + * @param PropFind $propFind + * @return void + */ + function propFind($path, PropFind $propFind) { + + if (!isset($this->data[$path])) { + return; + } + + foreach ($this->data[$path] as $name => $value) { + $propFind->set($name, $value); + } + + } + + /** + * 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 + * @param PropPatch $propPatch + * @return void + */ + function propPatch($path, PropPatch $propPatch) { + + if (!isset($this->data[$path])) { + $this->data[$path] = []; + } + $propPatch->handleRemaining(function($properties) use ($path) { + + foreach ($properties as $propName => $propValue) { + + if (is_null($propValue)) { + unset($this->data[$path][$propName]); + } else { + $this->data[$path][$propName] = $propValue; + } + return true; + + } + + }); + + } + + /** + * This method is called after a node is deleted. + * + * This allows a backend to clean up all associated properties. + * + * @param string $path + * @return void + */ + function delete($path) { + + unset($this->data[$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 + * @return void + */ + function move($source, $destination) { + + foreach ($this->data as $path => $props) { + + if ($path === $source) { + $this->data[$destination] = $props; + unset($this->data[$path]); + continue; + } + + if (strpos($path, $source . '/') === 0) { + $this->data[$destination . substr($path, strlen($source) + 1)] = $props; + unset($this->data[$path]); + } + + } + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/PDOMysqlTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/PDOMysqlTest.php new file mode 100644 index 00000000000..b92b034df84 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/PDOMysqlTest.php @@ -0,0 +1,9 @@ +<?php + +namespace Sabre\DAV\PropertyStorage\Backend; + +class PDOMysqlTest extends AbstractPDOTest { + + public $driver = 'mysql'; + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/PDOPgSqlTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/PDOPgSqlTest.php new file mode 100644 index 00000000000..616c2e67a6c --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/PDOPgSqlTest.php @@ -0,0 +1,9 @@ +<?php + +namespace Sabre\DAV\PropertyStorage\Backend; + +class PDOPgSqlTest extends AbstractPDOTest { + + public $driver = 'pgsql'; + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/PDOSqliteTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/PDOSqliteTest.php new file mode 100644 index 00000000000..20a6a09e5ba --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropertyStorage/Backend/PDOSqliteTest.php @@ -0,0 +1,9 @@ +<?php + +namespace Sabre\DAV\PropertyStorage\Backend; + +class PDOSqliteTest extends AbstractPDOTest { + + public $driver = 'sqlite'; + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropertyStorage/PluginTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropertyStorage/PluginTest.php new file mode 100644 index 00000000000..130f1490f17 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/PropertyStorage/PluginTest.php @@ -0,0 +1,117 @@ +<?php + +namespace Sabre\DAV\PropertyStorage; + +class PluginTest extends \Sabre\DAVServerTest { + + protected $backend; + protected $plugin; + + protected $setupFiles = true; + + function setUp() { + + parent::setUp(); + $this->backend = new Backend\Mock(); + $this->plugin = new Plugin( + $this->backend + ); + + $this->server->addPlugin($this->plugin); + + } + + function testGetInfo() { + + $this->assertArrayHasKey( + 'name', + $this->plugin->getPluginInfo() + ); + + } + + function testSetProperty() { + + $this->server->updateProperties('', ['{DAV:}displayname' => 'hi']); + $this->assertEquals([ + '' => [ + '{DAV:}displayname' => 'hi', + ] + ], $this->backend->data); + + } + + /** + * @depends testSetProperty + */ + function testGetProperty() { + + $this->testSetProperty(); + $result = $this->server->getProperties('', ['{DAV:}displayname']); + + $this->assertEquals([ + '{DAV:}displayname' => 'hi', + ], $result); + + } + + /** + * @depends testSetProperty + */ + function testDeleteProperty() { + + $this->testSetProperty(); + $this->server->emit('afterUnbind', ['']); + $this->assertEquals([], $this->backend->data); + + } + + function testMove() { + + $this->server->tree->getNodeForPath('files')->createFile('source'); + $this->server->updateProperties('files/source', ['{DAV:}displayname' => 'hi']); + + $request = new \Sabre\HTTP\Request('MOVE', '/files/source', ['Destination' => '/files/dest']); + $this->assertHTTPStatus(201, $request); + + $result = $this->server->getProperties('/files/dest', ['{DAV:}displayname']); + + $this->assertEquals([ + '{DAV:}displayname' => 'hi', + ], $result); + + $this->server->tree->getNodeForPath('files')->createFile('source'); + $result = $this->server->getProperties('/files/source', ['{DAV:}displayname']); + + $this->assertEquals([], $result); + + } + + /** + * @depends testDeleteProperty + */ + function testSetPropertyInFilteredPath() { + + $this->plugin->pathFilter = function($path) { + + return false; + + }; + + $this->server->updateProperties('', ['{DAV:}displayname' => 'hi']); + $this->assertEquals([], $this->backend->data); + + } + + /** + * @depends testSetPropertyInFilteredPath + */ + function testGetPropertyInFilteredPath() { + + $this->testSetPropertyInFilteredPath(); + $result = $this->server->getProperties('', ['{DAV:}displayname']); + + $this->assertEquals([], $result); + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerEventsTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerEventsTest.php new file mode 100644 index 00000000000..42759647ab0 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerEventsTest.php @@ -0,0 +1,126 @@ +<?php + +namespace Sabre\DAV; + +use Sabre\HTTP; + +require_once 'Sabre/DAV/AbstractServer.php'; + +class ServerEventsTest extends AbstractServer { + + private $tempPath; + + private $exception; + + function testAfterBind() { + + $this->server->on('afterBind', [$this, 'afterBindHandler']); + $newPath = 'afterBind'; + + $this->tempPath = ''; + $this->server->createFile($newPath, 'body'); + $this->assertEquals($newPath, $this->tempPath); + + } + + function afterBindHandler($path) { + + $this->tempPath = $path; + + } + + function testAfterResponse() { + + $mock = $this->getMockBuilder('stdClass') + ->setMethods(['afterResponseCallback']) + ->getMock(); + $mock->expects($this->once())->method('afterResponseCallback'); + + $this->server->on('afterResponse', [$mock, 'afterResponseCallback']); + + $this->server->httpRequest = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_URI' => '/test.txt', + ]); + + $this->server->exec(); + + } + + function testBeforeBindCancel() { + + $this->server->on('beforeBind', [$this, 'beforeBindCancelHandler']); + $this->assertFalse($this->server->createFile('bla', 'body')); + + // Also testing put() + $req = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'PUT', + 'REQUEST_URI' => '/barbar', + ]); + + $this->server->httpRequest = $req; + $this->server->exec(); + + $this->assertEquals(500, $this->server->httpResponse->getStatus()); + + } + + function beforeBindCancelHandler($path) { + + return false; + + } + + function testException() { + + $this->server->on('exception', [$this, 'exceptionHandler']); + + $req = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'GET', + 'REQUEST_URI' => '/not/exisitng', + ]); + $this->server->httpRequest = $req; + $this->server->exec(); + + $this->assertInstanceOf('Sabre\\DAV\\Exception\\NotFound', $this->exception); + + } + + function exceptionHandler(Exception $exception) { + + $this->exception = $exception; + + } + + function testMethod() { + + $k = 1; + $this->server->on('method', function($request, $response) use (&$k) { + + $k += 1; + + return false; + + }); + $this->server->on('method', function($request, $response) use (&$k) { + + $k += 2; + + return false; + + }); + + try { + $this->server->invokeMethod( + new HTTP\Request('BLABLA', '/'), + new HTTP\Response(), + false + ); + } catch (Exception $e) {} + + // Fun fact, PHP 7.1 changes the order when sorting-by-callback. + $this->assertTrue($k >= 2 && $k <= 3); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerMKCOLTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerMKCOLTest.php new file mode 100644 index 00000000000..557eddbbcf6 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerMKCOLTest.php @@ -0,0 +1,366 @@ +<?php + +namespace Sabre\DAV; + +use Sabre\HTTP; + +class ServerMKCOLTest extends AbstractServer { + + function testMkcol() { + + $serverVars = [ + 'REQUEST_URI' => '/testcol', + 'REQUEST_METHOD' => 'MKCOL', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody(""); + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Length' => ['0'], + ], $this->response->getHeaders()); + + $this->assertEquals(201, $this->response->status); + $this->assertEquals('', $this->response->body); + $this->assertTrue(is_dir($this->tempDir . '/testcol')); + + } + + /** + * @depends testMkcol + */ + function testMKCOLUnknownBody() { + + $serverVars = [ + 'REQUEST_URI' => '/testcol', + 'REQUEST_METHOD' => 'MKCOL', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody("Hello"); + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + ], $this->response->getHeaders()); + + $this->assertEquals(415, $this->response->status); + + } + + /** + * @depends testMkcol + */ + function testMKCOLBrokenXML() { + + $serverVars = [ + 'REQUEST_URI' => '/testcol', + 'REQUEST_METHOD' => 'MKCOL', + 'HTTP_CONTENT_TYPE' => 'application/xml', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody("Hello"); + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + ], $this->response->getHeaders()); + + $this->assertEquals(400, $this->response->getStatus(), $this->response->getBodyAsString()); + + } + + /** + * @depends testMkcol + */ + function testMKCOLUnknownXML() { + + $serverVars = [ + 'REQUEST_URI' => '/testcol', + 'REQUEST_METHOD' => 'MKCOL', + 'HTTP_CONTENT_TYPE' => 'application/xml', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody('<?xml version="1.0"?><html></html>'); + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + ], $this->response->getHeaders()); + + $this->assertEquals(400, $this->response->getStatus()); + + } + + /** + * @depends testMkcol + */ + function testMKCOLNoResourceType() { + + $serverVars = [ + 'REQUEST_URI' => '/testcol', + 'REQUEST_METHOD' => 'MKCOL', + 'HTTP_CONTENT_TYPE' => 'application/xml', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody('<?xml version="1.0"?> +<mkcol xmlns="DAV:"> + <set> + <prop> + <displayname>Evert</displayname> + </prop> + </set> +</mkcol>'); + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + ], $this->response->getHeaders()); + + $this->assertEquals(400, $this->response->status, 'Wrong statuscode received. Full response body: ' . $this->response->body); + + } + + /** + * @depends testMkcol + */ + function testMKCOLIncorrectResourceType() { + + $serverVars = [ + 'REQUEST_URI' => '/testcol', + 'REQUEST_METHOD' => 'MKCOL', + 'HTTP_CONTENT_TYPE' => 'application/xml', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody('<?xml version="1.0"?> +<mkcol xmlns="DAV:"> + <set> + <prop> + <resourcetype><collection /><blabla /></resourcetype> + </prop> + </set> +</mkcol>'); + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + ], $this->response->getHeaders()); + + $this->assertEquals(403, $this->response->status, 'Wrong statuscode received. Full response body: ' . $this->response->body); + + } + + /** + * @depends testMKCOLIncorrectResourceType + */ + function testMKCOLSuccess() { + + $serverVars = [ + 'REQUEST_URI' => '/testcol', + 'REQUEST_METHOD' => 'MKCOL', + 'HTTP_CONTENT_TYPE' => 'application/xml', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody('<?xml version="1.0"?> +<mkcol xmlns="DAV:"> + <set> + <prop> + <resourcetype><collection /></resourcetype> + </prop> + </set> +</mkcol>'); + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Length' => ['0'], + ], $this->response->getHeaders()); + + $this->assertEquals(201, $this->response->status, 'Wrong statuscode received. Full response body: ' . $this->response->body); + + } + + /** + * @depends testMKCOLIncorrectResourceType + */ + function testMKCOLWhiteSpaceResourceType() { + + $serverVars = [ + 'REQUEST_URI' => '/testcol', + 'REQUEST_METHOD' => 'MKCOL', + 'HTTP_CONTENT_TYPE' => 'application/xml', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody('<?xml version="1.0"?> +<mkcol xmlns="DAV:"> + <set> + <prop> + <resourcetype> + <collection /> + </resourcetype> + </prop> + </set> +</mkcol>'); + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Length' => ['0'], + ], $this->response->getHeaders()); + + $this->assertEquals(201, $this->response->status, 'Wrong statuscode received. Full response body: ' . $this->response->body); + + } + + /** + * @depends testMKCOLIncorrectResourceType + */ + function testMKCOLNoParent() { + + $serverVars = [ + 'REQUEST_URI' => '/testnoparent/409me', + 'REQUEST_METHOD' => 'MKCOL', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody(''); + + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + ], $this->response->getHeaders()); + + $this->assertEquals(409, $this->response->status, 'Wrong statuscode received. Full response body: ' . $this->response->body); + + } + + /** + * @depends testMKCOLIncorrectResourceType + */ + function testMKCOLParentIsNoCollection() { + + $serverVars = [ + 'REQUEST_URI' => '/test.txt/409me', + 'REQUEST_METHOD' => 'MKCOL', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody(''); + + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + ], $this->response->getHeaders()); + + $this->assertEquals(409, $this->response->status, 'Wrong statuscode received. Full response body: ' . $this->response->body); + + } + + /** + * @depends testMKCOLIncorrectResourceType + */ + function testMKCOLAlreadyExists() { + + $serverVars = [ + 'REQUEST_URI' => '/test.txt', + 'REQUEST_METHOD' => 'MKCOL', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody(''); + + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + 'Allow' => ['OPTIONS, GET, HEAD, DELETE, PROPFIND, PUT, PROPPATCH, COPY, MOVE, REPORT'], + ], $this->response->getHeaders()); + + $this->assertEquals(405, $this->response->status, 'Wrong statuscode received. Full response body: ' . $this->response->body); + + } + + /** + * @depends testMKCOLSuccess + * @depends testMKCOLAlreadyExists + */ + function testMKCOLAndProps() { + + $request = new HTTP\Request( + 'MKCOL', + '/testcol', + ['Content-Type' => 'application/xml'] + ); + $request->setBody('<?xml version="1.0"?> +<mkcol xmlns="DAV:"> + <set> + <prop> + <resourcetype><collection /></resourcetype> + <displayname>my new collection</displayname> + </prop> + </set> +</mkcol>'); + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals(207, $this->response->status, 'Wrong statuscode received. Full response body: ' . $this->response->body); + + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + ], $this->response->getHeaders()); + + $responseBody = $this->response->getBodyAsString(); + + $expected = <<<XML +<?xml version="1.0"?> +<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns"> + <d:response> + <d:href>/testcol</d:href> + <d:propstat> + <d:prop> + <d:displayname /> + </d:prop> + <d:status>HTTP/1.1 403 Forbidden</d:status> + </d:propstat> + </d:response> +</d:multistatus> +XML; + + $this->assertXmlStringEqualsXmlString( + $expected, + $responseBody + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerPluginTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerPluginTest.php new file mode 100644 index 00000000000..fa67102cc43 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerPluginTest.php @@ -0,0 +1,108 @@ +<?php + +namespace Sabre\DAV; + +use Sabre\HTTP; + +require_once 'Sabre/DAV/AbstractServer.php'; +require_once 'Sabre/DAV/TestPlugin.php'; + +class ServerPluginTest extends AbstractServer { + + /** + * @var Sabre\DAV\TestPlugin + */ + protected $testPlugin; + + function setUp() { + + parent::setUp(); + + $testPlugin = new TestPlugin(); + $this->server->addPlugin($testPlugin); + $this->testPlugin = $testPlugin; + + } + + /** + */ + function testBaseClass() { + + $p = new ServerPluginMock(); + $this->assertEquals([], $p->getFeatures()); + $this->assertEquals([], $p->getHTTPMethods('')); + $this->assertEquals( + [ + 'name' => 'Sabre\DAV\ServerPluginMock', + 'description' => null, + 'link' => null + ], $p->getPluginInfo() + ); + + } + + function testOptions() { + + $serverVars = [ + 'REQUEST_URI' => '/', + 'REQUEST_METHOD' => 'OPTIONS', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals([ + 'DAV' => ['1, 3, extended-mkcol, drinking'], + 'MS-Author-Via' => ['DAV'], + 'Allow' => ['OPTIONS, GET, HEAD, DELETE, PROPFIND, PUT, PROPPATCH, COPY, MOVE, REPORT, BEER, WINE'], + 'Accept-Ranges' => ['bytes'], + 'Content-Length' => ['0'], + 'X-Sabre-Version' => [Version::VERSION], + ], $this->response->getHeaders()); + + $this->assertEquals(200, $this->response->status); + $this->assertEquals('', $this->response->body); + $this->assertEquals('OPTIONS', $this->testPlugin->beforeMethod); + + + } + + function testGetPlugin() { + + $this->assertEquals($this->testPlugin, $this->server->getPlugin(get_class($this->testPlugin))); + + } + + function testUnknownPlugin() { + + $this->assertNull($this->server->getPlugin('SomeRandomClassName')); + + } + + function testGetSupportedReportSet() { + + $this->assertEquals([], $this->testPlugin->getSupportedReportSet('/')); + + } + + function testGetPlugins() { + + $this->assertEquals( + [ + get_class($this->testPlugin) => $this->testPlugin, + 'core' => $this->server->getPlugin('core'), + ], + $this->server->getPlugins() + ); + + } + + +} + +class ServerPluginMock extends ServerPlugin { + + function initialize(Server $s) { } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerPreconditionTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerPreconditionTest.php new file mode 100644 index 00000000000..203cf26d9d4 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerPreconditionTest.php @@ -0,0 +1,344 @@ +<?php + +namespace Sabre\DAV; + +use Sabre\HTTP; + +require_once 'Sabre/HTTP/ResponseMock.php'; + +class ServerPreconditionsTest extends \PHPUnit_Framework_TestCase { + + /** + * @expectedException Sabre\DAV\Exception\PreconditionFailed + */ + function testIfMatchNoNode() { + + $root = new SimpleCollection('root', [new ServerPreconditionsNode()]); + $server = new Server($root); + $httpRequest = new HTTP\Request('GET', '/bar', ['If-Match' => '*']); + $httpResponse = new HTTP\Response(); + $server->checkPreconditions($httpRequest, $httpResponse); + + } + + /** + */ + function testIfMatchHasNode() { + + $root = new SimpleCollection('root', [new ServerPreconditionsNode()]); + $server = new Server($root); + $httpRequest = new HTTP\Request('GET', '/foo', ['If-Match' => '*']); + $httpResponse = new HTTP\Response(); + $this->assertTrue($server->checkPreconditions($httpRequest, $httpResponse)); + + } + + /** + * @expectedException Sabre\DAV\Exception\PreconditionFailed + */ + function testIfMatchWrongEtag() { + + $root = new SimpleCollection('root', [new ServerPreconditionsNode()]); + $server = new Server($root); + $httpRequest = new HTTP\Request('GET', '/foo', ['If-Match' => '1234']); + $httpResponse = new HTTP\Response(); + $server->checkPreconditions($httpRequest, $httpResponse); + + } + + /** + */ + function testIfMatchCorrectEtag() { + + $root = new SimpleCollection('root', [new ServerPreconditionsNode()]); + $server = new Server($root); + $httpRequest = new HTTP\Request('GET', '/foo', ['If-Match' => '"abc123"']); + $httpResponse = new HTTP\Response(); + $this->assertTrue($server->checkPreconditions($httpRequest, $httpResponse)); + + } + + /** + * Evolution sometimes uses \" instead of " for If-Match headers. + * + * @depends testIfMatchCorrectEtag + */ + function testIfMatchEvolutionEtag() { + + $root = new SimpleCollection('root', [new ServerPreconditionsNode()]); + $server = new Server($root); + $httpRequest = new HTTP\Request('GET', '/foo', ['If-Match' => '\\"abc123\\"']); + $httpResponse = new HTTP\Response(); + $this->assertTrue($server->checkPreconditions($httpRequest, $httpResponse)); + + } + + /** + */ + function testIfMatchMultiple() { + + $root = new SimpleCollection('root', [new ServerPreconditionsNode()]); + $server = new Server($root); + $httpRequest = new HTTP\Request('GET', '/foo', ['If-Match' => '"hellothere", "abc123"']); + $httpResponse = new HTTP\Response(); + $this->assertTrue($server->checkPreconditions($httpRequest, $httpResponse)); + + } + + /** + */ + function testIfNoneMatchNoNode() { + + $root = new SimpleCollection('root', [new ServerPreconditionsNode()]); + $server = new Server($root); + $httpRequest = new HTTP\Request('GET', '/bar', ['If-None-Match' => '*']); + $httpResponse = new HTTP\Response(); + $this->assertTrue($server->checkPreconditions($httpRequest, $httpResponse)); + + } + + /** + * @expectedException Sabre\DAV\Exception\PreconditionFailed + */ + function testIfNoneMatchHasNode() { + + $root = new SimpleCollection('root', [new ServerPreconditionsNode()]); + $server = new Server($root); + $httpRequest = new HTTP\Request('POST', '/foo', ['If-None-Match' => '*']); + $httpResponse = new HTTP\Response(); + $server->checkPreconditions($httpRequest, $httpResponse); + + } + + /** + */ + function testIfNoneMatchWrongEtag() { + + $root = new SimpleCollection('root', [new ServerPreconditionsNode()]); + $server = new Server($root); + $httpRequest = new HTTP\Request('POST', '/foo', ['If-None-Match' => '"1234"']); + $httpResponse = new HTTP\Response(); + $this->assertTrue($server->checkPreconditions($httpRequest, $httpResponse)); + + } + + /** + */ + function testIfNoneMatchWrongEtagMultiple() { + + $root = new SimpleCollection('root', [new ServerPreconditionsNode()]); + $server = new Server($root); + $httpRequest = new HTTP\Request('POST', '/foo', ['If-None-Match' => '"1234", "5678"']); + $httpResponse = new HTTP\Response(); + $this->assertTrue($server->checkPreconditions($httpRequest, $httpResponse)); + + } + + /** + * @expectedException Sabre\DAV\Exception\PreconditionFailed + */ + function testIfNoneMatchCorrectEtag() { + + $root = new SimpleCollection('root', [new ServerPreconditionsNode()]); + $server = new Server($root); + $httpRequest = new HTTP\Request('POST', '/foo', ['If-None-Match' => '"abc123"']); + $httpResponse = new HTTP\Response(); + $server->checkPreconditions($httpRequest, $httpResponse); + + } + + /** + * @expectedException Sabre\DAV\Exception\PreconditionFailed + */ + function testIfNoneMatchCorrectEtagMultiple() { + + $root = new SimpleCollection('root', [new ServerPreconditionsNode()]); + $server = new Server($root); + $httpRequest = new HTTP\Request('POST', '/foo', ['If-None-Match' => '"1234, "abc123"']); + $httpResponse = new HTTP\Response(); + $server->checkPreconditions($httpRequest, $httpResponse); + + } + + /** + */ + function testIfNoneMatchCorrectEtagAsGet() { + + $root = new SimpleCollection('root', [new ServerPreconditionsNode()]); + $server = new Server($root); + $httpRequest = new HTTP\Request('GET', '/foo', ['If-None-Match' => '"abc123"']); + $server->httpResponse = new HTTP\ResponseMock(); + + $this->assertFalse($server->checkPreconditions($httpRequest, $server->httpResponse)); + $this->assertEquals(304, $server->httpResponse->getStatus()); + $this->assertEquals(['ETag' => ['"abc123"']], $server->httpResponse->getHeaders()); + + } + + /** + * This was a test written for issue #515. + */ + function testNoneMatchCorrectEtagEnsureSapiSent() { + + $root = new SimpleCollection('root', [new ServerPreconditionsNode()]); + $server = new Server($root); + $server->sapi = new HTTP\SapiMock(); + HTTP\SapiMock::$sent = 0; + $httpRequest = new HTTP\Request('GET', '/foo', ['If-None-Match' => '"abc123"']); + $server->httpRequest = $httpRequest; + $server->httpResponse = new HTTP\ResponseMock(); + + $server->exec(); + + $this->assertFalse($server->checkPreconditions($httpRequest, $server->httpResponse)); + $this->assertEquals(304, $server->httpResponse->getStatus()); + $this->assertEquals([ + 'ETag' => ['"abc123"'], + 'X-Sabre-Version' => [Version::VERSION], + ], $server->httpResponse->getHeaders()); + $this->assertEquals(1, HTTP\SapiMock::$sent); + + } + + /** + */ + function testIfModifiedSinceUnModified() { + + $root = new SimpleCollection('root', [new ServerPreconditionsNode()]); + $server = new Server($root); + $httpRequest = HTTP\Sapi::createFromServerArray([ + 'HTTP_IF_MODIFIED_SINCE' => 'Sun, 06 Nov 1994 08:49:37 GMT', + 'REQUEST_URI' => '/foo' + ]); + $server->httpResponse = new HTTP\ResponseMock(); + $this->assertFalse($server->checkPreconditions($httpRequest, $server->httpResponse)); + + $this->assertEquals(304, $server->httpResponse->status); + $this->assertEquals([ + 'Last-Modified' => ['Sat, 06 Apr 1985 23:30:00 GMT'], + ], $server->httpResponse->getHeaders()); + + } + + + /** + */ + function testIfModifiedSinceModified() { + + $root = new SimpleCollection('root', [new ServerPreconditionsNode()]); + $server = new Server($root); + $httpRequest = HTTP\Sapi::createFromServerArray([ + 'HTTP_IF_MODIFIED_SINCE' => 'Tue, 06 Nov 1984 08:49:37 GMT', + 'REQUEST_URI' => '/foo' + ]); + + $httpResponse = new HTTP\ResponseMock(); + $this->assertTrue($server->checkPreconditions($httpRequest, $httpResponse)); + + } + + /** + */ + function testIfModifiedSinceInvalidDate() { + + $root = new SimpleCollection('root', [new ServerPreconditionsNode()]); + $server = new Server($root); + $httpRequest = HTTP\Sapi::createFromServerArray([ + 'HTTP_IF_MODIFIED_SINCE' => 'Your mother', + 'REQUEST_URI' => '/foo' + ]); + $httpResponse = new HTTP\ResponseMock(); + + // Invalid dates must be ignored, so this should return true + $this->assertTrue($server->checkPreconditions($httpRequest, $httpResponse)); + + } + + /** + */ + function testIfModifiedSinceInvalidDate2() { + + $root = new SimpleCollection('root', [new ServerPreconditionsNode()]); + $server = new Server($root); + $httpRequest = HTTP\Sapi::createFromServerArray([ + 'HTTP_IF_MODIFIED_SINCE' => 'Sun, 06 Nov 1994 08:49:37 EST', + 'REQUEST_URI' => '/foo' + ]); + $httpResponse = new HTTP\ResponseMock(); + $this->assertTrue($server->checkPreconditions($httpRequest, $httpResponse)); + + } + + + /** + */ + function testIfUnmodifiedSinceUnModified() { + + $root = new SimpleCollection('root', [new ServerPreconditionsNode()]); + $server = new Server($root); + $httpRequest = HTTP\Sapi::createFromServerArray([ + 'HTTP_IF_UNMODIFIED_SINCE' => 'Sun, 06 Nov 1994 08:49:37 GMT', + 'REQUEST_URI' => '/foo' + ]); + $httpResponse = new HTTP\Response(); + $this->assertTrue($server->checkPreconditions($httpRequest, $httpResponse)); + + } + + + /** + * @expectedException Sabre\DAV\Exception\PreconditionFailed + */ + function testIfUnmodifiedSinceModified() { + + $root = new SimpleCollection('root', [new ServerPreconditionsNode()]); + $server = new Server($root); + $httpRequest = HTTP\Sapi::createFromServerArray([ + 'HTTP_IF_UNMODIFIED_SINCE' => 'Tue, 06 Nov 1984 08:49:37 GMT', + 'REQUEST_URI' => '/foo' + ]); + $httpResponse = new HTTP\ResponseMock(); + $server->checkPreconditions($httpRequest, $httpResponse); + + } + + /** + */ + function testIfUnmodifiedSinceInvalidDate() { + + $root = new SimpleCollection('root', [new ServerPreconditionsNode()]); + $server = new Server($root); + $httpRequest = HTTP\Sapi::createFromServerArray([ + 'HTTP_IF_UNMODIFIED_SINCE' => 'Sun, 06 Nov 1984 08:49:37 CET', + 'REQUEST_URI' => '/foo' + ]); + $httpResponse = new HTTP\ResponseMock(); + $this->assertTrue($server->checkPreconditions($httpRequest, $httpResponse)); + + } + + +} + +class ServerPreconditionsNode extends File { + + function getETag() { + + return '"abc123"'; + + } + + function getLastModified() { + + /* my birthday & time, I believe */ + return strtotime('1985-04-07 01:30 +02:00'); + + } + + function getName() { + + return 'foo'; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerPropsInfiniteDepthTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerPropsInfiniteDepthTest.php new file mode 100644 index 00000000000..c968e72008a --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerPropsInfiniteDepthTest.php @@ -0,0 +1,163 @@ +<?php + +namespace Sabre\DAV; + +use Sabre\HTTP; + +require_once 'Sabre/DAV/AbstractServer.php'; + +class ServerPropsInfiniteDepthTest extends AbstractServer { + + protected function getRootNode() { + + return new FSExt\Directory(SABRE_TEMPDIR); + + } + + function setUp() { + + if (file_exists(SABRE_TEMPDIR . '../.sabredav')) unlink(SABRE_TEMPDIR . '../.sabredav'); + parent::setUp(); + file_put_contents(SABRE_TEMPDIR . '/test2.txt', 'Test contents2'); + mkdir(SABRE_TEMPDIR . '/col'); + mkdir(SABRE_TEMPDIR . '/col/col'); + file_put_contents(SABRE_TEMPDIR . 'col/col/test.txt', 'Test contents'); + $this->server->addPlugin(new Locks\Plugin(new Locks\Backend\File(SABRE_TEMPDIR . '/.locksdb'))); + $this->server->enablePropfindDepthInfinity = true; + + } + + function tearDown() { + + parent::tearDown(); + if (file_exists(SABRE_TEMPDIR . '../.locksdb')) unlink(SABRE_TEMPDIR . '../.locksdb'); + + } + + private function sendRequest($body) { + + $request = new HTTP\Request('PROPFIND', '/', ['Depth' => 'infinity']); + $request->setBody($body); + + $this->server->httpRequest = $request; + $this->server->exec(); + + } + + function testPropFindEmptyBody() { + + $this->sendRequest(""); + + $this->assertEquals(207, $this->response->status, 'Incorrect status received. Full response body: ' . $this->response->getBodyAsString()); + + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + 'DAV' => ['1, 3, extended-mkcol, 2'], + 'Vary' => ['Brief,Prefer'], + ], + $this->response->getHeaders() + ); + + $body = preg_replace("/xmlns(:[A-Za-z0-9_])?=(\"|\')DAV:(\"|\')/", "xmlns\\1=\"urn:DAV\"", $this->response->body); + $xml = simplexml_load_string($body); + $xml->registerXPathNamespace('d', 'urn:DAV'); + + list($data) = $xml->xpath('/d:multistatus/d:response/d:href'); + $this->assertEquals('/', (string)$data, 'href element should have been /'); + + $data = $xml->xpath('/d:multistatus/d:response/d:propstat/d:prop/d:resourcetype'); + // 8 resources are to be returned: /, col, col/col, col/col/test.txt, dir, dir/child.txt, test.txt and test2.txt + $this->assertEquals(8, count($data)); + + } + + function testSupportedLocks() { + + $xml = '<?xml version="1.0"?> +<d:propfind xmlns:d="DAV:"> + <d:prop> + <d:supportedlock /> + </d:prop> +</d:propfind>'; + + $this->sendRequest($xml); + + $body = $this->response->getBodyAsString(); + $this->assertEquals(207, $this->response->getStatus(), $body); + + $body = preg_replace("/xmlns(:[A-Za-z0-9_])?=(\"|\')DAV:(\"|\')/", "xmlns\\1=\"urn:DAV\"", $body); + $xml = simplexml_load_string($body); + $xml->registerXPathNamespace('d', 'urn:DAV'); + + $data = $xml->xpath('/d:multistatus/d:response/d:propstat/d:prop/d:supportedlock/d:lockentry'); + $this->assertEquals(16, count($data), 'We expected sixteen \'d:lockentry\' tags'); + + $data = $xml->xpath('/d:multistatus/d:response/d:propstat/d:prop/d:supportedlock/d:lockentry/d:lockscope'); + $this->assertEquals(16, count($data), 'We expected sixteen \'d:lockscope\' tags'); + + $data = $xml->xpath('/d:multistatus/d:response/d:propstat/d:prop/d:supportedlock/d:lockentry/d:locktype'); + $this->assertEquals(16, count($data), 'We expected sixteen \'d:locktype\' tags'); + + $data = $xml->xpath('/d:multistatus/d:response/d:propstat/d:prop/d:supportedlock/d:lockentry/d:lockscope/d:shared'); + $this->assertEquals(8, count($data), 'We expected eight \'d:shared\' tags'); + + $data = $xml->xpath('/d:multistatus/d:response/d:propstat/d:prop/d:supportedlock/d:lockentry/d:lockscope/d:exclusive'); + $this->assertEquals(8, count($data), 'We expected eight \'d:exclusive\' tags'); + + $data = $xml->xpath('/d:multistatus/d:response/d:propstat/d:prop/d:supportedlock/d:lockentry/d:locktype/d:write'); + $this->assertEquals(16, count($data), 'We expected sixteen \'d:write\' tags'); + } + + function testLockDiscovery() { + + $xml = '<?xml version="1.0"?> +<d:propfind xmlns:d="DAV:"> + <d:prop> + <d:lockdiscovery /> + </d:prop> +</d:propfind>'; + + $this->sendRequest($xml); + + $body = preg_replace("/xmlns(:[A-Za-z0-9_])?=(\"|\')DAV:(\"|\')/", "xmlns\\1=\"urn:DAV\"", $this->response->body); + $xml = simplexml_load_string($body); + $xml->registerXPathNamespace('d', 'urn:DAV'); + + $data = $xml->xpath('/d:multistatus/d:response/d:propstat/d:prop/d:lockdiscovery'); + $this->assertEquals(8, count($data), 'We expected eight \'d:lockdiscovery\' tags'); + + } + + function testUnknownProperty() { + + $xml = '<?xml version="1.0"?> +<d:propfind xmlns:d="DAV:"> + <d:prop> + <d:macaroni /> + </d:prop> +</d:propfind>'; + + $this->sendRequest($xml); + $body = preg_replace("/xmlns(:[A-Za-z0-9_])?=(\"|\')DAV:(\"|\')/", "xmlns\\1=\"urn:DAV\"", $this->response->body); + $xml = simplexml_load_string($body); + $xml->registerXPathNamespace('d', 'urn:DAV'); + $pathTests = [ + '/d:multistatus', + '/d:multistatus/d:response', + '/d:multistatus/d:response/d:propstat', + '/d:multistatus/d:response/d:propstat/d:status', + '/d:multistatus/d:response/d:propstat/d:prop', + '/d:multistatus/d:response/d:propstat/d:prop/d:macaroni', + ]; + foreach ($pathTests as $test) { + $this->assertTrue(count($xml->xpath($test)) == true, 'We expected the ' . $test . ' element to appear in the response, we got: ' . $body); + } + + $val = $xml->xpath('/d:multistatus/d:response/d:propstat/d:status'); + $this->assertEquals(8, count($val), $body); + $this->assertEquals('HTTP/1.1 404 Not Found', (string)$val[0]); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerPropsTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerPropsTest.php new file mode 100644 index 00000000000..253200be7cf --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerPropsTest.php @@ -0,0 +1,201 @@ +<?php + +namespace Sabre\DAV; + +use Sabre\HTTP; + +require_once 'Sabre/HTTP/ResponseMock.php'; +require_once 'Sabre/DAV/AbstractServer.php'; + +class ServerPropsTest extends AbstractServer { + + protected function getRootNode() { + + return new FSExt\Directory(SABRE_TEMPDIR); + + } + + function setUp() { + + if (file_exists(SABRE_TEMPDIR . '../.sabredav')) unlink(SABRE_TEMPDIR . '../.sabredav'); + parent::setUp(); + file_put_contents(SABRE_TEMPDIR . '/test2.txt', 'Test contents2'); + mkdir(SABRE_TEMPDIR . '/col'); + file_put_contents(SABRE_TEMPDIR . 'col/test.txt', 'Test contents'); + $this->server->addPlugin(new Locks\Plugin(new Locks\Backend\File(SABRE_TEMPDIR . '/.locksdb'))); + + } + + function tearDown() { + + parent::tearDown(); + if (file_exists(SABRE_TEMPDIR . '../.locksdb')) unlink(SABRE_TEMPDIR . '../.locksdb'); + + } + + private function sendRequest($body, $path = '/', $headers = ['Depth' => '0']) { + + $request = new HTTP\Request('PROPFIND', $path, $headers, $body); + + $this->server->httpRequest = $request; + $this->server->exec(); + + } + + function testPropFindEmptyBody() { + + $this->sendRequest(""); + $this->assertEquals(207, $this->response->status); + + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + 'DAV' => ['1, 3, extended-mkcol, 2'], + 'Vary' => ['Brief,Prefer'], + ], + $this->response->getHeaders() + ); + + $body = preg_replace("/xmlns(:[A-Za-z0-9_])?=(\"|\')DAV:(\"|\')/", "xmlns\\1=\"urn:DAV\"", $this->response->body); + $xml = simplexml_load_string($body); + $xml->registerXPathNamespace('d', 'urn:DAV'); + + list($data) = $xml->xpath('/d:multistatus/d:response/d:href'); + $this->assertEquals('/', (string)$data, 'href element should have been /'); + + $data = $xml->xpath('/d:multistatus/d:response/d:propstat/d:prop/d:resourcetype'); + $this->assertEquals(1, count($data)); + + } + + function testPropFindEmptyBodyFile() { + + $this->sendRequest("", '/test2.txt', []); + $this->assertEquals(207, $this->response->status); + + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + 'DAV' => ['1, 3, extended-mkcol, 2'], + 'Vary' => ['Brief,Prefer'], + ], + $this->response->getHeaders() + ); + + $body = preg_replace("/xmlns(:[A-Za-z0-9_])?=(\"|\')DAV:(\"|\')/", "xmlns\\1=\"urn:DAV\"", $this->response->body); + $xml = simplexml_load_string($body); + $xml->registerXPathNamespace('d', 'urn:DAV'); + + list($data) = $xml->xpath('/d:multistatus/d:response/d:href'); + $this->assertEquals('/test2.txt', (string)$data, 'href element should have been /test2.txt'); + + $data = $xml->xpath('/d:multistatus/d:response/d:propstat/d:prop/d:getcontentlength'); + $this->assertEquals(1, count($data)); + + } + + function testSupportedLocks() { + + $xml = '<?xml version="1.0"?> +<d:propfind xmlns:d="DAV:"> + <d:prop> + <d:supportedlock /> + </d:prop> +</d:propfind>'; + + $this->sendRequest($xml); + + $body = preg_replace("/xmlns(:[A-Za-z0-9_])?=(\"|\')DAV:(\"|\')/", "xmlns\\1=\"urn:DAV\"", $this->response->body); + $xml = simplexml_load_string($body); + $xml->registerXPathNamespace('d', 'urn:DAV'); + + $data = $xml->xpath('/d:multistatus/d:response/d:propstat/d:prop/d:supportedlock/d:lockentry'); + $this->assertEquals(2, count($data), 'We expected two \'d:lockentry\' tags'); + + $data = $xml->xpath('/d:multistatus/d:response/d:propstat/d:prop/d:supportedlock/d:lockentry/d:lockscope'); + $this->assertEquals(2, count($data), 'We expected two \'d:lockscope\' tags'); + + $data = $xml->xpath('/d:multistatus/d:response/d:propstat/d:prop/d:supportedlock/d:lockentry/d:locktype'); + $this->assertEquals(2, count($data), 'We expected two \'d:locktype\' tags'); + + $data = $xml->xpath('/d:multistatus/d:response/d:propstat/d:prop/d:supportedlock/d:lockentry/d:lockscope/d:shared'); + $this->assertEquals(1, count($data), 'We expected a \'d:shared\' tag'); + + $data = $xml->xpath('/d:multistatus/d:response/d:propstat/d:prop/d:supportedlock/d:lockentry/d:lockscope/d:exclusive'); + $this->assertEquals(1, count($data), 'We expected a \'d:exclusive\' tag'); + + $data = $xml->xpath('/d:multistatus/d:response/d:propstat/d:prop/d:supportedlock/d:lockentry/d:locktype/d:write'); + $this->assertEquals(2, count($data), 'We expected two \'d:write\' tags'); + } + + function testLockDiscovery() { + + $xml = '<?xml version="1.0"?> +<d:propfind xmlns:d="DAV:"> + <d:prop> + <d:lockdiscovery /> + </d:prop> +</d:propfind>'; + + $this->sendRequest($xml); + + $body = preg_replace("/xmlns(:[A-Za-z0-9_])?=(\"|\')DAV:(\"|\')/", "xmlns\\1=\"urn:DAV\"", $this->response->body); + $xml = simplexml_load_string($body); + $xml->registerXPathNamespace('d', 'urn:DAV'); + + $data = $xml->xpath('/d:multistatus/d:response/d:propstat/d:prop/d:lockdiscovery'); + $this->assertEquals(1, count($data), 'We expected a \'d:lockdiscovery\' tag'); + + } + + function testUnknownProperty() { + + $xml = '<?xml version="1.0"?> +<d:propfind xmlns:d="DAV:"> + <d:prop> + <d:macaroni /> + </d:prop> +</d:propfind>'; + + $this->sendRequest($xml); + $body = preg_replace("/xmlns(:[A-Za-z0-9_])?=(\"|\')DAV:(\"|\')/", "xmlns\\1=\"urn:DAV\"", $this->response->body); + $xml = simplexml_load_string($body); + $xml->registerXPathNamespace('d', 'urn:DAV'); + $pathTests = [ + '/d:multistatus', + '/d:multistatus/d:response', + '/d:multistatus/d:response/d:propstat', + '/d:multistatus/d:response/d:propstat/d:status', + '/d:multistatus/d:response/d:propstat/d:prop', + '/d:multistatus/d:response/d:propstat/d:prop/d:macaroni', + ]; + foreach ($pathTests as $test) { + $this->assertTrue(count($xml->xpath($test)) == true, 'We expected the ' . $test . ' element to appear in the response, we got: ' . $body); + } + + $val = $xml->xpath('/d:multistatus/d:response/d:propstat/d:status'); + $this->assertEquals(1, count($val), $body); + $this->assertEquals('HTTP/1.1 404 Not Found', (string)$val[0]); + + } + + function testParsePropPatchRequest() { + + $body = '<?xml version="1.0"?> +<d:propertyupdate xmlns:d="DAV:" xmlns:s="http://sabredav.org/NS/test"> + <d:set><d:prop><s:someprop>somevalue</s:someprop></d:prop></d:set> + <d:remove><d:prop><s:someprop2 /></d:prop></d:remove> + <d:set><d:prop><s:someprop3>removeme</s:someprop3></d:prop></d:set> + <d:remove><d:prop><s:someprop3 /></d:prop></d:remove> +</d:propertyupdate>'; + + $result = $this->server->xml->parse($body); + $this->assertEquals([ + '{http://sabredav.org/NS/test}someprop' => 'somevalue', + '{http://sabredav.org/NS/test}someprop2' => null, + '{http://sabredav.org/NS/test}someprop3' => null, + ], $result->properties); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerRangeTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerRangeTest.php new file mode 100644 index 00000000000..81224d687c3 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerRangeTest.php @@ -0,0 +1,262 @@ +<?php + +namespace Sabre\DAV; + +use DateTime; +use Sabre\HTTP; + +/** + * This file tests HTTP requests that use the Range: header. + * + * @copyright Copyright (C) fruux GmbH. (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class ServerRangeTest extends \Sabre\DAVServerTest { + + protected $setupFiles = true; + + /** + * We need this string a lot + */ + protected $lastModified; + + function setUp() { + + parent::setUp(); + $this->server->createFile('files/test.txt', 'Test contents'); + + $this->lastModified = HTTP\Util::toHTTPDate( + new DateTime('@' . $this->server->tree->getNodeForPath('files/test.txt')->getLastModified()) + ); + + $stream = popen('echo "Test contents"', 'r'); + $streamingFile = new Mock\StreamingFile( + 'no-seeking.txt', + $stream + ); + $streamingFile->setSize(12); + $this->server->tree->getNodeForPath('files')->addNode($streamingFile); + + } + + function testRange() { + + $request = new HTTP\Request('GET', '/files/test.txt', ['Range' => 'bytes=2-5']); + $response = $this->request($request); + + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/octet-stream'], + 'Content-Length' => [4], + 'Content-Range' => ['bytes 2-5/13'], + 'ETag' => ['"' . md5('Test contents') . '"'], + 'Last-Modified' => [$this->lastModified], + ], + $response->getHeaders() + ); + $this->assertEquals(206, $response->getStatus()); + $this->assertEquals('st c', $response->getBodyAsString()); + + } + + /** + * @depends testRange + */ + function testStartRange() { + + $request = new HTTP\Request('GET', '/files/test.txt', ['Range' => 'bytes=2-']); + $response = $this->request($request); + + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/octet-stream'], + 'Content-Length' => [11], + 'Content-Range' => ['bytes 2-12/13'], + 'ETag' => ['"' . md5('Test contents') . '"'], + 'Last-Modified' => [$this->lastModified], + ], + $response->getHeaders() + ); + + $this->assertEquals(206, $response->getStatus()); + $this->assertEquals('st contents', $response->getBodyAsString()); + + } + + /** + * @depends testRange + */ + function testEndRange() { + + $request = new HTTP\Request('GET', '/files/test.txt', ['Range' => 'bytes=-8']); + $response = $this->request($request); + + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/octet-stream'], + 'Content-Length' => [8], + 'Content-Range' => ['bytes 5-12/13'], + 'ETag' => ['"' . md5('Test contents') . '"'], + 'Last-Modified' => [$this->lastModified], + ], + $response->getHeaders() + ); + + $this->assertEquals(206, $response->getStatus()); + $this->assertEquals('contents', $response->getBodyAsString()); + + } + + /** + * @depends testRange + */ + function testTooHighRange() { + + $request = new HTTP\Request('GET', '/files/test.txt', ['Range' => 'bytes=100-200']); + $response = $this->request($request); + + $this->assertEquals(416, $response->getStatus()); + + } + + /** + * @depends testRange + */ + function testCrazyRange() { + + $request = new HTTP\Request('GET', '/files/test.txt', ['Range' => 'bytes=8-4']); + $response = $this->request($request); + + $this->assertEquals(416, $response->getStatus()); + + } + + function testNonSeekableStream() { + + $request = new HTTP\Request('GET', '/files/no-seeking.txt', ['Range' => 'bytes=2-5']); + $response = $this->request($request); + + $this->assertEquals(206, $response->getStatus(), $response); + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/octet-stream'], + 'Content-Length' => [4], + 'Content-Range' => ['bytes 2-5/12'], + // 'ETag' => ['"' . md5('Test contents') . '"'], + 'Last-Modified' => [$this->lastModified], + ], + $response->getHeaders() + ); + + $this->assertEquals('st c', $response->getBodyAsString()); + + } + + /** + * @depends testRange + */ + function testIfRangeEtag() { + + $request = new HTTP\Request('GET', '/files/test.txt', [ + 'Range' => 'bytes=2-5', + 'If-Range' => '"' . md5('Test contents') . '"', + ]); + $response = $this->request($request); + + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/octet-stream'], + 'Content-Length' => [4], + 'Content-Range' => ['bytes 2-5/13'], + 'ETag' => ['"' . md5('Test contents') . '"'], + 'Last-Modified' => [$this->lastModified], + ], + $response->getHeaders() + ); + + $this->assertEquals(206, $response->getStatus()); + $this->assertEquals('st c', $response->getBodyAsString()); + + } + + /** + * @depends testIfRangeEtag + */ + function testIfRangeEtagIncorrect() { + + $request = new HTTP\Request('GET', '/files/test.txt', [ + 'Range' => 'bytes=2-5', + 'If-Range' => '"foobar"', + ]); + $response = $this->request($request); + + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/octet-stream'], + 'Content-Length' => [13], + 'ETag' => ['"' . md5('Test contents') . '"'], + 'Last-Modified' => [$this->lastModified], + ], + $response->getHeaders() + ); + + $this->assertEquals(200, $response->getStatus()); + $this->assertEquals('Test contents', $response->getBodyAsString()); + + } + + /** + * @depends testIfRangeEtag + */ + function testIfRangeModificationDate() { + + $request = new HTTP\Request('GET', '/files/test.txt', [ + 'Range' => 'bytes=2-5', + 'If-Range' => 'tomorrow', + ]); + $response = $this->request($request); + + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/octet-stream'], + 'Content-Length' => [4], + 'Content-Range' => ['bytes 2-5/13'], + 'ETag' => ['"' . md5('Test contents') . '"'], + 'Last-Modified' => [$this->lastModified], + ], + $response->getHeaders() + ); + + $this->assertEquals(206, $response->getStatus()); + $this->assertEquals('st c', $response->getBodyAsString()); + + } + + /** + * @depends testIfRangeModificationDate + */ + function testIfRangeModificationDateModified() { + + $request = new HTTP\Request('GET', '/files/test.txt', [ + 'Range' => 'bytes=2-5', + 'If-Range' => '-2 years', + ]); + $response = $this->request($request); + + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/octet-stream'], + 'Content-Length' => [13], + 'ETag' => ['"' . md5('Test contents') . '"'], + 'Last-Modified' => [$this->lastModified], + ], + $response->getHeaders() + ); + + $this->assertEquals(200, $response->getStatus()); + $this->assertEquals('Test contents', $response->getBodyAsString()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerSimpleTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerSimpleTest.php new file mode 100644 index 00000000000..043179a0051 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerSimpleTest.php @@ -0,0 +1,475 @@ +<?php + +namespace Sabre\DAV; + +use Sabre\HTTP; + +class ServerSimpleTest extends AbstractServer{ + + function testConstructArray() { + + $nodes = [ + new SimpleCollection('hello') + ]; + + $server = new Server($nodes); + $this->assertEquals($nodes[0], $server->tree->getNodeForPath('hello')); + + } + + /** + * @expectedException Sabre\DAV\Exception + */ + function testConstructIncorrectObj() { + + $nodes = [ + new SimpleCollection('hello'), + new \STDClass(), + ]; + + $server = new Server($nodes); + + } + + /** + * @expectedException Sabre\DAV\Exception + */ + function testConstructInvalidArg() { + + $server = new Server(1); + + } + + function testOptions() { + + $request = new HTTP\Request('OPTIONS', '/'); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals([ + 'DAV' => ['1, 3, extended-mkcol'], + 'MS-Author-Via' => ['DAV'], + 'Allow' => ['OPTIONS, GET, HEAD, DELETE, PROPFIND, PUT, PROPPATCH, COPY, MOVE, REPORT'], + 'Accept-Ranges' => ['bytes'], + 'Content-Length' => ['0'], + 'X-Sabre-Version' => [Version::VERSION], + ], $this->response->getHeaders()); + + $this->assertEquals(200, $this->response->status); + $this->assertEquals('', $this->response->body); + + } + + function testOptionsUnmapped() { + + $request = new HTTP\Request('OPTIONS', '/unmapped'); + $this->server->httpRequest = $request; + + $this->server->exec(); + + $this->assertEquals([ + 'DAV' => ['1, 3, extended-mkcol'], + 'MS-Author-Via' => ['DAV'], + 'Allow' => ['OPTIONS, GET, HEAD, DELETE, PROPFIND, PUT, PROPPATCH, COPY, MOVE, REPORT, MKCOL'], + 'Accept-Ranges' => ['bytes'], + 'Content-Length' => ['0'], + 'X-Sabre-Version' => [Version::VERSION], + ], $this->response->getHeaders()); + + $this->assertEquals(200, $this->response->status); + $this->assertEquals('', $this->response->body); + + } + + function testNonExistantMethod() { + + $serverVars = [ + 'REQUEST_URI' => '/', + 'REQUEST_METHOD' => 'BLABLA', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + ], $this->response->getHeaders()); + + $this->assertEquals(501, $this->response->status); + + + } + + function testBaseUri() { + + $serverVars = [ + 'REQUEST_URI' => '/blabla/test.txt', + 'REQUEST_METHOD' => 'GET', + ]; + $filename = $this->tempDir . '/test.txt'; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $this->server->setBaseUri('/blabla/'); + $this->assertEquals('/blabla/', $this->server->getBaseUri()); + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/octet-stream'], + 'Content-Length' => [13], + 'Last-Modified' => [HTTP\Util::toHTTPDate(new \DateTime('@' . filemtime($filename)))], + 'ETag' => ['"' . sha1(fileinode($filename) . filesize($filename) . filemtime($filename)) . '"'], + ], + $this->response->getHeaders() + ); + + $this->assertEquals(200, $this->response->status); + $this->assertEquals('Test contents', stream_get_contents($this->response->body)); + + } + + function testBaseUriAddSlash() { + + $tests = [ + '/' => '/', + '/foo' => '/foo/', + '/foo/' => '/foo/', + '/foo/bar' => '/foo/bar/', + '/foo/bar/' => '/foo/bar/', + ]; + + foreach ($tests as $test => $result) { + $this->server->setBaseUri($test); + + $this->assertEquals($result, $this->server->getBaseUri()); + + } + + } + + function testCalculateUri() { + + $uris = [ + 'http://www.example.org/root/somepath', + '/root/somepath', + '/root/somepath/', + ]; + + $this->server->setBaseUri('/root/'); + + foreach ($uris as $uri) { + + $this->assertEquals('somepath', $this->server->calculateUri($uri)); + + } + + $this->server->setBaseUri('/root'); + + foreach ($uris as $uri) { + + $this->assertEquals('somepath', $this->server->calculateUri($uri)); + + } + + $this->assertEquals('', $this->server->calculateUri('/root')); + + } + + function testCalculateUriSpecialChars() { + + $uris = [ + 'http://www.example.org/root/%C3%A0fo%C3%B3', + '/root/%C3%A0fo%C3%B3', + '/root/%C3%A0fo%C3%B3/' + ]; + + $this->server->setBaseUri('/root/'); + + foreach ($uris as $uri) { + + $this->assertEquals("\xc3\xa0fo\xc3\xb3", $this->server->calculateUri($uri)); + + } + + $this->server->setBaseUri('/root'); + + foreach ($uris as $uri) { + + $this->assertEquals("\xc3\xa0fo\xc3\xb3", $this->server->calculateUri($uri)); + + } + + $this->server->setBaseUri('/'); + + foreach ($uris as $uri) { + + $this->assertEquals("root/\xc3\xa0fo\xc3\xb3", $this->server->calculateUri($uri)); + + } + + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + */ + function testCalculateUriBreakout() { + + $uri = '/path1/'; + + $this->server->setBaseUri('/path2/'); + $this->server->calculateUri($uri); + + } + + /** + */ + function testGuessBaseUri() { + + $serverVars = [ + 'REQUEST_URI' => '/index.php/root', + 'PATH_INFO' => '/root', + ]; + + $httpRequest = HTTP\Sapi::createFromServerArray($serverVars); + $server = new Server(); + $server->httpRequest = $httpRequest; + + $this->assertEquals('/index.php/', $server->guessBaseUri()); + + } + + /** + * @depends testGuessBaseUri + */ + function testGuessBaseUriPercentEncoding() { + + $serverVars = [ + 'REQUEST_URI' => '/index.php/dir/path2/path%20with%20spaces', + 'PATH_INFO' => '/dir/path2/path with spaces', + ]; + + $httpRequest = HTTP\Sapi::createFromServerArray($serverVars); + $server = new Server(); + $server->httpRequest = $httpRequest; + + $this->assertEquals('/index.php/', $server->guessBaseUri()); + + } + + /** + * @depends testGuessBaseUri + */ + /* + function testGuessBaseUriPercentEncoding2() { + + $this->markTestIncomplete('This behaviour is not yet implemented'); + $serverVars = [ + 'REQUEST_URI' => '/some%20directory+mixed/index.php/dir/path2/path%20with%20spaces', + 'PATH_INFO' => '/dir/path2/path with spaces', + ]; + + $httpRequest = HTTP\Sapi::createFromServerArray($serverVars); + $server = new Server(); + $server->httpRequest = $httpRequest; + + $this->assertEquals('/some%20directory+mixed/index.php/', $server->guessBaseUri()); + + }*/ + + function testGuessBaseUri2() { + + $serverVars = [ + 'REQUEST_URI' => '/index.php/root/', + 'PATH_INFO' => '/root/', + ]; + + $httpRequest = HTTP\Sapi::createFromServerArray($serverVars); + $server = new Server(); + $server->httpRequest = $httpRequest; + + $this->assertEquals('/index.php/', $server->guessBaseUri()); + + } + + function testGuessBaseUriNoPathInfo() { + + $serverVars = [ + 'REQUEST_URI' => '/index.php/root', + ]; + + $httpRequest = HTTP\Sapi::createFromServerArray($serverVars); + $server = new Server(); + $server->httpRequest = $httpRequest; + + $this->assertEquals('/', $server->guessBaseUri()); + + } + + function testGuessBaseUriNoPathInfo2() { + + $serverVars = [ + 'REQUEST_URI' => '/a/b/c/test.php', + ]; + + $httpRequest = HTTP\Sapi::createFromServerArray($serverVars); + $server = new Server(); + $server->httpRequest = $httpRequest; + + $this->assertEquals('/', $server->guessBaseUri()); + + } + + + /** + * @depends testGuessBaseUri + */ + function testGuessBaseUriQueryString() { + + $serverVars = [ + 'REQUEST_URI' => '/index.php/root?query_string=blabla', + 'PATH_INFO' => '/root', + ]; + + $httpRequest = HTTP\Sapi::createFromServerArray($serverVars); + $server = new Server(); + $server->httpRequest = $httpRequest; + + $this->assertEquals('/index.php/', $server->guessBaseUri()); + + } + + /** + * @depends testGuessBaseUri + * @expectedException \Sabre\DAV\Exception + */ + function testGuessBaseUriBadConfig() { + + $serverVars = [ + 'REQUEST_URI' => '/index.php/root/heyyy', + 'PATH_INFO' => '/root', + ]; + + $httpRequest = HTTP\Sapi::createFromServerArray($serverVars); + $server = new Server(); + $server->httpRequest = $httpRequest; + + $server->guessBaseUri(); + + } + + function testTriggerException() { + + $serverVars = [ + 'REQUEST_URI' => '/', + 'REQUEST_METHOD' => 'FOO', + ]; + + $httpRequest = HTTP\Sapi::createFromServerArray($serverVars); + $this->server->httpRequest = $httpRequest; + $this->server->on('beforeMethod', [$this, 'exceptionTrigger']); + $this->server->exec(); + + $this->assertEquals([ + 'Content-Type' => ['application/xml; charset=utf-8'], + ], $this->response->getHeaders()); + + $this->assertEquals(500, $this->response->status); + + } + + function exceptionTrigger($request, $response) { + + throw new Exception('Hola'); + + } + + function testReportNotFound() { + + $serverVars = [ + 'REQUEST_URI' => '/', + 'REQUEST_METHOD' => 'REPORT', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $this->server->httpRequest = ($request); + $this->server->httpRequest->setBody('<?xml version="1.0"?><bla:myreport xmlns:bla="http://www.rooftopsolutions.nl/NS"></bla:myreport>'); + $this->server->exec(); + + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + ], + $this->response->getHeaders() + ); + + $this->assertEquals(415, $this->response->status, 'We got an incorrect status back. Full response body follows: ' . $this->response->body); + + } + + function testReportIntercepted() { + + $serverVars = [ + 'REQUEST_URI' => '/', + 'REQUEST_METHOD' => 'REPORT', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $this->server->httpRequest = ($request); + $this->server->httpRequest->setBody('<?xml version="1.0"?><bla:myreport xmlns:bla="http://www.rooftopsolutions.nl/NS"></bla:myreport>'); + $this->server->on('report', [$this, 'reportHandler']); + $this->server->exec(); + + $this->assertEquals([ + 'X-Sabre-Version' => [Version::VERSION], + 'testheader' => ['testvalue'], + ], + $this->response->getHeaders() + ); + + $this->assertEquals(418, $this->response->status, 'We got an incorrect status back. Full response body follows: ' . $this->response->body); + + } + + function reportHandler($reportName, $result, $path) { + + if ($reportName == '{http://www.rooftopsolutions.nl/NS}myreport') { + $this->server->httpResponse->setStatus(418); + $this->server->httpResponse->setHeader('testheader', 'testvalue'); + return false; + } + else return; + + } + + function testGetPropertiesForChildren() { + + $result = $this->server->getPropertiesForChildren('', [ + '{DAV:}getcontentlength', + ]); + + $expected = [ + 'test.txt' => ['{DAV:}getcontentlength' => 13], + 'dir/' => [], + ]; + + $this->assertEquals($expected, $result); + + } + + /** + * There are certain cases where no HTTP status may be set. We need to + * intercept these and set it to a default error message. + */ + function testNoHTTPStatusSet() { + + $this->server->on('method:GET', function() { return false; }, 1); + $this->server->httpRequest = new HTTP\Request('GET', '/'); + $this->server->exec(); + $this->assertEquals(500, $this->response->getStatus()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerUpdatePropertiesTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerUpdatePropertiesTest.php new file mode 100644 index 00000000000..383f8e657ef --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/ServerUpdatePropertiesTest.php @@ -0,0 +1,102 @@ +<?php + +namespace Sabre\DAV; + +class ServerUpdatePropertiesTest extends \PHPUnit_Framework_TestCase { + + function testUpdatePropertiesFail() { + + $tree = [ + new SimpleCollection('foo'), + ]; + $server = new Server($tree); + + $result = $server->updateProperties('foo', [ + '{DAV:}foo' => 'bar' + ]); + + $expected = [ + '{DAV:}foo' => 403, + ]; + $this->assertEquals($expected, $result); + + } + + function testUpdatePropertiesProtected() { + + $tree = [ + new SimpleCollection('foo'), + ]; + $server = new Server($tree); + + $server->on('propPatch', function($path, PropPatch $propPatch) { + $propPatch->handleRemaining(function() { return true; }); + }); + $result = $server->updateProperties('foo', [ + '{DAV:}getetag' => 'bla', + '{DAV:}foo' => 'bar' + ]); + + $expected = [ + '{DAV:}getetag' => 403, + '{DAV:}foo' => 424, + ]; + $this->assertEquals($expected, $result); + + } + + function testUpdatePropertiesEventFail() { + + $tree = [ + new SimpleCollection('foo'), + ]; + $server = new Server($tree); + $server->on('propPatch', function($path, PropPatch $propPatch) { + $propPatch->setResultCode('{DAV:}foo', 404); + $propPatch->handleRemaining(function() { return true; }); + }); + + $result = $server->updateProperties('foo', [ + '{DAV:}foo' => 'bar', + '{DAV:}foo2' => 'bla', + ]); + + $expected = [ + '{DAV:}foo' => 404, + '{DAV:}foo2' => 424, + ]; + $this->assertEquals($expected, $result); + + } + + function testUpdatePropertiesEventSuccess() { + + $tree = [ + new SimpleCollection('foo'), + ]; + $server = new Server($tree); + $server->on('propPatch', function($path, PropPatch $propPatch) { + + $propPatch->handle(['{DAV:}foo', '{DAV:}foo2'], function() { + return [ + '{DAV:}foo' => 200, + '{DAV:}foo2' => 201, + ]; + }); + + }); + + $result = $server->updateProperties('foo', [ + '{DAV:}foo' => 'bar', + '{DAV:}foo2' => 'bla', + ]); + + $expected = [ + '{DAV:}foo' => 200, + '{DAV:}foo2' => 201, + ]; + $this->assertEquals($expected, $result); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Sharing/PluginTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Sharing/PluginTest.php new file mode 100644 index 00000000000..6aa09cac072 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Sharing/PluginTest.php @@ -0,0 +1,190 @@ +<?php + +namespace Sabre\DAV\Sharing; + +use Sabre\DAV\Mock; +use Sabre\DAV\Xml\Property; + +class PluginTest extends \Sabre\DAVServerTest { + + protected $setupSharing = true; + protected $setupACL = true; + protected $autoLogin = 'admin'; + + function setUpTree() { + + $this->tree[] = new Mock\SharedNode( + 'shareable', + Plugin::ACCESS_READWRITE + ); + + } + + function testFeatures() { + + $this->assertEquals( + ['resource-sharing'], + $this->sharingPlugin->getFeatures() + ); + + } + + function testProperties() { + + $result = $this->server->getPropertiesForPath( + 'shareable', + ['{DAV:}share-access'] + ); + + $expected = [ + [ + 200 => [ + '{DAV:}share-access' => new Property\ShareAccess(Plugin::ACCESS_READWRITE) + ], + 404 => [], + 'href' => 'shareable', + ] + ]; + + $this->assertEquals( + $expected, + $result + ); + + } + + function testGetPluginInfo() { + + $result = $this->sharingPlugin->getPluginInfo(); + $this->assertInternalType('array', $result); + $this->assertEquals('sharing', $result['name']); + + } + + function testHtmlActionsPanel() { + + $node = new \Sabre\DAV\Mock\Collection('foo'); + $html = ''; + + $this->assertNull( + $this->sharingPlugin->htmlActionsPanel($node, $html, 'foo/bar') + ); + + $this->assertEquals( + '', + $html + ); + + $node = new \Sabre\DAV\Mock\SharedNode('foo', \Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER); + $html = ''; + + $this->assertNull( + $this->sharingPlugin->htmlActionsPanel($node, $html, 'shareable') + ); + $this->assertContains( + 'Share this resource', + $html + ); + + } + + function testBrowserPostActionUnknownAction() { + + $this->assertNull($this->sharingPlugin->browserPostAction( + 'shareable', + 'foo', + [] + )); + + } + + function testBrowserPostActionSuccess() { + + $this->assertFalse($this->sharingPlugin->browserPostAction( + 'shareable', + 'share', + [ + 'access' => 'read', + 'href' => 'mailto:foo@example.org', + ] + )); + + $expected = [ + new \Sabre\DAV\Xml\Element\Sharee([ + 'href' => 'mailto:foo@example.org', + 'access' => \Sabre\DAV\Sharing\Plugin::ACCESS_READ, + 'inviteStatus' => \Sabre\DAV\Sharing\Plugin::INVITE_NORESPONSE, + ]) + ]; + $this->assertEquals( + $expected, + $this->tree[0]->getInvites() + ); + + } + + /** + * @expectedException \Sabre\DAV\Exception\BadRequest + */ + function testBrowserPostActionNoHref() { + + $this->sharingPlugin->browserPostAction( + 'shareable', + 'share', + [ + 'access' => 'read', + ] + ); + + } + + /** + * @expectedException \Sabre\DAV\Exception\BadRequest + */ + function testBrowserPostActionNoAccess() { + + $this->sharingPlugin->browserPostAction( + 'shareable', + 'share', + [ + 'href' => 'mailto:foo@example.org', + ] + ); + + } + + + /** + * @expectedException \Sabre\DAV\Exception\BadRequest + */ + function testBrowserPostActionBadAccess() { + + $this->sharingPlugin->browserPostAction( + 'shareable', + 'share', + [ + 'href' => 'mailto:foo@example.org', + 'access' => 'bleed', + ] + ); + + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + */ + function testBrowserPostActionAccessDenied() { + + $this->aclPlugin->setDefaultAcl([]); + $this->sharingPlugin->browserPostAction( + 'shareable', + 'share', + [ + 'access' => 'read', + 'href' => 'mailto:foo@example.org', + ] + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Sharing/ShareResourceTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Sharing/ShareResourceTest.php new file mode 100644 index 00000000000..959811166eb --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Sharing/ShareResourceTest.php @@ -0,0 +1,210 @@ +<?php + +namespace Sabre\DAV\Sharing; + +use Sabre\DAV\Mock; +use Sabre\DAV\Xml\Element\Sharee; +use Sabre\HTTP\Request; + +class ShareResourceTest extends \Sabre\DAVServerTest { + + protected $setupSharing = true; + protected $sharingNodeMock; + + function setUpTree() { + + $this->tree[] = $this->sharingNodeMock = new Mock\SharedNode( + 'shareable', + Plugin::ACCESS_SHAREDOWNER + ); + + } + + function testShareResource() { + + $body = <<<XML +<?xml version="1.0" encoding="utf-8" ?> +<D:share-resource xmlns:D="DAV:"> + <D:sharee> + <D:href>mailto:eric@example.com</D:href> + <D:prop> + <D:displayname>Eric York</D:displayname> + </D:prop> + <D:comment>Shared workspace</D:comment> + <D:share-access> + <D:read-write /> + </D:share-access> + </D:sharee> +</D:share-resource> +XML; + $request = new Request('POST', '/shareable', ['Content-Type' => 'application/davsharing+xml; charset="utf-8"'], $body); + + $response = $this->request($request); + $this->assertEquals(200, $response->getStatus(), (string)$response->getBodyAsString()); + + $expected = [ + new Sharee([ + 'href' => 'mailto:eric@example.com', + 'properties' => [ + '{DAV:}displayname' => 'Eric York', + ], + 'access' => Plugin::ACCESS_READWRITE, + 'comment' => 'Shared workspace', + 'inviteStatus' => \Sabre\DAV\Sharing\Plugin::INVITE_NORESPONSE, + ]) + ]; + + $this->assertEquals( + $expected, + $this->sharingNodeMock->getInvites() + ); + + } + + /** + * @depends testShareResource + */ + function testShareResourceRemoveAccess() { + + // First we just want to execute all the actions from the first + // test. + $this->testShareResource(); + + $body = <<<XML +<?xml version="1.0" encoding="utf-8" ?> +<D:share-resource xmlns:D="DAV:"> + <D:sharee> + <D:href>mailto:eric@example.com</D:href> + <D:share-access> + <D:no-access /> + </D:share-access> + </D:sharee> +</D:share-resource> +XML; + $request = new Request('POST', '/shareable', ['Content-Type' => 'application/davsharing+xml; charset="utf-8"'], $body); + + $response = $this->request($request); + $this->assertEquals(200, $response->getStatus(), (string)$response->getBodyAsString()); + + $expected = []; + + $this->assertEquals( + $expected, + $this->sharingNodeMock->getInvites() + ); + + + } + + /** + * @depends testShareResource + */ + function testShareResourceInviteProperty() { + + // First we just want to execute all the actions from the first + // test. + $this->testShareResource(); + + $body = <<<XML +<?xml version="1.0" encoding="utf-8" ?> +<D:propfind xmlns:D="DAV:"> + <D:prop> + <D:invite /> + <D:share-access /> + <D:share-resource-uri /> + </D:prop> +</D:propfind> +XML; + $request = new Request('PROPFIND', '/shareable', ['Content-Type' => 'application/xml'], $body); + $response = $this->request($request); + + $this->assertEquals(207, $response->getStatus()); + + $expected = <<<XML +<?xml version="1.0" encoding="utf-8" ?> +<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns"> + <d:response> + <d:href>/shareable</d:href> + <d:propstat> + <d:prop> + <d:invite> + <d:sharee> + <d:href>mailto:eric@example.com</d:href> + <d:prop> + <d:displayname>Eric York</d:displayname> + </d:prop> + <d:share-access><d:read-write /></d:share-access> + <d:invite-noresponse /> + </d:sharee> + </d:invite> + <d:share-access><d:shared-owner /></d:share-access> + <d:share-resource-uri><d:href>urn:example:bar</d:href></d:share-resource-uri> + </d:prop> + <d:status>HTTP/1.1 200 OK</d:status> + </d:propstat> + </d:response> +</d:multistatus> +XML; + + $this->assertXmlStringEqualsXmlString($expected, $response->getBodyAsString()); + + } + + function testShareResourceNotFound() { + + $body = <<<XML +<?xml version="1.0" encoding="utf-8" ?> +<D:share-resource xmlns:D="DAV:"> + <D:sharee> + <D:href>mailto:eric@example.com</D:href> + <D:prop> + <D:displayname>Eric York</D:displayname> + </D:prop> + <D:comment>Shared workspace</D:comment> + <D:share-access> + <D:read-write /> + </D:share-access> + </D:sharee> +</D:share-resource> +XML; + $request = new Request('POST', '/not-found', ['Content-Type' => 'application/davsharing+xml; charset="utf-8"'], $body); + + $response = $this->request($request, 404); + + } + + function testShareResourceNotISharedNode() { + + $body = <<<XML +<?xml version="1.0" encoding="utf-8" ?> +<D:share-resource xmlns:D="DAV:"> + <D:sharee> + <D:href>mailto:eric@example.com</D:href> + <D:prop> + <D:displayname>Eric York</D:displayname> + </D:prop> + <D:comment>Shared workspace</D:comment> + <D:share-access> + <D:read-write /> + </D:share-access> + </D:sharee> +</D:share-resource> +XML; + $request = new Request('POST', '/', ['Content-Type' => 'application/davsharing+xml; charset="utf-8"'], $body); + + $response = $this->request($request, 403); + + } + + function testShareResourceUnknownDoc() { + + $body = <<<XML +<?xml version="1.0" encoding="utf-8" ?> +<D:blablabla xmlns:D="DAV:" /> +XML; + $request = new Request('POST', '/shareable', ['Content-Type' => 'application/davsharing+xml; charset="utf-8"'], $body); + $response = $this->request($request, 400); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/SimpleFileTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/SimpleFileTest.php new file mode 100644 index 00000000000..15ccfaf9e36 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/SimpleFileTest.php @@ -0,0 +1,19 @@ +<?php + +namespace Sabre\DAV; + +class SimpleFileTest extends \PHPUnit_Framework_TestCase { + + function testAll() { + + $file = new SimpleFile('filename.txt', 'contents', 'text/plain'); + + $this->assertEquals('filename.txt', $file->getName()); + $this->assertEquals('contents', $file->get()); + $this->assertEquals(8, $file->getSize()); + $this->assertEquals('"' . sha1('contents') . '"', $file->getETag()); + $this->assertEquals('text/plain', $file->getContentType()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/StringUtilTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/StringUtilTest.php new file mode 100644 index 00000000000..e98fe904884 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/StringUtilTest.php @@ -0,0 +1,129 @@ +<?php + +namespace Sabre\DAV; + +class StringUtilTest extends \PHPUnit_Framework_TestCase { + + /** + * @param string $haystack + * @param string $needle + * @param string $collation + * @param string $matchType + * @param string $result + * @throws Exception\BadRequest + * + * @dataProvider dataset + */ + function testTextMatch($haystack, $needle, $collation, $matchType, $result) { + + $this->assertEquals($result, StringUtil::textMatch($haystack, $needle, $collation, $matchType)); + + } + + function dataset() { + + return [ + ['FOOBAR', 'FOO', 'i;octet', 'contains', true], + ['FOOBAR', 'foo', 'i;octet', 'contains', false], + ['FÖÖBAR', 'FÖÖ', 'i;octet', 'contains', true], + ['FÖÖBAR', 'föö', 'i;octet', 'contains', false], + ['FOOBAR', 'FOOBAR', 'i;octet', 'equals', true], + ['FOOBAR', 'fooBAR', 'i;octet', 'equals', false], + ['FOOBAR', 'FOO', 'i;octet', 'starts-with', true], + ['FOOBAR', 'foo', 'i;octet', 'starts-with', false], + ['FOOBAR', 'BAR', 'i;octet', 'starts-with', false], + ['FOOBAR', 'bar', 'i;octet', 'starts-with', false], + ['FOOBAR', 'FOO', 'i;octet', 'ends-with', false], + ['FOOBAR', 'foo', 'i;octet', 'ends-with', false], + ['FOOBAR', 'BAR', 'i;octet', 'ends-with', true], + ['FOOBAR', 'bar', 'i;octet', 'ends-with', false], + + ['FOOBAR', 'FOO', 'i;ascii-casemap', 'contains', true], + ['FOOBAR', 'foo', 'i;ascii-casemap', 'contains', true], + ['FÖÖBAR', 'FÖÖ', 'i;ascii-casemap', 'contains', true], + ['FÖÖBAR', 'föö', 'i;ascii-casemap', 'contains', false], + ['FOOBAR', 'FOOBAR', 'i;ascii-casemap', 'equals', true], + ['FOOBAR', 'fooBAR', 'i;ascii-casemap', 'equals', true], + ['FOOBAR', 'FOO', 'i;ascii-casemap', 'starts-with', true], + ['FOOBAR', 'foo', 'i;ascii-casemap', 'starts-with', true], + ['FOOBAR', 'BAR', 'i;ascii-casemap', 'starts-with', false], + ['FOOBAR', 'bar', 'i;ascii-casemap', 'starts-with', false], + ['FOOBAR', 'FOO', 'i;ascii-casemap', 'ends-with', false], + ['FOOBAR', 'foo', 'i;ascii-casemap', 'ends-with', false], + ['FOOBAR', 'BAR', 'i;ascii-casemap', 'ends-with', true], + ['FOOBAR', 'bar', 'i;ascii-casemap', 'ends-with', true], + + ['FOOBAR', 'FOO', 'i;unicode-casemap', 'contains', true], + ['FOOBAR', 'foo', 'i;unicode-casemap', 'contains', true], + ['FÖÖBAR', 'FÖÖ', 'i;unicode-casemap', 'contains', true], + ['FÖÖBAR', 'föö', 'i;unicode-casemap', 'contains', true], + ['FOOBAR', 'FOOBAR', 'i;unicode-casemap', 'equals', true], + ['FOOBAR', 'fooBAR', 'i;unicode-casemap', 'equals', true], + ['FOOBAR', 'FOO', 'i;unicode-casemap', 'starts-with', true], + ['FOOBAR', 'foo', 'i;unicode-casemap', 'starts-with', true], + ['FOOBAR', 'BAR', 'i;unicode-casemap', 'starts-with', false], + ['FOOBAR', 'bar', 'i;unicode-casemap', 'starts-with', false], + ['FOOBAR', 'FOO', 'i;unicode-casemap', 'ends-with', false], + ['FOOBAR', 'foo', 'i;unicode-casemap', 'ends-with', false], + ['FOOBAR', 'BAR', 'i;unicode-casemap', 'ends-with', true], + ['FOOBAR', 'bar', 'i;unicode-casemap', 'ends-with', true], + ]; + + } + + /** + * @expectedException Sabre\DAV\Exception\BadRequest + */ + function testBadCollation() { + + StringUtil::textMatch('foobar', 'foo', 'blabla', 'contains'); + + } + + + /** + * @expectedException Sabre\DAV\Exception\BadRequest + */ + function testBadMatchType() { + + StringUtil::textMatch('foobar', 'foo', 'i;octet', 'booh'); + + } + + function testEnsureUTF8_ascii() { + + $inputString = "harkema"; + $outputString = "harkema"; + + $this->assertEquals( + $outputString, + StringUtil::ensureUTF8($inputString) + ); + + } + + function testEnsureUTF8_latin1() { + + $inputString = "m\xfcnster"; + $outputString = "münster"; + + $this->assertEquals( + $outputString, + StringUtil::ensureUTF8($inputString) + ); + + } + + function testEnsureUTF8_utf8() { + + $inputString = "m\xc3\xbcnster"; + $outputString = "münster"; + + $this->assertEquals( + $outputString, + StringUtil::ensureUTF8($inputString) + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Sync/MockSyncCollection.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Sync/MockSyncCollection.php new file mode 100644 index 00000000000..aac1dee489b --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Sync/MockSyncCollection.php @@ -0,0 +1,169 @@ +<?php + +namespace Sabre\DAV\Sync; + +use Sabre\DAV; + +/** + * This mocks a ISyncCollection, for unittesting. + * + * This object behaves the same as SimpleCollection. Call addChange to update + * the 'changelog' that this class uses for the collection. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class MockSyncCollection extends DAV\SimpleCollection implements ISyncCollection { + + public $changeLog = []; + + public $token = null; + + /** + * 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 + */ + function getSyncToken() { + + // Will be 'null' in the first round, and will increment ever after. + return $this->token; + + } + + function addChange(array $added, array $modified, array $deleted) { + + $this->token++; + $this->changeLog[$this->token] = [ + 'added' => $added, + 'modified' => $modified, + 'deleted' => $deleted, + ]; + + } + + /** + * 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: + * + * array( + * 'syncToken' => 'The current synctoken', + * 'modified' => array( + * 'new.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 + */ + function getChanges($syncToken, $syncLevel, $limit = null) { + + // This is an initial sync + if (is_null($syncToken)) { + return [ + 'added' => array_map( + function($item) { + return $item->getName(); + }, $this->getChildren() + ), + 'modified' => [], + 'deleted' => [], + 'syncToken' => $this->getSyncToken(), + ]; + } + + if (!is_int($syncToken) && !ctype_digit($syncToken)) { + + return null; + + } + if (is_null($this->token)) return null; + + $added = []; + $modified = []; + $deleted = []; + + foreach ($this->changeLog as $token => $change) { + + if ($token > $syncToken) { + + $added = array_merge($added, $change['added']); + $modified = array_merge($modified, $change['modified']); + $deleted = array_merge($deleted, $change['deleted']); + + if ($limit) { + // If there's a limit, we may need to cut things off. + // This alghorithm is weird and stupid, but it works. + $left = $limit - (count($modified) + count($deleted)); + if ($left > 0) continue; + if ($left === 0) break; + if ($left < 0) { + $modified = array_slice($modified, 0, $left); + } + $left = $limit - (count($modified) + count($deleted)); + if ($left === 0) break; + if ($left < 0) { + $deleted = array_slice($deleted, 0, $left); + } + break; + + } + + } + + } + + return [ + 'syncToken' => $this->token, + 'added' => $added, + 'modified' => $modified, + 'deleted' => $deleted, + ]; + + } + + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Sync/PluginTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Sync/PluginTest.php new file mode 100644 index 00000000000..6bcec8b7535 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Sync/PluginTest.php @@ -0,0 +1,523 @@ +<?php + +namespace Sabre\DAV\Sync; + +use Sabre\DAV; +use Sabre\HTTP; + +require_once __DIR__ . '/MockSyncCollection.php'; + +class PluginTest extends \Sabre\DAVServerTest { + + protected $collection; + + function setUp() { + + parent::setUp(); + $this->server->addPlugin(new Plugin()); + + } + + function testGetInfo() { + + $this->assertArrayHasKey( + 'name', + (new Plugin())->getPluginInfo() + ); + + } + + function setUpTree() { + + $this->collection = + new MockSyncCollection('coll', [ + new DAV\SimpleFile('file1.txt', 'foo'), + new DAV\SimpleFile('file2.txt', 'bar'), + ]); + $this->tree = [ + $this->collection, + new DAV\SimpleCollection('normalcoll', []) + ]; + + } + + function testSupportedReportSet() { + + $result = $this->server->getProperties('/coll', ['{DAV:}supported-report-set']); + $this->assertFalse($result['{DAV:}supported-report-set']->has('{DAV:}sync-collection')); + + // Making a change + $this->collection->addChange(['file1.txt'], [], []); + + $result = $this->server->getProperties('/coll', ['{DAV:}supported-report-set']); + $this->assertTrue($result['{DAV:}supported-report-set']->has('{DAV:}sync-collection')); + + } + + function testGetSyncToken() { + + $result = $this->server->getProperties('/coll', ['{DAV:}sync-token']); + $this->assertFalse(isset($result['{DAV:}sync-token'])); + + // Making a change + $this->collection->addChange(['file1.txt'], [], []); + + $result = $this->server->getProperties('/coll', ['{DAV:}sync-token']); + $this->assertTrue(isset($result['{DAV:}sync-token'])); + + // non-sync-enabled collection + $this->collection->addChange(['file1.txt'], [], []); + + $result = $this->server->getProperties('/normalcoll', ['{DAV:}sync-token']); + $this->assertFalse(isset($result['{DAV:}sync-token'])); + } + + function testSyncInitialSyncCollection() { + + // Making a change + $this->collection->addChange(['file1.txt'], [], []); + + $request = new HTTP\Request('REPORT', '/coll/', ['Content-Type' => 'application/xml']); + + $body = <<<BLA +<?xml version="1.0" encoding="utf-8" ?> +<D:sync-collection xmlns:D="DAV:"> + <D:sync-token/> + <D:sync-level>1</D:sync-level> + <D:prop> + <D:getcontentlength/> + </D:prop> +</D:sync-collection> +BLA; + + $request->setBody($body); + + $response = $this->request($request); + + $this->assertEquals(207, $response->status, 'Full response body:' . $response->body); + + $multiStatus = $this->server->xml->parse($response->getBodyAsString()); + + // Checking the sync-token + $this->assertEquals( + 'http://sabre.io/ns/sync/1', + $multiStatus->getSyncToken() + ); + + $responses = $multiStatus->getResponses(); + $this->assertEquals(2, count($responses), 'We expected exactly 2 {DAV:}response'); + + $response = $responses[0]; + + $this->assertNull($response->getHttpStatus()); + $this->assertEquals('/coll/file1.txt', $response->getHref()); + $this->assertEquals([ + 200 => [ + '{DAV:}getcontentlength' => 3, + ] + ], $response->getResponseProperties()); + + $response = $responses[1]; + + $this->assertNull($response->getHttpStatus()); + $this->assertEquals('/coll/file2.txt', $response->getHref()); + $this->assertEquals([ + 200 => [ + '{DAV:}getcontentlength' => 3, + ] + ], $response->getResponseProperties()); + + } + + function testSubsequentSyncSyncCollection() { + + // Making a change + $this->collection->addChange(['file1.txt'], [], []); + // Making another change + $this->collection->addChange([], ['file2.txt'], ['file3.txt']); + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'REPORT', + 'REQUEST_URI' => '/coll/', + 'CONTENT_TYPE' => 'application/xml', + ]); + + $body = <<<BLA +<?xml version="1.0" encoding="utf-8" ?> +<D:sync-collection xmlns:D="DAV:"> + <D:sync-token>http://sabre.io/ns/sync/1</D:sync-token> + <D:sync-level>infinite</D:sync-level> + <D:prop> + <D:getcontentlength/> + </D:prop> +</D:sync-collection> +BLA; + + $request->setBody($body); + + $response = $this->request($request); + + $this->assertEquals(207, $response->status, 'Full response body:' . $response->body); + + $multiStatus = $this->server->xml->parse($response->getBodyAsString()); + + // Checking the sync-token + $this->assertEquals( + 'http://sabre.io/ns/sync/2', + $multiStatus->getSyncToken() + ); + + $responses = $multiStatus->getResponses(); + $this->assertEquals(2, count($responses), 'We expected exactly 2 {DAV:}response'); + + $response = $responses[0]; + + $this->assertNull($response->getHttpStatus()); + $this->assertEquals('/coll/file2.txt', $response->getHref()); + $this->assertEquals([ + 200 => [ + '{DAV:}getcontentlength' => 3, + ] + ], $response->getResponseProperties()); + + $response = $responses[1]; + + $this->assertEquals('404', $response->getHttpStatus()); + $this->assertEquals('/coll/file3.txt', $response->getHref()); + $this->assertEquals([], $response->getResponseProperties()); + + } + + function testSubsequentSyncSyncCollectionLimit() { + + // Making a change + $this->collection->addChange(['file1.txt'], [], []); + // Making another change + $this->collection->addChange([], ['file2.txt'], ['file3.txt']); + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'REPORT', + 'REQUEST_URI' => '/coll/', + 'CONTENT_TYPE' => 'application/xml', + ]); + + $body = <<<BLA +<?xml version="1.0" encoding="utf-8" ?> +<D:sync-collection xmlns:D="DAV:"> + <D:sync-token>http://sabre.io/ns/sync/1</D:sync-token> + <D:sync-level>infinite</D:sync-level> + <D:prop> + <D:getcontentlength/> + </D:prop> + <D:limit><D:nresults>1</D:nresults></D:limit> +</D:sync-collection> +BLA; + + $request->setBody($body); + + $response = $this->request($request); + + $this->assertEquals(207, $response->status, 'Full response body:' . $response->body); + + $multiStatus = $this->server->xml->parse( + $response->getBodyAsString() + ); + + // Checking the sync-token + $this->assertEquals( + 'http://sabre.io/ns/sync/2', + $multiStatus->getSyncToken() + ); + + $responses = $multiStatus->getResponses(); + $this->assertEquals(1, count($responses), 'We expected exactly 1 {DAV:}response'); + + $response = $responses[0]; + + $this->assertEquals('404', $response->getHttpStatus()); + $this->assertEquals('/coll/file3.txt', $response->getHref()); + $this->assertEquals([], $response->getResponseProperties()); + + } + + function testSubsequentSyncSyncCollectionDepthFallBack() { + + // Making a change + $this->collection->addChange(['file1.txt'], [], []); + // Making another change + $this->collection->addChange([], ['file2.txt'], ['file3.txt']); + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'REPORT', + 'REQUEST_URI' => '/coll/', + 'CONTENT_TYPE' => 'application/xml', + 'HTTP_DEPTH' => "1", + ]); + + $body = <<<BLA +<?xml version="1.0" encoding="utf-8" ?> +<D:sync-collection xmlns:D="DAV:"> + <D:sync-token>http://sabre.io/ns/sync/1</D:sync-token> + <D:prop> + <D:getcontentlength/> + </D:prop> +</D:sync-collection> +BLA; + + $request->setBody($body); + + $response = $this->request($request); + + $this->assertEquals(207, $response->status, 'Full response body:' . $response->body); + + $multiStatus = $this->server->xml->parse( + $response->getBodyAsString() + ); + + // Checking the sync-token + $this->assertEquals( + 'http://sabre.io/ns/sync/2', + $multiStatus->getSyncToken() + ); + + $responses = $multiStatus->getResponses(); + $this->assertEquals(2, count($responses), 'We expected exactly 2 {DAV:}response'); + + $response = $responses[0]; + + $this->assertNull($response->getHttpStatus()); + $this->assertEquals('/coll/file2.txt', $response->getHref()); + $this->assertEquals([ + 200 => [ + '{DAV:}getcontentlength' => 3, + ] + ], $response->getResponseProperties()); + + $response = $responses[1]; + + $this->assertEquals('404', $response->getHttpStatus()); + $this->assertEquals('/coll/file3.txt', $response->getHref()); + $this->assertEquals([], $response->getResponseProperties()); + + } + + function testSyncNoSyncInfo() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'REPORT', + 'REQUEST_URI' => '/coll/', + 'CONTENT_TYPE' => 'application/xml', + ]); + + $body = <<<BLA +<?xml version="1.0" encoding="utf-8" ?> +<D:sync-collection xmlns:D="DAV:"> + <D:sync-token/> + <D:sync-level>1</D:sync-level> + <D:prop> + <D:getcontentlength/> + </D:prop> +</D:sync-collection> +BLA; + + $request->setBody($body); + + $response = $this->request($request); + + // The default state has no sync-token, so this report should not yet + // be supported. + $this->assertEquals(415, $response->status, 'Full response body:' . $response->body); + + } + + function testSyncNoSyncCollection() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'REPORT', + 'REQUEST_URI' => '/normalcoll/', + 'CONTENT_TYPE' => 'application/xml', + ]); + + $body = <<<BLA +<?xml version="1.0" encoding="utf-8" ?> +<D:sync-collection xmlns:D="DAV:"> + <D:sync-token/> + <D:sync-level>1</D:sync-level> + <D:prop> + <D:getcontentlength/> + </D:prop> +</D:sync-collection> +BLA; + + $request->setBody($body); + + $response = $this->request($request); + + // The default state has no sync-token, so this report should not yet + // be supported. + $this->assertEquals(415, $response->status, 'Full response body:' . $response->body); + + } + + function testSyncInvalidToken() { + + $this->collection->addChange(['file1.txt'], [], []); + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'REPORT', + 'REQUEST_URI' => '/coll/', + 'CONTENT_TYPE' => 'application/xml', + ]); + + $body = <<<BLA +<?xml version="1.0" encoding="utf-8" ?> +<D:sync-collection xmlns:D="DAV:"> + <D:sync-token>http://sabre.io/ns/sync/invalid</D:sync-token> + <D:sync-level>1</D:sync-level> + <D:prop> + <D:getcontentlength/> + </D:prop> +</D:sync-collection> +BLA; + + $request->setBody($body); + + $response = $this->request($request); + + // The default state has no sync-token, so this report should not yet + // be supported. + $this->assertEquals(403, $response->status, 'Full response body:' . $response->body); + + } + function testSyncInvalidTokenNoPrefix() { + + $this->collection->addChange(['file1.txt'], [], []); + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'REPORT', + 'REQUEST_URI' => '/coll/', + 'CONTENT_TYPE' => 'application/xml', + ]); + + $body = <<<BLA +<?xml version="1.0" encoding="utf-8" ?> +<D:sync-collection xmlns:D="DAV:"> + <D:sync-token>invalid</D:sync-token> + <D:sync-level>1</D:sync-level> + <D:prop> + <D:getcontentlength/> + </D:prop> +</D:sync-collection> +BLA; + + $request->setBody($body); + + $response = $this->request($request); + + // The default state has no sync-token, so this report should not yet + // be supported. + $this->assertEquals(403, $response->status, 'Full response body:' . $response->body); + + } + + function testSyncNoSyncToken() { + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'REPORT', + 'REQUEST_URI' => '/coll/', + 'CONTENT_TYPE' => 'application/xml', + ]); + + $body = <<<BLA +<?xml version="1.0" encoding="utf-8" ?> +<D:sync-collection xmlns:D="DAV:"> + <D:sync-level>1</D:sync-level> + <D:prop> + <D:getcontentlength/> + </D:prop> +</D:sync-collection> +BLA; + + $request->setBody($body); + + $response = $this->request($request); + + // The default state has no sync-token, so this report should not yet + // be supported. + $this->assertEquals(400, $response->status, 'Full response body:' . $response->body); + + } + + function testSyncNoProp() { + + $this->collection->addChange(['file1.txt'], [], []); + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'REPORT', + 'REQUEST_URI' => '/coll/', + 'CONTENT_TYPE' => 'application/xml', + ]); + + $body = <<<BLA +<?xml version="1.0" encoding="utf-8" ?> +<D:sync-collection xmlns:D="DAV:"> + <D:sync-token /> + <D:sync-level>1</D:sync-level> +</D:sync-collection> +BLA; + + $request->setBody($body); + + $response = $this->request($request); + + // The default state has no sync-token, so this report should not yet + // be supported. + $this->assertEquals(400, $response->status, 'Full response body:' . $response->body); + + } + + function testIfConditions() { + + $this->collection->addChange(['file1.txt'], [], []); + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'DELETE', + 'REQUEST_URI' => '/coll/file1.txt', + 'HTTP_IF' => '</coll> (<http://sabre.io/ns/sync/1>)', + ]); + $response = $this->request($request); + + // If a 403 is thrown this works correctly. The file in questions + // doesn't allow itself to be deleted. + // If the If conditions failed, it would have been a 412 instead. + $this->assertEquals(403, $response->status); + + } + + function testIfConditionsNot() { + + $this->collection->addChange(['file1.txt'], [], []); + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'DELETE', + 'REQUEST_URI' => '/coll/file1.txt', + 'HTTP_IF' => '</coll> (Not <http://sabre.io/ns/sync/2>)', + ]); + $response = $this->request($request); + + // If a 403 is thrown this works correctly. The file in questions + // doesn't allow itself to be deleted. + // If the If conditions failed, it would have been a 412 instead. + $this->assertEquals(403, $response->status); + + } + + function testIfConditionsNoSyncToken() { + + $this->collection->addChange(['file1.txt'], [], []); + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'DELETE', + 'REQUEST_URI' => '/coll/file1.txt', + 'HTTP_IF' => '</coll> (<opaquelocktoken:foo>)', + ]); + $response = $this->request($request); + + $this->assertEquals(412, $response->status); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/SyncTokenPropertyTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/SyncTokenPropertyTest.php new file mode 100644 index 00000000000..ff139f78c41 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/SyncTokenPropertyTest.php @@ -0,0 +1,106 @@ +<?php + +namespace Sabre\DAV; + +class SyncTokenPropertyTest extends \Sabre\DAVServerTest { + + /** + * The assumption in these tests is that a PROPFIND is going on, and to + * fetch the sync-token, the event handler is just able to use the existing + * result. + * + * @param string $name + * @param mixed $value + * + * @dataProvider data + */ + function testAlreadyThere1($name, $value) { + + $propFind = new PropFind('foo', [ + '{http://calendarserver.org/ns/}getctag', + $name, + ]); + + $propFind->set($name, $value); + $corePlugin = new CorePlugin(); + $corePlugin->propFindLate($propFind, new SimpleCollection('hi')); + + $this->assertEquals("hello", $propFind->get('{http://calendarserver.org/ns/}getctag')); + + } + + /** + * In these test-cases, the plugin is forced to do a local propfind to + * fetch the items. + * + * @param string $name + * @param mixed $value + * + * @dataProvider data + */ + function testRefetch($name, $value) { + + $this->server->tree = new Tree( + new SimpleCollection('root', [ + new Mock\PropertiesCollection( + 'foo', + [], + [$name => $value] + ) + ]) + ); + $propFind = new PropFind('foo', [ + '{http://calendarserver.org/ns/}getctag', + $name, + ]); + + $corePlugin = $this->server->getPlugin('core'); + $corePlugin->propFindLate($propFind, new SimpleCollection('hi')); + + $this->assertEquals("hello", $propFind->get('{http://calendarserver.org/ns/}getctag')); + + } + + function testNoData() { + + $this->server->tree = new Tree( + new SimpleCollection('root', [ + new Mock\PropertiesCollection( + 'foo', + [], + [] + ) + ]) + ); + + $propFind = new PropFind('foo', [ + '{http://calendarserver.org/ns/}getctag', + ]); + + $corePlugin = $this->server->getPlugin('core'); + $corePlugin->propFindLate($propFind, new SimpleCollection('hi')); + + $this->assertNull($propFind->get('{http://calendarserver.org/ns/}getctag')); + + } + + function data() { + + return [ + [ + '{http://sabredav.org/ns}sync-token', + "hello" + ], + [ + '{DAV:}sync-token', + "hello" + ], + [ + '{DAV:}sync-token', + new Xml\Property\Href(Sync\Plugin::SYNCTOKEN_PREFIX . "hello", false) + ] + ]; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/TemporaryFileFilterTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/TemporaryFileFilterTest.php new file mode 100644 index 00000000000..6acd6b077b8 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/TemporaryFileFilterTest.php @@ -0,0 +1,199 @@ +<?php + +namespace Sabre\DAV; + +use Sabre\HTTP; + +class TemporaryFileFilterTest extends AbstractServer { + + function setUp() { + + parent::setUp(); + $plugin = new TemporaryFileFilterPlugin(SABRE_TEMPDIR . '/tff'); + $this->server->addPlugin($plugin); + + } + + function testPutNormal() { + + $request = new HTTP\Request('PUT', '/testput.txt', [], 'Testing new file'); + + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals('', $this->response->body); + $this->assertEquals(201, $this->response->status); + $this->assertEquals('0', $this->response->getHeader('Content-Length')); + + $this->assertEquals('Testing new file', file_get_contents(SABRE_TEMPDIR . '/testput.txt')); + + } + + function testPutTemp() { + + // mimicking an OS/X resource fork + $request = new HTTP\Request('PUT', '/._testput.txt', [], 'Testing new file'); + + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals('', $this->response->body); + $this->assertEquals(201, $this->response->status); + $this->assertEquals([ + 'X-Sabre-Temp' => ['true'], + ], $this->response->getHeaders()); + + $this->assertFalse(file_exists(SABRE_TEMPDIR . '/._testput.txt'), '._testput.txt should not exist in the regular file structure.'); + + } + + function testPutTempIfNoneMatch() { + + // mimicking an OS/X resource fork + $request = new HTTP\Request('PUT', '/._testput.txt', ['If-None-Match' => '*'], 'Testing new file'); + + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals('', $this->response->body); + $this->assertEquals(201, $this->response->status); + $this->assertEquals([ + 'X-Sabre-Temp' => ['true'], + ], $this->response->getHeaders()); + + $this->assertFalse(file_exists(SABRE_TEMPDIR . '/._testput.txt'), '._testput.txt should not exist in the regular file structure.'); + + + $this->server->exec(); + + $this->assertEquals(412, $this->response->status); + $this->assertEquals([ + 'X-Sabre-Temp' => ['true'], + 'Content-Type' => ['application/xml; charset=utf-8'], + ], $this->response->getHeaders()); + + } + + function testPutGet() { + + // mimicking an OS/X resource fork + $request = new HTTP\Request('PUT', '/._testput.txt', [], 'Testing new file'); + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals('', $this->response->body); + $this->assertEquals(201, $this->response->status); + $this->assertEquals([ + 'X-Sabre-Temp' => ['true'], + ], $this->response->getHeaders()); + + $request = new HTTP\Request('GET', '/._testput.txt'); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(200, $this->response->status); + $this->assertEquals([ + 'X-Sabre-Temp' => ['true'], + 'Content-Length' => [16], + 'Content-Type' => ['application/octet-stream'], + ], $this->response->getHeaders()); + + $this->assertEquals('Testing new file', stream_get_contents($this->response->body)); + + } + + function testLockNonExistant() { + + mkdir(SABRE_TEMPDIR . '/locksdir'); + $locksBackend = new Locks\Backend\File(SABRE_TEMPDIR . '/locks'); + $locksPlugin = new Locks\Plugin($locksBackend); + $this->server->addPlugin($locksPlugin); + + // mimicking an OS/X resource fork + $request = new HTTP\Request('LOCK', '/._testput.txt'); + $request->setBody('<?xml version="1.0"?> +<D:lockinfo xmlns:D="DAV:"> + <D:lockscope><D:exclusive/></D:lockscope> + <D:locktype><D:write/></D:locktype> + <D:owner> + <D:href>http://example.org/~ejw/contact.html</D:href> + </D:owner> +</D:lockinfo>'); + + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals(201, $this->response->status); + $this->assertEquals('application/xml; charset=utf-8', $this->response->getHeader('Content-Type')); + $this->assertTrue(preg_match('/^<opaquelocktoken:(.*)>$/', $this->response->getHeader('Lock-Token')) === 1, 'We did not get a valid Locktoken back (' . $this->response->getHeader('Lock-Token') . ')'); + $this->assertEquals('true', $this->response->getHeader('X-Sabre-Temp')); + + $this->assertFalse(file_exists(SABRE_TEMPDIR . '/._testlock.txt'), '._testlock.txt should not exist in the regular file structure.'); + + } + + function testPutDelete() { + + // mimicking an OS/X resource fork + $request = new HTTP\Request('PUT', '/._testput.txt', [], 'Testing new file'); + + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals('', $this->response->body); + $this->assertEquals(201, $this->response->status); + $this->assertEquals([ + 'X-Sabre-Temp' => ['true'], + ], $this->response->getHeaders()); + + $request = new HTTP\Request('DELETE', '/._testput.txt'); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals(204, $this->response->status, "Incorrect status code received. Full body:\n" . $this->response->body); + $this->assertEquals([ + 'X-Sabre-Temp' => ['true'], + ], $this->response->getHeaders()); + + $this->assertEquals('', $this->response->body); + + } + + function testPutPropfind() { + + // mimicking an OS/X resource fork + $request = new HTTP\Request('PUT', '/._testput.txt', [], 'Testing new file'); + $this->server->httpRequest = $request; + $this->server->exec(); + + $this->assertEquals('', $this->response->body); + $this->assertEquals(201, $this->response->status); + $this->assertEquals([ + 'X-Sabre-Temp' => ['true'], + ], $this->response->getHeaders()); + + $request = new HTTP\Request('PROPFIND', '/._testput.txt'); + + $this->server->httpRequest = ($request); + $this->server->exec(); + + $this->assertEquals(207, $this->response->status, 'Incorrect status code returned. Body: ' . $this->response->body); + $this->assertEquals([ + 'X-Sabre-Temp' => ['true'], + 'Content-Type' => ['application/xml; charset=utf-8'], + ], $this->response->getHeaders()); + + $body = preg_replace("/xmlns(:[A-Za-z0-9_])?=(\"|\')DAV:(\"|\')/", "xmlns\\1=\"urn:DAV\"", $this->response->body); + $xml = simplexml_load_string($body); + $xml->registerXPathNamespace('d', 'urn:DAV'); + + list($data) = $xml->xpath('/d:multistatus/d:response/d:href'); + $this->assertEquals('/._testput.txt', (string)$data, 'href element should have been /._testput.txt'); + + $data = $xml->xpath('/d:multistatus/d:response/d:propstat/d:prop/d:resourcetype'); + $this->assertEquals(1, count($data)); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/TestPlugin.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/TestPlugin.php new file mode 100644 index 00000000000..619ac03fd73 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/TestPlugin.php @@ -0,0 +1,37 @@ +<?php + +namespace Sabre\DAV; + +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +class TestPlugin extends ServerPlugin { + + public $beforeMethod; + + function getFeatures() { + + return ['drinking']; + + } + + function getHTTPMethods($uri) { + + return ['BEER','WINE']; + + } + + function initialize(Server $server) { + + $server->on('beforeMethod', [$this, 'beforeMethod']); + + } + + function beforeMethod(RequestInterface $request, ResponseInterface $response) { + + $this->beforeMethod = $request->getMethod(); + return true; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/TreeTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/TreeTest.php new file mode 100644 index 00000000000..e719e38d59d --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/TreeTest.php @@ -0,0 +1,242 @@ +<?php + +namespace Sabre\DAV; + +class TreeTest extends \PHPUnit_Framework_TestCase { + + function testNodeExists() { + + $tree = new TreeMock(); + + $this->assertTrue($tree->nodeExists('hi')); + $this->assertFalse($tree->nodeExists('hello')); + + } + + function testCopy() { + + $tree = new TreeMock(); + $tree->copy('hi', 'hi2'); + + $this->assertArrayHasKey('hi2', $tree->getNodeForPath('')->newDirectories); + $this->assertEquals('foobar', $tree->getNodeForPath('hi/file')->get()); + $this->assertEquals(['test1' => 'value'], $tree->getNodeForPath('hi/file')->getProperties([])); + + } + + function testMove() { + + $tree = new TreeMock(); + $tree->move('hi', 'hi2'); + + $this->assertEquals('hi2', $tree->getNodeForPath('hi')->getName()); + $this->assertTrue($tree->getNodeForPath('hi')->isRenamed); + + } + + function testDeepMove() { + + $tree = new TreeMock(); + $tree->move('hi/sub', 'hi2'); + + $this->assertArrayHasKey('hi2', $tree->getNodeForPath('')->newDirectories); + $this->assertTrue($tree->getNodeForPath('hi/sub')->isDeleted); + + } + + function testDelete() { + + $tree = new TreeMock(); + $tree->delete('hi'); + $this->assertTrue($tree->getNodeForPath('hi')->isDeleted); + + } + + function testGetChildren() { + + $tree = new TreeMock(); + $children = $tree->getChildren(''); + $this->assertEquals(2, count($children)); + $this->assertEquals('hi', $children[0]->getName()); + + } + + function testGetMultipleNodes() { + + $tree = new TreeMock(); + $result = $tree->getMultipleNodes(['hi/sub', 'hi/file']); + $this->assertArrayHasKey('hi/sub', $result); + $this->assertArrayHasKey('hi/file', $result); + + $this->assertEquals('sub', $result['hi/sub']->getName()); + $this->assertEquals('file', $result['hi/file']->getName()); + + } + function testGetMultipleNodes2() { + + $tree = new TreeMock(); + $result = $tree->getMultipleNodes(['multi/1', 'multi/2']); + $this->assertArrayHasKey('multi/1', $result); + $this->assertArrayHasKey('multi/2', $result); + + } + +} + +class TreeMock extends Tree { + + private $nodes = []; + + function __construct() { + + $file = new TreeFileTester('file'); + $file->properties = ['test1' => 'value']; + $file->data = 'foobar'; + + parent::__construct( + new TreeDirectoryTester('root', [ + new TreeDirectoryTester('hi', [ + new TreeDirectoryTester('sub'), + $file, + ]), + new TreeMultiGetTester('multi', [ + new TreeFileTester('1'), + new TreeFileTester('2'), + new TreeFileTester('3'), + ]) + ]) + ); + + } + +} + +class TreeDirectoryTester extends SimpleCollection { + + public $newDirectories = []; + public $newFiles = []; + public $isDeleted = false; + public $isRenamed = false; + + function createDirectory($name) { + + $this->newDirectories[$name] = true; + + } + + function createFile($name, $data = null) { + + $this->newFiles[$name] = $data; + + } + + function getChild($name) { + + if (isset($this->newDirectories[$name])) return new self($name); + if (isset($this->newFiles[$name])) return new TreeFileTester($name, $this->newFiles[$name]); + return parent::getChild($name); + + } + + function childExists($name) { + + return !!$this->getChild($name); + + } + + function delete() { + + $this->isDeleted = true; + + } + + function setName($name) { + + $this->isRenamed = true; + $this->name = $name; + + } + +} + +class TreeFileTester extends File implements IProperties { + + public $name; + public $data; + public $properties; + + function __construct($name, $data = null) { + + $this->name = $name; + if (is_null($data)) $data = 'bla'; + $this->data = $data; + + } + + function getName() { + + return $this->name; + + } + + function get() { + + return $this->data; + + } + + function getProperties($properties) { + + return $this->properties; + + } + + /** + * 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. + * + * @param PropPatch $propPatch + * @return void + */ + function propPatch(PropPatch $propPatch) { + + $this->properties = $propPatch->getMutations(); + $propPatch->setRemainingResultCode(200); + + } + +} + +class TreeMultiGetTester extends TreeDirectoryTester implements IMultiGet { + + /** + * 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 array $paths + * @return array + */ + function getMultipleChildren(array $paths) { + + $result = []; + foreach ($paths as $path) { + try { + $child = $this->getChild($path); + $result[] = $child; + } catch (Exception\NotFound $e) { + // Do nothing + } + } + + return $result; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/UUIDUtilTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/UUIDUtilTest.php new file mode 100644 index 00000000000..f005ecc75f0 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/UUIDUtilTest.php @@ -0,0 +1,25 @@ +<?php + +namespace Sabre\DAV; + +class UUIDUtilTest extends \PHPUnit_Framework_TestCase { + + function testValidateUUID() { + + $this->assertTrue( + UUIDUtil::validateUUID('11111111-2222-3333-4444-555555555555') + ); + $this->assertFalse( + UUIDUtil::validateUUID(' 11111111-2222-3333-4444-555555555555') + ); + $this->assertTrue( + UUIDUtil::validateUUID('ffffffff-2222-3333-4444-555555555555') + ); + $this->assertFalse( + UUIDUtil::validateUUID('fffffffg-2222-3333-4444-555555555555') + ); + + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Element/PropTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Element/PropTest.php new file mode 100644 index 00000000000..7cc10650cba --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Element/PropTest.php @@ -0,0 +1,154 @@ +<?php + +namespace Sabre\DAV\Xml\Element; + +use Sabre\DAV\Xml\Property\Complex; +use Sabre\DAV\Xml\Property\Href; +use Sabre\DAV\Xml\XmlTest; + +class PropTest extends XmlTest { + + function testDeserializeSimple() { + + $input = <<<XML +<?xml version="1.0"?> +<root xmlns="DAV:"> + <foo>bar</foo> +</root> +XML; + + $expected = [ + '{DAV:}foo' => 'bar', + ]; + + $this->assertDecodeProp($input, $expected); + + } + function testDeserializeEmpty() { + + $input = <<<XML +<?xml version="1.0"?> +<root xmlns="DAV:" /> +XML; + + $expected = [ + ]; + + $this->assertDecodeProp($input, $expected); + + } + function testDeserializeComplex() { + + $input = <<<XML +<?xml version="1.0"?> +<root xmlns="DAV:"> + <foo><no>yes</no></foo> +</root> +XML; + + $expected = [ + '{DAV:}foo' => new Complex('<no xmlns="DAV:">yes</no>') + ]; + + $this->assertDecodeProp($input, $expected); + + } + function testDeserializeCustom() { + + $input = <<<XML +<?xml version="1.0"?> +<root xmlns="DAV:"> + <foo><href>/hello</href></foo> +</root> +XML; + + $expected = [ + '{DAV:}foo' => new Href('/hello', false) + ]; + + $elementMap = [ + '{DAV:}foo' => 'Sabre\DAV\Xml\Property\Href' + ]; + + $this->assertDecodeProp($input, $expected, $elementMap); + + } + function testDeserializeCustomCallback() { + + $input = <<<XML +<?xml version="1.0"?> +<root xmlns="DAV:"> + <foo>blabla</foo> +</root> +XML; + + $expected = [ + '{DAV:}foo' => 'zim', + ]; + + $elementMap = [ + '{DAV:}foo' => function($reader) { + $reader->next(); + return 'zim'; + } + ]; + + $this->assertDecodeProp($input, $expected, $elementMap); + + } + + /** + * @expectedException \LogicException + */ + function testDeserializeCustomBad() { + + $input = <<<XML +<?xml version="1.0"?> +<root xmlns="DAV:"> + <foo>blabla</foo> +</root> +XML; + + $expected = []; + + $elementMap = [ + '{DAV:}foo' => 'idk?', + ]; + + $this->assertDecodeProp($input, $expected, $elementMap); + + } + + /** + * @expectedException \LogicException + */ + function testDeserializeCustomBadObj() { + + $input = <<<XML +<?xml version="1.0"?> +<root xmlns="DAV:"> + <foo>blabla</foo> +</root> +XML; + + $expected = []; + + $elementMap = [ + '{DAV:}foo' => new \StdClass(), + ]; + + $this->assertDecodeProp($input, $expected, $elementMap); + + } + + function assertDecodeProp($input, array $expected, array $elementMap = []) { + + $elementMap['{DAV:}root'] = 'Sabre\DAV\Xml\Element\Prop'; + + $result = $this->parse($input, $elementMap); + $this->assertInternalType('array', $result); + $this->assertEquals($expected, $result['value']); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Element/ResponseTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Element/ResponseTest.php new file mode 100644 index 00000000000..f19e7df7c0c --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Element/ResponseTest.php @@ -0,0 +1,313 @@ +<?php + +namespace Sabre\DAV\Xml\Element; + +use Sabre\DAV; + +class ResponseTest extends DAV\Xml\XmlTest { + + function testSimple() { + + $innerProps = [ + 200 => [ + '{DAV:}displayname' => 'my file', + ], + 404 => [ + '{DAV:}owner' => null, + ] + ]; + + $property = new Response('uri', $innerProps); + + $this->assertEquals('uri', $property->getHref()); + $this->assertEquals($innerProps, $property->getResponseProperties()); + + + } + + /** + * @depends testSimple + */ + function testSerialize() { + + $innerProps = [ + 200 => [ + '{DAV:}displayname' => 'my file', + ], + 404 => [ + '{DAV:}owner' => null, + ] + ]; + + $property = new Response('uri', $innerProps); + + $xml = $this->write(['{DAV:}root' => ['{DAV:}response' => $property]]); + + $this->assertXmlStringEqualsXmlString( +'<?xml version="1.0"?> +<d:root xmlns:d="DAV:"> + <d:response> + <d:href>/uri</d:href> + <d:propstat> + <d:prop> + <d:displayname>my file</d:displayname> + </d:prop> + <d:status>HTTP/1.1 200 OK</d:status> + </d:propstat> + <d:propstat> + <d:prop> + <d:owner/> + </d:prop> + <d:status>HTTP/1.1 404 Not Found</d:status> + </d:propstat> + </d:response> +</d:root> +', $xml); + + } + + /** + * This one is specifically for testing properties with no namespaces, which is legal xml + * + * @depends testSerialize + */ + function testSerializeEmptyNamespace() { + + $innerProps = [ + 200 => [ + '{}propertyname' => 'value', + ], + ]; + + $property = new Response('uri', $innerProps); + + $xml = $this->write(['{DAV:}root' => ['{DAV:}response' => $property]]); + + $this->assertEquals( +'<d:root xmlns:d="DAV:"> + <d:response> + <d:href>/uri</d:href> + <d:propstat> + <d:prop> + <propertyname xmlns="">value</propertyname> + </d:prop> + <d:status>HTTP/1.1 200 OK</d:status> + </d:propstat> + </d:response> +</d:root> +', $xml); + + } + + /** + * This one is specifically for testing properties with no namespaces, which is legal xml + * + * @depends testSerialize + */ + function testSerializeCustomNamespace() { + + $innerProps = [ + 200 => [ + '{http://sabredav.org/NS/example}propertyname' => 'value', + ], + ]; + + $property = new Response('uri', $innerProps); + $xml = $this->write(['{DAV:}root' => ['{DAV:}response' => $property]]); + + $this->assertXmlStringEqualsXmlString( +'<?xml version="1.0"?> +<d:root xmlns:d="DAV:"> + <d:response> + <d:href>/uri</d:href> + <d:propstat> + <d:prop> + <x1:propertyname xmlns:x1="http://sabredav.org/NS/example">value</x1:propertyname> + </d:prop> + <d:status>HTTP/1.1 200 OK</d:status> + </d:propstat> + </d:response> +</d:root>', $xml); + + } + + /** + * @depends testSerialize + */ + function testSerializeComplexProperty() { + + $innerProps = [ + 200 => [ + '{DAV:}link' => new DAV\Xml\Property\Href('http://sabredav.org/', false) + ], + ]; + + $property = new Response('uri', $innerProps); + $xml = $this->write(['{DAV:}root' => ['{DAV:}response' => $property]]); + + $this->assertXmlStringEqualsXmlString( +'<?xml version="1.0"?> +<d:root xmlns:d="DAV:"> + <d:response> + <d:href>/uri</d:href> + <d:propstat> + <d:prop> + <d:link><d:href>http://sabredav.org/</d:href></d:link> + </d:prop> + <d:status>HTTP/1.1 200 OK</d:status> + </d:propstat> + </d:response> +</d:root> +', $xml); + + } + + /** + * @depends testSerialize + * @expectedException \InvalidArgumentException + */ + function testSerializeBreak() { + + $innerProps = [ + 200 => [ + '{DAV:}link' => new \STDClass() + ], + ]; + + $property = new Response('uri', $innerProps); + $this->write(['{DAV:}root' => ['{DAV:}response' => $property]]); + + } + + function testDeserializeComplexProperty() { + + $xml = '<?xml version="1.0"?> +<d:response xmlns:d="DAV:"> + <d:href>/uri</d:href> + <d:propstat> + <d:prop> + <d:foo>hello</d:foo> + </d:prop> + <d:status>HTTP/1.1 200 OK</d:status> + </d:propstat> +</d:response> +'; + + $result = $this->parse($xml, [ + '{DAV:}response' => 'Sabre\DAV\Xml\Element\Response', + '{DAV:}foo' => function($reader) { + + $reader->next(); + return 'world'; + }, + ]); + $this->assertEquals( + new Response('/uri', [ + '200' => [ + '{DAV:}foo' => 'world', + ] + ]), + $result['value'] + ); + + } + + /** + * @depends testSimple + */ + function testSerializeUrlencoding() { + + $innerProps = [ + 200 => [ + '{DAV:}displayname' => 'my file', + ], + ]; + + $property = new Response('space here', $innerProps); + + $xml = $this->write(['{DAV:}root' => ['{DAV:}response' => $property]]); + + $this->assertXmlStringEqualsXmlString( +'<?xml version="1.0"?> +<d:root xmlns:d="DAV:"> + <d:response> + <d:href>/space%20here</d:href> + <d:propstat> + <d:prop> + <d:displayname>my file</d:displayname> + </d:prop> + <d:status>HTTP/1.1 200 OK</d:status> + </d:propstat> + </d:response> +</d:root> +', $xml); + + } + + /** + * @depends testSerialize + * + * The WebDAV spec _requires_ at least one DAV:propstat to appear for + * every DAV:response. 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. + */ + function testSerializeNoProperties() { + + $innerProps = []; + + $property = new Response('uri', $innerProps); + $xml = $this->write(['{DAV:}root' => ['{DAV:}response' => $property]]); + + $this->assertXmlStringEqualsXmlString( +'<?xml version="1.0"?> +<d:root xmlns:d="DAV:"> + <d:response> + <d:href>/uri</d:href> + <d:propstat> + <d:prop /> + <d:status>HTTP/1.1 418 I\'m a teapot</d:status> + </d:propstat> + </d:response> +</d:root> +', $xml); + + } + + /** + * In the case of {DAV:}prop, a deserializer should never get called, if + * the property element is empty. + */ + function testDeserializeComplexPropertyEmpty() { + + $xml = '<?xml version="1.0"?> +<d:response xmlns:d="DAV:"> + <d:href>/uri</d:href> + <d:propstat> + <d:prop> + <d:foo /> + </d:prop> + <d:status>HTTP/1.1 404 Not Found</d:status> + </d:propstat> +</d:response> +'; + + $result = $this->parse($xml, [ + '{DAV:}response' => 'Sabre\DAV\Xml\Element\Response', + '{DAV:}foo' => function($reader) { + throw new \LogicException('This should never happen'); + }, + ]); + $this->assertEquals( + new Response('/uri', [ + '404' => [ + '{DAV:}foo' => null + ] + ]), + $result['value'] + ); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Element/ShareeTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Element/ShareeTest.php new file mode 100644 index 00000000000..3704d878244 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Element/ShareeTest.php @@ -0,0 +1,98 @@ +<?php + +namespace Sabre\DAV\Xml\Element; + +use Sabre\DAV\Sharing\Plugin; +use Sabre\DAV\Xml\XmlTest; + +class ShareeTest extends XmlTest { + + /** + * @expectedException \InvalidArgumentException + */ + function testShareeUnknownPropertyInConstructor() { + + new Sharee(['foo' => 'bar']); + + } + + function testDeserialize() { + + $xml = <<<XML +<?xml version="1.0" encoding="utf-8" ?> +<D:sharee xmlns:D="DAV:"> + <D:href>mailto:eric@example.com</D:href> + <D:prop> + <D:displayname>Eric York</D:displayname> + </D:prop> + <D:comment>Shared workspace</D:comment> + <D:share-access> + <D:read-write /> + </D:share-access> +</D:sharee> +XML; + + $result = $this->parse($xml, [ + '{DAV:}sharee' => 'Sabre\\DAV\\Xml\\Element\\Sharee' + ]); + + $expected = new Sharee([ + 'href' => 'mailto:eric@example.com', + 'properties' => ['{DAV:}displayname' => 'Eric York'], + 'comment' => 'Shared workspace', + 'access' => Plugin::ACCESS_READWRITE, + ]); + $this->assertEquals( + $expected, + $result['value'] + ); + + } + + /** + * @expectedException \Sabre\DAV\Exception\BadRequest + */ + function testDeserializeNoHref() { + + $xml = <<<XML +<?xml version="1.0" encoding="utf-8" ?> +<D:sharee xmlns:D="DAV:"> + <D:prop> + <D:displayname>Eric York</D:displayname> + </D:prop> + <D:comment>Shared workspace</D:comment> + <D:share-access> + <D:read-write /> + </D:share-access> +</D:sharee> +XML; + + $this->parse($xml, [ + '{DAV:}sharee' => 'Sabre\\DAV\\Xml\\Element\\Sharee' + ]); + + } + + + /** + * @expectedException \Sabre\DAV\Exception\BadRequest + */ + function testDeserializeNoShareeAccess() { + + $xml = <<<XML +<?xml version="1.0" encoding="utf-8" ?> +<D:sharee xmlns:D="DAV:"> + <D:href>mailto:eric@example.com</D:href> + <D:prop> + <D:displayname>Eric York</D:displayname> + </D:prop> + <D:comment>Shared workspace</D:comment> +</D:sharee> +XML; + + $this->parse($xml, [ + '{DAV:}sharee' => 'Sabre\\DAV\\Xml\\Element\\Sharee' + ]); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/HrefTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/HrefTest.php new file mode 100644 index 00000000000..bf58853371e --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/HrefTest.php @@ -0,0 +1,109 @@ +<?php + +namespace Sabre\DAV\Xml\Property; + +use Sabre\DAV; +use Sabre\DAV\Browser\HtmlOutputHelper; +use Sabre\DAV\Xml\XmlTest; + +class HrefTest extends XmlTest { + + function testConstruct() { + + $href = new Href('path'); + $this->assertEquals('path', $href->getHref()); + + } + + function testSerialize() { + + $href = new Href('path'); + $this->assertEquals('path', $href->getHref()); + + $this->contextUri = '/bla/'; + + $xml = $this->write(['{DAV:}anything' => $href]); + + $this->assertXmlStringEqualsXmlString( +'<?xml version="1.0"?> +<d:anything xmlns:d="DAV:"><d:href>/bla/path</d:href></d:anything> +', $xml); + + } + + function testUnserialize() { + + $xml = '<?xml version="1.0"?> +<d:anything xmlns:d="DAV:"><d:href>/bla/path</d:href></d:anything> +'; + + $result = $this->parse($xml, ['{DAV:}anything' => 'Sabre\\DAV\\Xml\\Property\\Href']); + + $href = $result['value']; + + $this->assertInstanceOf('Sabre\\DAV\\Xml\\Property\\Href', $href); + + $this->assertEquals('/bla/path', $href->getHref()); + + } + + function testUnserializeIncompatible() { + + $xml = '<?xml version="1.0"?> +<d:anything xmlns:d="DAV:"><d:href2>/bla/path</d:href2></d:anything> +'; + $result = $this->parse($xml, ['{DAV:}anything' => 'Sabre\\DAV\\Xml\\Property\\Href']); + $href = $result['value']; + $this->assertNull($href); + + } + function testUnserializeEmpty() { + + $xml = '<?xml version="1.0"?> +<d:anything xmlns:d="DAV:"></d:anything> +'; + $result = $this->parse($xml, ['{DAV:}anything' => 'Sabre\\DAV\\Xml\\Property\\Href']); + $href = $result['value']; + $this->assertNull($href); + + } + + /** + * This method tests if hrefs containing & are correctly encoded. + */ + function testSerializeEntity() { + + $href = new Href('http://example.org/?a&b', false); + $this->assertEquals('http://example.org/?a&b', $href->getHref()); + + $xml = $this->write(['{DAV:}anything' => $href]); + + $this->assertXmlStringEqualsXmlString( +'<?xml version="1.0"?> +<d:anything xmlns:d="DAV:"><d:href>http://example.org/?a&b</d:href></d:anything> +', $xml); + + } + + function testToHtml() { + + $href = new Href([ + '/foo/bar', + 'foo/bar', + 'http://example.org/bar' + ]); + + $html = new HtmlOutputHelper( + '/base/', + [] + ); + + $expected = + '<a href="/foo/bar">/foo/bar</a><br />' . + '<a href="/base/foo/bar">/base/foo/bar</a><br />' . + '<a href="http://example.org/bar">http://example.org/bar</a>'; + $this->assertEquals($expected, $href->toHtml($html)); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/InviteTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/InviteTest.php new file mode 100644 index 00000000000..6f8d6cc6c30 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/InviteTest.php @@ -0,0 +1,76 @@ +<?php + +namespace Sabre\DAV\Xml\Property; + +use Sabre\DAV\Sharing\Plugin; +use Sabre\DAV\Xml\Element\Sharee; +use Sabre\DAV\Xml\XmlTest; + +class InviteTest extends XmlTest { + + function testSerialize() { + + $sharees = [ + new Sharee(), + new Sharee(), + new Sharee(), + new Sharee() + ]; + $sharees[0]->href = 'mailto:foo@example.org'; + $sharees[0]->properties['{DAV:}displayname'] = 'Foo Bar'; + $sharees[0]->access = Plugin::ACCESS_SHAREDOWNER; + $sharees[0]->inviteStatus = Plugin::INVITE_ACCEPTED; + + $sharees[1]->href = 'mailto:bar@example.org'; + $sharees[1]->access = Plugin::ACCESS_READ; + $sharees[1]->inviteStatus = Plugin::INVITE_DECLINED; + + $sharees[2]->href = 'mailto:baz@example.org'; + $sharees[2]->access = Plugin::ACCESS_READWRITE; + $sharees[2]->inviteStatus = Plugin::INVITE_NORESPONSE; + + $sharees[3]->href = 'mailto:zim@example.org'; + $sharees[3]->access = Plugin::ACCESS_READWRITE; + $sharees[3]->inviteStatus = Plugin::INVITE_INVALID; + + $invite = new Invite($sharees); + + $xml = $this->write(['{DAV:}root' => $invite]); + + $expected = <<<XML +<?xml version="1.0"?> +<d:root xmlns:d="DAV:"> +<d:sharee> + <d:href>mailto:foo@example.org</d:href> + <d:prop> + <d:displayname>Foo Bar</d:displayname> + </d:prop> + <d:share-access><d:shared-owner /></d:share-access> + <d:invite-accepted/> +</d:sharee> +<d:sharee> + <d:href>mailto:bar@example.org</d:href> + <d:prop /> + <d:share-access><d:read /></d:share-access> + <d:invite-declined/> +</d:sharee> +<d:sharee> + <d:href>mailto:baz@example.org</d:href> + <d:prop /> + <d:share-access><d:read-write /></d:share-access> + <d:invite-noresponse/> +</d:sharee> +<d:sharee> + <d:href>mailto:zim@example.org</d:href> + <d:prop /> + <d:share-access><d:read-write /></d:share-access> + <d:invite-invalid/> +</d:sharee> +</d:root> +XML; + + $this->assertXmlStringEqualsXmlString($expected, $xml); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/LastModifiedTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/LastModifiedTest.php new file mode 100644 index 00000000000..669efbd4534 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/LastModifiedTest.php @@ -0,0 +1,59 @@ +<?php + +namespace Sabre\DAV\Xml\Property; + +use DateTime; +use DateTimeZone; +use Sabre\DAV\Xml\XmlTest; + +class LastModifiedTest extends XmlTest { + + function testSerializeDateTime() { + + $dt = new DateTime('2015-03-24 11:47:00', new DateTimeZone('America/Vancouver')); + $val = ['{DAV:}getlastmodified' => new GetLastModified($dt)]; + + $result = $this->write($val); + $expected = <<<XML +<?xml version="1.0"?> +<d:getlastmodified xmlns:d="DAV:">Tue, 24 Mar 2015 18:47:00 GMT</d:getlastmodified> +XML; + + $this->assertXmlStringEqualsXmlString($expected, $result); + + } + + function testSerializeTimeStamp() { + + $dt = new DateTime('2015-03-24 11:47:00', new DateTimeZone('America/Vancouver')); + $dt = $dt->getTimeStamp(); + $val = ['{DAV:}getlastmodified' => new GetLastModified($dt)]; + + $result = $this->write($val); + $expected = <<<XML +<?xml version="1.0"?> +<d:getlastmodified xmlns:d="DAV:">Tue, 24 Mar 2015 18:47:00 GMT</d:getlastmodified> +XML; + + $this->assertXmlStringEqualsXmlString($expected, $result); + + } + + function testDeserialize() { + + $input = <<<XML +<?xml version="1.0"?> +<d:getlastmodified xmlns:d="DAV:">Tue, 24 Mar 2015 18:47:00 GMT</d:getlastmodified> +XML; + + $elementMap = ['{DAV:}getlastmodified' => 'Sabre\DAV\Xml\Property\GetLastModified']; + $result = $this->parse($input, $elementMap); + + $this->assertEquals( + new DateTime('2015-03-24 18:47:00', new DateTimeZone('UTC')), + $result['value']->getTime() + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/LocalHrefTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/LocalHrefTest.php new file mode 100644 index 00000000000..c3f69c929f6 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/LocalHrefTest.php @@ -0,0 +1,69 @@ +<?php + +namespace Sabre\DAV\Xml\Property; + +use Sabre\DAV; +use Sabre\DAV\Browser\HtmlOutputHelper; +use Sabre\DAV\Xml\XmlTest; + +class LocalHrefTest extends XmlTest { + + function testConstruct() { + + $href = new LocalHref('path'); + $this->assertEquals('path', $href->getHref()); + + } + + function testSerialize() { + + $href = new LocalHref('path'); + $this->assertEquals('path', $href->getHref()); + + $this->contextUri = '/bla/'; + + $xml = $this->write(['{DAV:}anything' => $href]); + + $this->assertXmlStringEqualsXmlString( +'<?xml version="1.0"?> +<d:anything xmlns:d="DAV:"><d:href>/bla/path</d:href></d:anything> +', $xml); + + } + function testSerializeSpace() { + + $href = new LocalHref('path alsopath'); + $this->assertEquals('path%20alsopath', $href->getHref()); + + $this->contextUri = '/bla/'; + + $xml = $this->write(['{DAV:}anything' => $href]); + + $this->assertXmlStringEqualsXmlString( +'<?xml version="1.0"?> +<d:anything xmlns:d="DAV:"><d:href>/bla/path%20alsopath</d:href></d:anything> +', $xml); + + } + function testToHtml() { + + $href = new LocalHref([ + '/foo/bar', + 'foo/bar', + 'http://example.org/bar' + ]); + + $html = new HtmlOutputHelper( + '/base/', + [] + ); + + $expected = + '<a href="/foo/bar">/foo/bar</a><br />' . + '<a href="/base/foo/bar">/base/foo/bar</a><br />' . + '<a href="http://example.org/bar">http://example.org/bar</a>'; + $this->assertEquals($expected, $href->toHtml($html)); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/LockDiscoveryTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/LockDiscoveryTest.php new file mode 100644 index 00000000000..0ad069c4723 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/LockDiscoveryTest.php @@ -0,0 +1,86 @@ +<?php + +namespace Sabre\DAV\Xml\Property; + +use Sabre\DAV\Locks\LockInfo; +use Sabre\DAV\Xml\XmlTest; + +class LockDiscoveryTest extends XmlTest { + + function testSerialize() { + + $lock = new LockInfo(); + $lock->owner = 'hello'; + $lock->token = 'blabla'; + $lock->timeout = 600; + $lock->created = strtotime('2015-03-25 19:21:00'); + $lock->scope = LockInfo::EXCLUSIVE; + $lock->depth = 0; + $lock->uri = 'hi'; + + $prop = new LockDiscovery([$lock]); + + $xml = $this->write(['{DAV:}root' => $prop]); + + $this->assertXmlStringEqualsXmlString( +'<?xml version="1.0"?> +<d:root xmlns:d="DAV:"> + <d:activelock> + <d:lockscope><d:exclusive /></d:lockscope> + <d:locktype><d:write /></d:locktype> + <d:lockroot> + <d:href>/hi</d:href> + </d:lockroot> + <d:depth>0</d:depth> + <d:timeout>Second-600</d:timeout> + <d:locktoken> + <d:href>opaquelocktoken:blabla</d:href> + </d:locktoken> + <d:owner>hello</d:owner> + + +</d:activelock> +</d:root> +', $xml); + + } + + function testSerializeShared() { + + $lock = new LockInfo(); + $lock->owner = 'hello'; + $lock->token = 'blabla'; + $lock->timeout = 600; + $lock->created = strtotime('2015-03-25 19:21:00'); + $lock->scope = LockInfo::SHARED; + $lock->depth = 0; + $lock->uri = 'hi'; + + $prop = new LockDiscovery([$lock]); + + $xml = $this->write(['{DAV:}root' => $prop]); + + $this->assertXmlStringEqualsXmlString( +'<?xml version="1.0"?> +<d:root xmlns:d="DAV:"> + <d:activelock> + <d:lockscope><d:shared /></d:lockscope> + <d:locktype><d:write /></d:locktype> + <d:lockroot> + <d:href>/hi</d:href> + </d:lockroot> + <d:depth>0</d:depth> + <d:timeout>Second-600</d:timeout> + <d:locktoken> + <d:href>opaquelocktoken:blabla</d:href> + </d:locktoken> + <d:owner>hello</d:owner> + + +</d:activelock> +</d:root> +', $xml); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/ShareAccessTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/ShareAccessTest.php new file mode 100644 index 00000000000..6e733dded21 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/ShareAccessTest.php @@ -0,0 +1,121 @@ +<?php + +namespace Sabre\DAV\Xml\Property; + +use Sabre\DAV\Sharing\Plugin; +use Sabre\DAV\Xml\XmlTest; + +class ShareAccessTest extends XmlTest { + + function testSerialize() { + + $data = ['{DAV:}root' => [ + [ + 'name' => '{DAV:}share-access', + 'value' => new ShareAccess(Plugin::ACCESS_READ), + ], + [ + 'name' => '{DAV:}share-access', + 'value' => new ShareAccess(Plugin::ACCESS_READWRITE), + ], + [ + 'name' => '{DAV:}share-access', + 'value' => new ShareAccess(Plugin::ACCESS_NOTSHARED), + ], + [ + 'name' => '{DAV:}share-access', + 'value' => new ShareAccess(Plugin::ACCESS_NOACCESS), + ], + [ + 'name' => '{DAV:}share-access', + 'value' => new ShareAccess(Plugin::ACCESS_SHAREDOWNER), + ], + + ]]; + + $xml = $this->write($data); + + $expected = <<<XML +<?xml version="1.0"?> +<d:root xmlns:d="DAV:"> + <d:share-access><d:read /></d:share-access> + <d:share-access><d:read-write /></d:share-access> + <d:share-access><d:not-shared /></d:share-access> + <d:share-access><d:no-access /></d:share-access> + <d:share-access><d:shared-owner /></d:share-access> +</d:root> +XML; + + $this->assertXmlStringEqualsXmlString($expected, $xml); + + } + + function testDeserialize() { + + $input = <<<XML +<?xml version="1.0"?> +<d:root xmlns:d="DAV:"> + <d:share-access><d:read /></d:share-access> + <d:share-access><d:read-write /></d:share-access> + <d:share-access><d:not-shared /></d:share-access> + <d:share-access><d:no-access /></d:share-access> + <d:share-access><d:shared-owner /></d:share-access> +</d:root> +XML; + + $data = [ + [ + 'name' => '{DAV:}share-access', + 'value' => new ShareAccess(Plugin::ACCESS_READ), + 'attributes' => [], + ], + [ + 'name' => '{DAV:}share-access', + 'value' => new ShareAccess(Plugin::ACCESS_READWRITE), + 'attributes' => [], + ], + [ + 'name' => '{DAV:}share-access', + 'value' => new ShareAccess(Plugin::ACCESS_NOTSHARED), + 'attributes' => [], + ], + [ + 'name' => '{DAV:}share-access', + 'value' => new ShareAccess(Plugin::ACCESS_NOACCESS), + 'attributes' => [], + ], + [ + 'name' => '{DAV:}share-access', + 'value' => new ShareAccess(Plugin::ACCESS_SHAREDOWNER), + 'attributes' => [], + ], + + ]; + + $this->assertParsedValue( + $data, + $input, + ['{DAV:}share-access' => ShareAccess::class] + ); + + } + + /** + * @expectedException \Sabre\DAV\Exception\BadRequest + */ + function testDeserializeInvalid() { + + $input = <<<XML +<?xml version="1.0"?> +<d:root xmlns:d="DAV:"> + <d:share-access><d:foo /></d:share-access> +</d:root> +XML; + + $this->parse( + $input, + ['{DAV:}share-access' => ShareAccess::class] + ); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/SupportedMethodSetTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/SupportedMethodSetTest.php new file mode 100644 index 00000000000..3d54acd2d9d --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/SupportedMethodSetTest.php @@ -0,0 +1,45 @@ +<?php + +namespace Sabre\DAV\Xml\Property; + +use Sabre\DAV\Xml\XmlTest; + +class SupportedMethodSetTest extends XmlTest { + + function testSimple() { + + $cus = new SupportedMethodSet(['GET', 'PUT']); + $this->assertEquals(['GET', 'PUT'], $cus->getValue()); + + $this->assertTrue($cus->has('GET')); + $this->assertFalse($cus->has('HEAD')); + + } + + function testSerialize() { + + $cus = new SupportedMethodSet(['GET', 'PUT']); + $xml = $this->write(['{DAV:}foo' => $cus]); + + $expected = '<?xml version="1.0"?> +<d:foo xmlns:d="DAV:"> + <d:supported-method name="GET"/> + <d:supported-method name="PUT"/> +</d:foo>'; + + $this->assertXmlStringEqualsXmlString($expected, $xml); + + } + + function testSerializeHtml() { + + $cus = new SupportedMethodSet(['GET', 'PUT']); + $result = $cus->toHtml( + new \Sabre\DAV\Browser\HtmlOutputHelper('/', []) + ); + + $this->assertEquals('GET, PUT', $result); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/SupportedReportSetTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/SupportedReportSetTest.php new file mode 100644 index 00000000000..cc25697f68d --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Property/SupportedReportSetTest.php @@ -0,0 +1,115 @@ +<?php + +namespace Sabre\DAV\Property; + +use Sabre\DAV; +use Sabre\HTTP; + +require_once 'Sabre/HTTP/ResponseMock.php'; +require_once 'Sabre/DAV/AbstractServer.php'; + +class SupportedReportSetTest extends DAV\AbstractServer { + + function sendPROPFIND($body) { + + $serverVars = [ + 'REQUEST_URI' => '/', + 'REQUEST_METHOD' => 'PROPFIND', + 'HTTP_DEPTH' => '0', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody($body); + + $this->server->httpRequest = ($request); + $this->server->exec(); + + } + + /** + */ + function testNoReports() { + + $xml = '<?xml version="1.0"?> +<d:propfind xmlns:d="DAV:"> + <d:prop> + <d:supported-report-set /> + </d:prop> +</d:propfind>'; + + $this->sendPROPFIND($xml); + + $this->assertEquals(207, $this->response->status, 'We expected a multi-status response. Full response body: ' . $this->response->body); + + $body = preg_replace("/xmlns(:[A-Za-z0-9_])?=(\"|\')DAV:(\"|\')/", "xmlns\\1=\"urn:DAV\"", $this->response->body); + $xml = simplexml_load_string($body); + $xml->registerXPathNamespace('d', 'urn:DAV'); + + $data = $xml->xpath('/d:multistatus/d:response/d:propstat/d:prop'); + $this->assertEquals(1, count($data), 'We expected 1 \'d:prop\' element'); + + $data = $xml->xpath('/d:multistatus/d:response/d:propstat/d:prop/d:supported-report-set'); + $this->assertEquals(1, count($data), 'We expected 1 \'d:supported-report-set\' element'); + + $data = $xml->xpath('/d:multistatus/d:response/d:propstat/d:status'); + $this->assertEquals(1, count($data), 'We expected 1 \'d:status\' element'); + + $this->assertEquals('HTTP/1.1 200 OK', (string)$data[0], 'The status for this property should have been 200'); + + } + + /** + * @depends testNoReports + */ + function testCustomReport() { + + // Intercepting the report property + $this->server->on('propFind', function(DAV\PropFind $propFind, DAV\INode $node) { + if ($prop = $propFind->get('{DAV:}supported-report-set')) { + $prop->addReport('{http://www.rooftopsolutions.nl/testnamespace}myreport'); + $prop->addReport('{DAV:}anotherreport'); + } + }, 200); + + $xml = '<?xml version="1.0"?> +<d:propfind xmlns:d="DAV:"> + <d:prop> + <d:supported-report-set /> + </d:prop> +</d:propfind>'; + + $this->sendPROPFIND($xml); + + $this->assertEquals(207, $this->response->status, 'We expected a multi-status response. Full response body: ' . $this->response->body); + + $body = preg_replace("/xmlns(:[A-Za-z0-9_])?=(\"|\')DAV:(\"|\')/", "xmlns\\1=\"urn:DAV\"", $this->response->body); + $xml = simplexml_load_string($body); + $xml->registerXPathNamespace('d', 'urn:DAV'); + $xml->registerXPathNamespace('x', 'http://www.rooftopsolutions.nl/testnamespace'); + + $data = $xml->xpath('/d:multistatus/d:response/d:propstat/d:prop'); + $this->assertEquals(1, count($data), 'We expected 1 \'d:prop\' element'); + + $data = $xml->xpath('/d:multistatus/d:response/d:propstat/d:prop/d:supported-report-set'); + $this->assertEquals(1, count($data), 'We expected 1 \'d:supported-report-set\' element'); + + $data = $xml->xpath('/d:multistatus/d:response/d:propstat/d:prop/d:supported-report-set/d:supported-report'); + $this->assertEquals(2, count($data), 'We expected 2 \'d:supported-report\' elements'); + + $data = $xml->xpath('/d:multistatus/d:response/d:propstat/d:prop/d:supported-report-set/d:supported-report/d:report'); + $this->assertEquals(2, count($data), 'We expected 2 \'d:report\' elements'); + + $data = $xml->xpath('/d:multistatus/d:response/d:propstat/d:prop/d:supported-report-set/d:supported-report/d:report/x:myreport'); + $this->assertEquals(1, count($data), 'We expected 1 \'x:myreport\' element. Full body: ' . $this->response->body); + + $data = $xml->xpath('/d:multistatus/d:response/d:propstat/d:prop/d:supported-report-set/d:supported-report/d:report/d:anotherreport'); + $this->assertEquals(1, count($data), 'We expected 1 \'d:anotherreport\' element. Full body: ' . $this->response->body); + + $data = $xml->xpath('/d:multistatus/d:response/d:propstat/d:status'); + $this->assertEquals(1, count($data), 'We expected 1 \'d:status\' element'); + + $this->assertEquals('HTTP/1.1 200 OK', (string)$data[0], 'The status for this property should have been 200'); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Request/PropFindTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Request/PropFindTest.php new file mode 100644 index 00000000000..c11668b6188 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Request/PropFindTest.php @@ -0,0 +1,48 @@ +<?php + +namespace Sabre\DAV\Xml\Request; + +use Sabre\DAV\Xml\XmlTest; + +class PropFindTest extends XmlTest { + + function testDeserializeProp() { + + $xml = '<?xml version="1.0"?> +<d:root xmlns:d="DAV:"> + <d:prop> + <d:hello /> + </d:prop> +</d:root> +'; + + $result = $this->parse($xml, ['{DAV:}root' => 'Sabre\\DAV\\Xml\\Request\PropFind']); + + $propFind = new PropFind(); + $propFind->properties = ['{DAV:}hello']; + + $this->assertEquals($propFind, $result['value']); + + + } + + function testDeserializeAllProp() { + + $xml = '<?xml version="1.0"?> +<d:root xmlns:d="DAV:"> + <d:allprop /> +</d:root> +'; + + $result = $this->parse($xml, ['{DAV:}root' => 'Sabre\\DAV\\Xml\\Request\PropFind']); + + $propFind = new PropFind(); + $propFind->allProp = true; + + $this->assertEquals($propFind, $result['value']); + + + } + + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Request/PropPatchTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Request/PropPatchTest.php new file mode 100644 index 00000000000..03514da5cb6 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Request/PropPatchTest.php @@ -0,0 +1,53 @@ +<?php + +namespace Sabre\DAV\Xml\Request; + +use Sabre\DAV\Xml\Property\Href; +use Sabre\DAV\Xml\XmlTest; + +class PropPatchTest extends XmlTest { + + function testSerialize() { + + $propPatch = new PropPatch(); + $propPatch->properties = [ + '{DAV:}displayname' => 'Hello!', + '{DAV:}delete-me' => null, + '{DAV:}some-url' => new Href('foo/bar') + ]; + + $result = $this->write( + ['{DAV:}propertyupdate' => $propPatch] + ); + + $expected = <<<XML +<?xml version="1.0"?> +<d:propertyupdate xmlns:d="DAV:"> + <d:set> + <d:prop> + <d:displayname>Hello!</d:displayname> + </d:prop> + </d:set> + <d:remove> + <d:prop> + <d:delete-me /> + </d:prop> + </d:remove> + <d:set> + <d:prop> + <d:some-url> + <d:href>/foo/bar</d:href> + </d:some-url> + </d:prop> + </d:set> +</d:propertyupdate> +XML; + + $this->assertXmlStringEqualsXmlString( + $expected, + $result + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Request/ShareResourceTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Request/ShareResourceTest.php new file mode 100644 index 00000000000..1e6b5602de0 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Request/ShareResourceTest.php @@ -0,0 +1,75 @@ +<?php + +namespace Sabre\DAV\Xml\Request; + +use Sabre\DAV\Sharing\Plugin; +use Sabre\DAV\Xml\Element\Sharee; +use Sabre\DAV\Xml\XmlTest; + +class ShareResourceTest extends XmlTest { + + function testDeserialize() { + + $xml = <<<XML +<?xml version="1.0" encoding="utf-8" ?> +<D:share-resource xmlns:D="DAV:"> + <D:sharee> + <D:href>mailto:eric@example.com</D:href> + <D:prop> + <D:displayname>Eric York</D:displayname> + </D:prop> + <D:comment>Shared workspace</D:comment> + <D:share-access> + <D:read-write /> + </D:share-access> + </D:sharee> + <D:sharee> + <D:href>mailto:eric@example.com</D:href> + <D:share-access> + <D:read /> + </D:share-access> + </D:sharee> + <D:sharee> + <D:href>mailto:wilfredo@example.com</D:href> + <D:share-access> + <D:no-access /> + </D:share-access> + </D:sharee> +</D:share-resource> +XML; + + $result = $this->parse($xml, [ + '{DAV:}share-resource' => 'Sabre\\DAV\\Xml\\Request\\ShareResource' + ]); + + $this->assertInstanceOf( + 'Sabre\\DAV\\Xml\\Request\\ShareResource', + $result['value'] + ); + + $expected = [ + new Sharee(), + new Sharee(), + new Sharee(), + ]; + + $expected[0]->href = 'mailto:eric@example.com'; + $expected[0]->properties['{DAV:}displayname'] = 'Eric York'; + $expected[0]->comment = 'Shared workspace'; + $expected[0]->access = Plugin::ACCESS_READWRITE; + + $expected[1]->href = 'mailto:eric@example.com'; + $expected[1]->access = Plugin::ACCESS_READ; + + $expected[2]->href = 'mailto:wilfredo@example.com'; + $expected[2]->access = Plugin::ACCESS_NOACCESS; + + $this->assertEquals( + $expected, + $result['value']->sharees + ); + + } + + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Request/SyncCollectionTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Request/SyncCollectionTest.php new file mode 100644 index 00000000000..bde1a103d19 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/Request/SyncCollectionTest.php @@ -0,0 +1,94 @@ +<?php + +namespace Sabre\DAV\Xml\Request; + +use Sabre\DAV\Xml\XmlTest; + +class SyncCollectionTest extends XmlTest { + + function testDeserializeProp() { + + $xml = '<?xml version="1.0"?> +<d:sync-collection xmlns:d="DAV:"> + <d:sync-token /> + <d:sync-level>1</d:sync-level> + <d:prop> + <d:foo /> + </d:prop> +</d:sync-collection> +'; + + $result = $this->parse($xml, ['{DAV:}sync-collection' => 'Sabre\\DAV\\Xml\\Request\\SyncCollectionReport']); + + $elem = new SyncCollectionReport(); + $elem->syncLevel = 1; + $elem->properties = ['{DAV:}foo']; + + $this->assertEquals($elem, $result['value']); + + } + + + function testDeserializeLimit() { + + $xml = '<?xml version="1.0"?> +<d:sync-collection xmlns:d="DAV:"> + <d:sync-token /> + <d:sync-level>1</d:sync-level> + <d:prop> + <d:foo /> + </d:prop> + <d:limit><d:nresults>5</d:nresults></d:limit> +</d:sync-collection> +'; + + $result = $this->parse($xml, ['{DAV:}sync-collection' => 'Sabre\\DAV\\Xml\\Request\\SyncCollectionReport']); + + $elem = new SyncCollectionReport(); + $elem->syncLevel = 1; + $elem->properties = ['{DAV:}foo']; + $elem->limit = 5; + + $this->assertEquals($elem, $result['value']); + + } + + + function testDeserializeInfinity() { + + $xml = '<?xml version="1.0"?> +<d:sync-collection xmlns:d="DAV:"> + <d:sync-token /> + <d:sync-level>infinity</d:sync-level> + <d:prop> + <d:foo /> + </d:prop> +</d:sync-collection> +'; + + $result = $this->parse($xml, ['{DAV:}sync-collection' => 'Sabre\\DAV\\Xml\\Request\\SyncCollectionReport']); + + $elem = new SyncCollectionReport(); + $elem->syncLevel = \Sabre\DAV\Server::DEPTH_INFINITY; + $elem->properties = ['{DAV:}foo']; + + $this->assertEquals($elem, $result['value']); + + } + + /** + * @expectedException \Sabre\DAV\Exception\BadRequest + */ + function testDeserializeMissingElem() { + + $xml = '<?xml version="1.0"?> +<d:sync-collection xmlns:d="DAV:"> + <d:sync-token /> +</d:sync-collection> +'; + + $result = $this->parse($xml, ['{DAV:}sync-collection' => 'Sabre\\DAV\\Xml\\Request\\SyncCollectionReport']); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/XmlTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/XmlTest.php new file mode 100644 index 00000000000..906a36085bc --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAV/Xml/XmlTest.php @@ -0,0 +1,48 @@ +<?php + +namespace Sabre\DAV\Xml; + +use Sabre\Xml\Reader; +use Sabre\Xml\Writer; + +abstract class XmlTest extends \PHPUnit_Framework_TestCase { + + protected $elementMap = []; + protected $namespaceMap = ['DAV:' => 'd']; + protected $contextUri = '/'; + + function write($input) { + + $writer = new Writer(); + $writer->contextUri = $this->contextUri; + $writer->namespaceMap = $this->namespaceMap; + $writer->openMemory(); + $writer->setIndent(true); + $writer->write($input); + return $writer->outputMemory(); + + } + + function parse($xml, array $elementMap = []) { + + $reader = new Reader(); + $reader->elementMap = array_merge($this->elementMap, $elementMap); + $reader->xml($xml); + return $reader->parse(); + + } + + function assertParsedValue($expected, $xml, array $elementMap = []) { + + $result = $this->parse($xml, $elementMap); + $this->assertEquals($expected, $result['value']); + + } + + function cleanUp() { + + libxml_clear_errors(); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/ACLMethodTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/ACLMethodTest.php new file mode 100644 index 00000000000..7d7a54d064c --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/ACLMethodTest.php @@ -0,0 +1,337 @@ +<?php + +namespace Sabre\DAVACL; + +use Sabre\DAV; +use Sabre\HTTP; + +class ACLMethodTest extends \PHPUnit_Framework_TestCase { + + /** + * @expectedException Sabre\DAV\Exception\BadRequest + */ + function testCallback() { + + $acl = new Plugin(); + $server = new DAV\Server(); + $server->addPlugin(new DAV\Auth\Plugin()); + $server->addPlugin($acl); + + $acl->httpAcl($server->httpRequest, $server->httpResponse); + + } + + /** + /** + * @expectedException Sabre\DAV\Exception\MethodNotAllowed + */ + function testNotSupportedByNode() { + + $tree = [ + new DAV\SimpleCollection('test'), + ]; + $acl = new Plugin(); + $server = new DAV\Server($tree); + $server->httpRequest = new HTTP\Request(); + $body = '<?xml version="1.0"?> +<d:acl xmlns:d="DAV:"> +</d:acl>'; + $server->httpRequest->setBody($body); + $server->addPlugin(new DAV\Auth\Plugin()); + $server->addPlugin($acl); + + $acl->httpACL($server->httpRequest, $server->httpResponse); + + } + + function testSuccessSimple() { + + $tree = [ + new MockACLNode('test', []), + ]; + $acl = new Plugin(); + $server = new DAV\Server($tree); + $server->httpRequest = new HTTP\Request(); + $server->httpRequest->setUrl('/test'); + + $body = '<?xml version="1.0"?> +<d:acl xmlns:d="DAV:"> +</d:acl>'; + $server->httpRequest->setBody($body); + $server->addPlugin(new DAV\Auth\Plugin()); + $server->addPlugin($acl); + + $this->assertFalse($acl->httpACL($server->httpRequest, $server->httpResponse)); + + } + + /** + * @expectedException Sabre\DAVACL\Exception\NotRecognizedPrincipal + */ + function testUnrecognizedPrincipal() { + + $tree = [ + new MockACLNode('test', []), + ]; + $acl = new Plugin(); + $server = new DAV\Server($tree); + $server->httpRequest = new HTTP\Request('ACL', '/test'); + $body = '<?xml version="1.0"?> +<d:acl xmlns:d="DAV:"> + <d:ace> + <d:grant><d:privilege><d:read /></d:privilege></d:grant> + <d:principal><d:href>/principals/notfound</d:href></d:principal> + </d:ace> +</d:acl>'; + $server->httpRequest->setBody($body); + $server->addPlugin(new DAV\Auth\Plugin()); + $server->addPlugin($acl); + + $acl->httpACL($server->httpRequest, $server->httpResponse); + + } + + /** + * @expectedException Sabre\DAVACL\Exception\NotRecognizedPrincipal + */ + function testUnrecognizedPrincipal2() { + + $tree = [ + new MockACLNode('test', []), + new DAV\SimpleCollection('principals', [ + new DAV\SimpleCollection('notaprincipal'), + ]), + ]; + $acl = new Plugin(); + $server = new DAV\Server($tree); + $server->httpRequest = new HTTP\Request('ACL', '/test'); + $body = '<?xml version="1.0"?> +<d:acl xmlns:d="DAV:"> + <d:ace> + <d:grant><d:privilege><d:read /></d:privilege></d:grant> + <d:principal><d:href>/principals/notaprincipal</d:href></d:principal> + </d:ace> +</d:acl>'; + $server->httpRequest->setBody($body); + $server->addPlugin(new DAV\Auth\Plugin()); + $server->addPlugin($acl); + + $acl->httpACL($server->httpRequest, $server->httpResponse); + + } + + /** + * @expectedException Sabre\DAVACL\Exception\NotSupportedPrivilege + */ + function testUnknownPrivilege() { + + $tree = [ + new MockACLNode('test', []), + ]; + $acl = new Plugin(); + $server = new DAV\Server($tree); + $server->httpRequest = new HTTP\Request('ACL', '/test'); + $body = '<?xml version="1.0"?> +<d:acl xmlns:d="DAV:"> + <d:ace> + <d:grant><d:privilege><d:bananas /></d:privilege></d:grant> + <d:principal><d:href>/principals/notfound</d:href></d:principal> + </d:ace> +</d:acl>'; + $server->httpRequest->setBody($body); + $server->addPlugin(new DAV\Auth\Plugin()); + $server->addPlugin($acl); + + $acl->httpACL($server->httpRequest, $server->httpResponse); + + } + + /** + * @expectedException Sabre\DAVACL\Exception\NoAbstract + */ + function testAbstractPrivilege() { + + $tree = [ + new MockACLNode('test', []), + ]; + $acl = new Plugin(); + $server = new DAV\Server($tree); + $server->on('getSupportedPrivilegeSet', function($node, &$supportedPrivilegeSet) { + $supportedPrivilegeSet['{DAV:}foo'] = ['abstract' => true]; + }); + $server->httpRequest = new HTTP\Request('ACL', '/test'); + $body = '<?xml version="1.0"?> +<d:acl xmlns:d="DAV:"> + <d:ace> + <d:grant><d:privilege><d:foo /></d:privilege></d:grant> + <d:principal><d:href>/principals/foo/</d:href></d:principal> + </d:ace> +</d:acl>'; + $server->httpRequest->setBody($body); + $server->addPlugin(new DAV\Auth\Plugin()); + $server->addPlugin($acl); + + $acl->httpACL($server->httpRequest, $server->httpResponse); + + } + + /** + * @expectedException Sabre\DAVACL\Exception\AceConflict + */ + function testUpdateProtectedPrivilege() { + + $oldACL = [ + [ + 'principal' => 'principals/notfound', + 'privilege' => '{DAV:}write', + 'protected' => true, + ], + ]; + + $tree = [ + new MockACLNode('test', $oldACL), + ]; + $acl = new Plugin(); + $server = new DAV\Server($tree); + $server->httpRequest = new HTTP\Request('ACL', '/test'); + $body = '<?xml version="1.0"?> +<d:acl xmlns:d="DAV:"> + <d:ace> + <d:grant><d:privilege><d:read /></d:privilege></d:grant> + <d:principal><d:href>/principals/notfound</d:href></d:principal> + </d:ace> +</d:acl>'; + $server->httpRequest->setBody($body); + $server->addPlugin(new DAV\Auth\Plugin()); + $server->addPlugin($acl); + + $acl->httpACL($server->httpRequest, $server->httpResponse); + + } + + /** + * @expectedException Sabre\DAVACL\Exception\AceConflict + */ + function testUpdateProtectedPrivilege2() { + + $oldACL = [ + [ + 'principal' => 'principals/notfound', + 'privilege' => '{DAV:}write', + 'protected' => true, + ], + ]; + + $tree = [ + new MockACLNode('test', $oldACL), + ]; + $acl = new Plugin(); + $server = new DAV\Server($tree); + $server->httpRequest = new HTTP\Request('ACL', '/test'); + $body = '<?xml version="1.0"?> +<d:acl xmlns:d="DAV:"> + <d:ace> + <d:grant><d:privilege><d:write /></d:privilege></d:grant> + <d:principal><d:href>/principals/foo</d:href></d:principal> + </d:ace> +</d:acl>'; + $server->httpRequest->setBody($body); + $server->addPlugin(new DAV\Auth\Plugin()); + $server->addPlugin($acl); + + $acl->httpACL($server->httpRequest, $server->httpResponse); + + } + + /** + * @expectedException Sabre\DAVACL\Exception\AceConflict + */ + function testUpdateProtectedPrivilege3() { + + $oldACL = [ + [ + 'principal' => 'principals/notfound', + 'privilege' => '{DAV:}write', + 'protected' => true, + ], + ]; + + $tree = [ + new MockACLNode('test', $oldACL), + ]; + $acl = new Plugin(); + $server = new DAV\Server($tree); + $server->httpRequest = new HTTP\Request('ACL', '/test'); + $body = '<?xml version="1.0"?> +<d:acl xmlns:d="DAV:"> + <d:ace> + <d:grant><d:privilege><d:write /></d:privilege></d:grant> + <d:principal><d:href>/principals/notfound</d:href></d:principal> + </d:ace> +</d:acl>'; + $server->httpRequest->setBody($body); + $server->addPlugin(new DAV\Auth\Plugin()); + $server->addPlugin($acl); + + $acl->httpACL($server->httpRequest, $server->httpResponse); + + } + + function testSuccessComplex() { + + $oldACL = [ + [ + 'principal' => 'principals/foo', + 'privilege' => '{DAV:}write', + 'protected' => true, + ], + [ + 'principal' => 'principals/bar', + 'privilege' => '{DAV:}read', + ], + ]; + + $tree = [ + $node = new MockACLNode('test', $oldACL), + new DAV\SimpleCollection('principals', [ + new MockPrincipal('foo', 'principals/foo'), + new MockPrincipal('baz', 'principals/baz'), + ]), + ]; + $acl = new Plugin(); + $server = new DAV\Server($tree); + $server->httpRequest = new HTTP\Request('ACL', '/test'); + $body = '<?xml version="1.0"?> +<d:acl xmlns:d="DAV:"> + <d:ace> + <d:grant><d:privilege><d:write /></d:privilege></d:grant> + <d:principal><d:href>/principals/foo</d:href></d:principal> + <d:protected /> + </d:ace> + <d:ace> + <d:grant><d:privilege><d:write /></d:privilege></d:grant> + <d:principal><d:href>/principals/baz</d:href></d:principal> + </d:ace> +</d:acl>'; + $server->httpRequest->setBody($body); + $server->addPlugin(new DAV\Auth\Plugin()); + $server->addPlugin($acl); + + + $this->assertFalse($acl->httpAcl($server->httpRequest, $server->httpResponse)); + + $this->assertEquals([ + [ + 'principal' => 'principals/foo', + 'privilege' => '{DAV:}write', + 'protected' => true, + ], + [ + 'principal' => 'principals/baz', + 'privilege' => '{DAV:}write', + 'protected' => false, + ], + ], $node->getACL()); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/AclPrincipalPropSetReportTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/AclPrincipalPropSetReportTest.php new file mode 100644 index 00000000000..338fe36ab0c --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/AclPrincipalPropSetReportTest.php @@ -0,0 +1,69 @@ +<?php + +namespace Sabre\DAVACL; + +use Sabre\HTTP\Request; + +class AclPrincipalPropSetReportTest extends \Sabre\DAVServerTest { + + public $setupACL = true; + public $autoLogin = 'admin'; + + function testReport() { + + $xml = <<<XML +<?xml version="1.0"?> +<acl-principal-prop-set xmlns="DAV:"> + <prop> + <principal-URL /> + <displayname /> + </prop> +</acl-principal-prop-set> +XML; + + $request = new Request('REPORT', '/principals/user1', ['Content-Type' => 'application/xml', 'Depth' => 0]); + $request->setBody($xml); + + $response = $this->request($request, 207); + + $expected = <<<XML +<?xml version="1.0"?> +<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns"> + <d:response> + <d:href>/principals/admin/</d:href> + <d:propstat> + <d:prop> + <d:principal-URL><d:href>/principals/admin/</d:href></d:principal-URL> + <d:displayname>Admin</d:displayname> + </d:prop> + <d:status>HTTP/1.1 200 OK</d:status> + </d:propstat> + </d:response> +</d:multistatus> +XML; + + $this->assertXmlStringEqualsXmlString( + $expected, + $response->getBodyAsString() + ); + + } + + function testReportDepth1() { + + $xml = <<<XML +<?xml version="1.0"?> +<acl-principal-prop-set xmlns="DAV:"> + <principal-URL /> + <displayname /> +</acl-principal-prop-set> +XML; + + $request = new Request('REPORT', '/principals/user1', ['Content-Type' => 'application/xml', 'Depth' => 1]); + $request->setBody($xml); + + $this->request($request, 400); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/AllowAccessTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/AllowAccessTest.php new file mode 100644 index 00000000000..f16693625b2 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/AllowAccessTest.php @@ -0,0 +1,132 @@ +<?php + +namespace Sabre\DAVACL; + +use Sabre\DAV; + +class AllowAccessTest extends \PHPUnit_Framework_TestCase { + + /** + * @var DAV\Server + */ + protected $server; + + function setUp() { + + $nodes = [ + new DAV\Mock\Collection('testdir', [ + 'file1.txt' => 'contents', + ]), + ]; + + $this->server = new DAV\Server($nodes); + $this->server->addPlugin( + new DAV\Auth\Plugin( + new DAV\Auth\Backend\Mock() + ) + ); + // Login + $this->server->getPlugin('auth')->beforeMethod( + new \Sabre\HTTP\Request(), + new \Sabre\HTTP\Response() + ); + $aclPlugin = new Plugin(); + $this->server->addPlugin($aclPlugin); + + } + + function testGet() { + + $this->server->httpRequest->setMethod('GET'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->assertTrue($this->server->emit('beforeMethod', [$this->server->httpRequest, $this->server->httpResponse])); + + } + + function testGetDoesntExist() { + + $this->server->httpRequest->setMethod('GET'); + $this->server->httpRequest->setUrl('/foo'); + + $this->assertTrue($this->server->emit('beforeMethod', [$this->server->httpRequest, $this->server->httpResponse])); + + } + + function testHEAD() { + + $this->server->httpRequest->setMethod('HEAD'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->assertTrue($this->server->emit('beforeMethod', [$this->server->httpRequest, $this->server->httpResponse])); + + } + + function testOPTIONS() { + + $this->server->httpRequest->setMethod('OPTIONS'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->assertTrue($this->server->emit('beforeMethod', [$this->server->httpRequest, $this->server->httpResponse])); + + } + + function testPUT() { + + $this->server->httpRequest->setMethod('PUT'); + $this->server->httpRequest->setUrl('/testdir/file1.txt'); + + $this->assertTrue($this->server->emit('beforeMethod', [$this->server->httpRequest, $this->server->httpResponse])); + + } + + function testPROPPATCH() { + + $this->server->httpRequest->setMethod('PROPPATCH'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->assertTrue($this->server->emit('beforeMethod', [$this->server->httpRequest, $this->server->httpResponse])); + + } + + function testCOPY() { + + $this->server->httpRequest->setMethod('COPY'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->assertTrue($this->server->emit('beforeMethod', [$this->server->httpRequest, $this->server->httpResponse])); + + } + + function testMOVE() { + + $this->server->httpRequest->setMethod('MOVE'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->assertTrue($this->server->emit('beforeMethod', [$this->server->httpRequest, $this->server->httpResponse])); + + } + + function testLOCK() { + + $this->server->httpRequest->setMethod('LOCK'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->assertTrue($this->server->emit('beforeMethod', [$this->server->httpRequest, $this->server->httpResponse])); + + } + + function testBeforeBind() { + + $this->assertTrue($this->server->emit('beforeBind', ['testdir/file'])); + + } + + + function testBeforeUnbind() { + + $this->assertTrue($this->server->emit('beforeUnbind', ['testdir'])); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/BlockAccessTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/BlockAccessTest.php new file mode 100644 index 00000000000..ceae9aed059 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/BlockAccessTest.php @@ -0,0 +1,215 @@ +<?php + +namespace Sabre\DAVACL; + +use Sabre\DAV; + +class BlockAccessTest extends \PHPUnit_Framework_TestCase { + + /** + * @var DAV\Server + */ + protected $server; + protected $plugin; + + function setUp() { + + $nodes = [ + new DAV\SimpleCollection('testdir'), + ]; + + $this->server = new DAV\Server($nodes); + $this->plugin = new Plugin(); + $this->plugin->setDefaultAcl([]); + $this->server->addPlugin( + new DAV\Auth\Plugin( + new DAV\Auth\Backend\Mock() + ) + ); + // Login + $this->server->getPlugin('auth')->beforeMethod( + new \Sabre\HTTP\Request(), + new \Sabre\HTTP\Response() + ); + $this->server->addPlugin($this->plugin); + + } + + /** + * @expectedException Sabre\DAVACL\Exception\NeedPrivileges + */ + function testGet() { + + $this->server->httpRequest->setMethod('GET'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->server->emit('beforeMethod', [$this->server->httpRequest, $this->server->httpResponse]); + + } + + function testGetDoesntExist() { + + $this->server->httpRequest->setMethod('GET'); + $this->server->httpRequest->setUrl('/foo'); + + $r = $this->server->emit('beforeMethod', [$this->server->httpRequest, $this->server->httpResponse]); + $this->assertTrue($r); + + } + + /** + * @expectedException Sabre\DAVACL\Exception\NeedPrivileges + */ + function testHEAD() { + + $this->server->httpRequest->setMethod('HEAD'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->server->emit('beforeMethod', [$this->server->httpRequest, $this->server->httpResponse]); + + } + + /** + * @expectedException Sabre\DAVACL\Exception\NeedPrivileges + */ + function testOPTIONS() { + + $this->server->httpRequest->setMethod('OPTIONS'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->server->emit('beforeMethod', [$this->server->httpRequest, $this->server->httpResponse]); + + } + + /** + * @expectedException Sabre\DAVACL\Exception\NeedPrivileges + */ + function testPUT() { + + $this->server->httpRequest->setMethod('PUT'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->server->emit('beforeMethod', [$this->server->httpRequest, $this->server->httpResponse]); + + } + + /** + * @expectedException Sabre\DAVACL\Exception\NeedPrivileges + */ + function testPROPPATCH() { + + $this->server->httpRequest->setMethod('PROPPATCH'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->server->emit('beforeMethod', [$this->server->httpRequest, $this->server->httpResponse]); + + } + + /** + * @expectedException Sabre\DAVACL\Exception\NeedPrivileges + */ + function testCOPY() { + + $this->server->httpRequest->setMethod('COPY'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->server->emit('beforeMethod', [$this->server->httpRequest, $this->server->httpResponse]); + + } + + /** + * @expectedException Sabre\DAVACL\Exception\NeedPrivileges + */ + function testMOVE() { + + $this->server->httpRequest->setMethod('MOVE'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->server->emit('beforeMethod', [$this->server->httpRequest, $this->server->httpResponse]); + + } + + /** + * @expectedException Sabre\DAVACL\Exception\NeedPrivileges + */ + function testACL() { + + $this->server->httpRequest->setMethod('ACL'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->server->emit('beforeMethod', [$this->server->httpRequest, $this->server->httpResponse]); + + } + + /** + * @expectedException Sabre\DAVACL\Exception\NeedPrivileges + */ + function testLOCK() { + + $this->server->httpRequest->setMethod('LOCK'); + $this->server->httpRequest->setUrl('/testdir'); + + $this->server->emit('beforeMethod', [$this->server->httpRequest, $this->server->httpResponse]); + + } + + /** + * @expectedException Sabre\DAVACL\Exception\NeedPrivileges + */ + function testBeforeBind() { + + $this->server->emit('beforeBind', ['testdir/file']); + + } + + /** + * @expectedException Sabre\DAVACL\Exception\NeedPrivileges + */ + function testBeforeUnbind() { + + $this->server->emit('beforeUnbind', ['testdir']); + + } + + function testPropFind() { + + $propFind = new DAV\PropFind('testdir', [ + '{DAV:}displayname', + '{DAV:}getcontentlength', + '{DAV:}bar', + '{DAV:}owner', + ]); + + $r = $this->server->emit('propFind', [$propFind, new DAV\SimpleCollection('testdir')]); + $this->assertTrue($r); + + $expected = [ + 200 => [], + 404 => [], + 403 => [ + '{DAV:}displayname' => null, + '{DAV:}getcontentlength' => null, + '{DAV:}bar' => null, + '{DAV:}owner' => null, + ], + ]; + + $this->assertEquals($expected, $propFind->getResultForMultiStatus()); + + } + + function testBeforeGetPropertiesNoListing() { + + $this->plugin->hideNodesFromListings = true; + $propFind = new DAV\PropFind('testdir', [ + '{DAV:}displayname', + '{DAV:}getcontentlength', + '{DAV:}bar', + '{DAV:}owner', + ]); + + $r = $this->server->emit('propFind', [$propFind, new DAV\SimpleCollection('testdir')]); + $this->assertFalse($r); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Exception/AceConflictTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Exception/AceConflictTest.php new file mode 100644 index 00000000000..1cdf2949f2e --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Exception/AceConflictTest.php @@ -0,0 +1,39 @@ +<?php + +namespace Sabre\DAVACL\Exception; + +use Sabre\DAV; + +class AceConflictTest extends \PHPUnit_Framework_TestCase { + + function testSerialize() { + + $ex = new AceConflict('message'); + + $server = new DAV\Server(); + $dom = new \DOMDocument('1.0', 'utf-8'); + $root = $dom->createElementNS('DAV:', 'd:root'); + $dom->appendChild($root); + + $ex->serialize($server, $root); + + $xpaths = [ + '/d:root' => 1, + '/d:root/d:no-ace-conflict' => 1, + ]; + + // Reloading because PHP DOM sucks + $dom2 = new \DOMDocument('1.0', 'utf-8'); + $dom2->loadXML($dom->saveXML()); + + $dxpath = new \DOMXPath($dom2); + $dxpath->registerNamespace('d', 'DAV:'); + foreach ($xpaths as $xpath => $count) { + + $this->assertEquals($count, $dxpath->query($xpath)->length, 'Looking for : ' . $xpath . ', we could only find ' . $dxpath->query($xpath)->length . ' elements, while we expected ' . $count); + + } + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Exception/NeedPrivilegesExceptionTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Exception/NeedPrivilegesExceptionTest.php new file mode 100644 index 00000000000..b13e7722d89 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Exception/NeedPrivilegesExceptionTest.php @@ -0,0 +1,49 @@ +<?php + +namespace Sabre\DAVACL\Exception; + +use Sabre\DAV; + +class NeedPrivilegesExceptionTest extends \PHPUnit_Framework_TestCase { + + function testSerialize() { + + $uri = 'foo'; + $privileges = [ + '{DAV:}read', + '{DAV:}write', + ]; + $ex = new NeedPrivileges($uri, $privileges); + + $server = new DAV\Server(); + $dom = new \DOMDocument('1.0', 'utf-8'); + $root = $dom->createElementNS('DAV:', 'd:root'); + $dom->appendChild($root); + + $ex->serialize($server, $root); + + $xpaths = [ + '/d:root' => 1, + '/d:root/d:need-privileges' => 1, + '/d:root/d:need-privileges/d:resource' => 2, + '/d:root/d:need-privileges/d:resource/d:href' => 2, + '/d:root/d:need-privileges/d:resource/d:privilege' => 2, + '/d:root/d:need-privileges/d:resource/d:privilege/d:read' => 1, + '/d:root/d:need-privileges/d:resource/d:privilege/d:write' => 1, + ]; + + // Reloading because PHP DOM sucks + $dom2 = new \DOMDocument('1.0', 'utf-8'); + $dom2->loadXML($dom->saveXML()); + + $dxpath = new \DOMXPath($dom2); + $dxpath->registerNamespace('d', 'DAV:'); + foreach ($xpaths as $xpath => $count) { + + $this->assertEquals($count, $dxpath->query($xpath)->length, 'Looking for : ' . $xpath . ', we could only find ' . $dxpath->query($xpath)->length . ' elements, while we expected ' . $count); + + } + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Exception/NoAbstractTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Exception/NoAbstractTest.php new file mode 100644 index 00000000000..f52b1737140 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Exception/NoAbstractTest.php @@ -0,0 +1,39 @@ +<?php + +namespace Sabre\DAVACL\Exception; + +use Sabre\DAV; + +class NoAbstractTest extends \PHPUnit_Framework_TestCase { + + function testSerialize() { + + $ex = new NoAbstract('message'); + + $server = new DAV\Server(); + $dom = new \DOMDocument('1.0', 'utf-8'); + $root = $dom->createElementNS('DAV:', 'd:root'); + $dom->appendChild($root); + + $ex->serialize($server, $root); + + $xpaths = [ + '/d:root' => 1, + '/d:root/d:no-abstract' => 1, + ]; + + // Reloading because PHP DOM sucks + $dom2 = new \DOMDocument('1.0', 'utf-8'); + $dom2->loadXML($dom->saveXML()); + + $dxpath = new \DOMXPath($dom2); + $dxpath->registerNamespace('d', 'DAV:'); + foreach ($xpaths as $xpath => $count) { + + $this->assertEquals($count, $dxpath->query($xpath)->length, 'Looking for : ' . $xpath . ', we could only find ' . $dxpath->query($xpath)->length . ' elements, while we expected ' . $count); + + } + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Exception/NotRecognizedPrincipalTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Exception/NotRecognizedPrincipalTest.php new file mode 100644 index 00000000000..df89aaf84d1 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Exception/NotRecognizedPrincipalTest.php @@ -0,0 +1,39 @@ +<?php + +namespace Sabre\DAVACL\Exception; + +use Sabre\DAV; + +class NotRecognizedPrincipalTest extends \PHPUnit_Framework_TestCase { + + function testSerialize() { + + $ex = new NotRecognizedPrincipal('message'); + + $server = new DAV\Server(); + $dom = new \DOMDocument('1.0', 'utf-8'); + $root = $dom->createElementNS('DAV:', 'd:root'); + $dom->appendChild($root); + + $ex->serialize($server, $root); + + $xpaths = [ + '/d:root' => 1, + '/d:root/d:recognized-principal' => 1, + ]; + + // Reloading because PHP DOM sucks + $dom2 = new \DOMDocument('1.0', 'utf-8'); + $dom2->loadXML($dom->saveXML()); + + $dxpath = new \DOMXPath($dom2); + $dxpath->registerNamespace('d', 'DAV:'); + foreach ($xpaths as $xpath => $count) { + + $this->assertEquals($count, $dxpath->query($xpath)->length, 'Looking for : ' . $xpath . ', we could only find ' . $dxpath->query($xpath)->length . ' elements, while we expected ' . $count); + + } + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Exception/NotSupportedPrivilegeTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Exception/NotSupportedPrivilegeTest.php new file mode 100644 index 00000000000..50623952be7 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Exception/NotSupportedPrivilegeTest.php @@ -0,0 +1,39 @@ +<?php + +namespace Sabre\DAVACL\Exception; + +use Sabre\DAV; + +class NotSupportedPrivilegeTest extends \PHPUnit_Framework_TestCase { + + function testSerialize() { + + $ex = new NotSupportedPrivilege('message'); + + $server = new DAV\Server(); + $dom = new \DOMDocument('1.0', 'utf-8'); + $root = $dom->createElementNS('DAV:', 'd:root'); + $dom->appendChild($root); + + $ex->serialize($server, $root); + + $xpaths = [ + '/d:root' => 1, + '/d:root/d:not-supported-privilege' => 1, + ]; + + // Reloading because PHP DOM sucks + $dom2 = new \DOMDocument('1.0', 'utf-8'); + $dom2->loadXML($dom->saveXML()); + + $dxpath = new \DOMXPath($dom2); + $dxpath->registerNamespace('d', 'DAV:'); + foreach ($xpaths as $xpath => $count) { + + $this->assertEquals($count, $dxpath->query($xpath)->length, 'Looking for : ' . $xpath . ', we could only find ' . $dxpath->query($xpath)->length . ' elements, while we expected ' . $count); + + } + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/ExpandPropertiesTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/ExpandPropertiesTest.php new file mode 100644 index 00000000000..91de64372ab --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/ExpandPropertiesTest.php @@ -0,0 +1,317 @@ +<?php + +namespace Sabre\DAVACL; + +use Sabre\DAV; +use Sabre\HTTP; + +require_once 'Sabre/HTTP/ResponseMock.php'; + +class ExpandPropertiesTest extends \PHPUnit_Framework_TestCase { + + function getServer() { + + $tree = [ + new DAV\Mock\PropertiesCollection('node1', [], [ + '{http://sabredav.org/ns}simple' => 'foo', + '{http://sabredav.org/ns}href' => new DAV\Xml\Property\Href('node2'), + '{DAV:}displayname' => 'Node 1', + ]), + new DAV\Mock\PropertiesCollection('node2', [], [ + '{http://sabredav.org/ns}simple' => 'simple', + '{http://sabredav.org/ns}hreflist' => new DAV\Xml\Property\Href(['node1', 'node3']), + '{DAV:}displayname' => 'Node 2', + ]), + new DAV\Mock\PropertiesCollection('node3', [], [ + '{http://sabredav.org/ns}simple' => 'simple', + '{DAV:}displayname' => 'Node 3', + ]), + ]; + + $fakeServer = new DAV\Server($tree); + $fakeServer->sapi = new HTTP\SapiMock(); + $fakeServer->debugExceptions = true; + $fakeServer->httpResponse = new HTTP\ResponseMock(); + $plugin = new Plugin(); + $plugin->allowUnauthenticatedAccess = false; + // Anyone can do anything + $plugin->setDefaultACL([ + [ + 'principal' => '{DAV:}all', + 'privilege' => '{DAV:}all', + ] + ]); + $this->assertTrue($plugin instanceof Plugin); + + $fakeServer->addPlugin($plugin); + $this->assertEquals($plugin, $fakeServer->getPlugin('acl')); + + return $fakeServer; + + } + + function testSimple() { + + $xml = '<?xml version="1.0"?> +<d:expand-property xmlns:d="DAV:"> + <d:property name="displayname" /> + <d:property name="foo" namespace="http://www.sabredav.org/NS/2010/nonexistant" /> + <d:property name="simple" namespace="http://sabredav.org/ns" /> + <d:property name="href" namespace="http://sabredav.org/ns" /> +</d:expand-property>'; + + $serverVars = [ + 'REQUEST_METHOD' => 'REPORT', + 'HTTP_DEPTH' => '0', + 'REQUEST_URI' => '/node1', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody($xml); + + $server = $this->getServer(); + $server->httpRequest = $request; + + $server->exec(); + + $this->assertEquals(207, $server->httpResponse->status, 'Incorrect status code received. Full body: ' . $server->httpResponse->body); + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + ], $server->httpResponse->getHeaders()); + + + $check = [ + '/d:multistatus', + '/d:multistatus/d:response' => 1, + '/d:multistatus/d:response/d:href' => 1, + '/d:multistatus/d:response/d:propstat' => 2, + '/d:multistatus/d:response/d:propstat/d:prop' => 2, + '/d:multistatus/d:response/d:propstat/d:prop/d:displayname' => 1, + '/d:multistatus/d:response/d:propstat/d:prop/s:simple' => 1, + '/d:multistatus/d:response/d:propstat/d:prop/s:href' => 1, + '/d:multistatus/d:response/d:propstat/d:prop/s:href/d:href' => 1, + ]; + + $xml = simplexml_load_string($server->httpResponse->body); + $xml->registerXPathNamespace('d', 'DAV:'); + $xml->registerXPathNamespace('s', 'http://sabredav.org/ns'); + foreach ($check as $v1 => $v2) { + + $xpath = is_int($v1) ? $v2 : $v1; + + $result = $xml->xpath($xpath); + + $count = 1; + if (!is_int($v1)) $count = $v2; + + $this->assertEquals($count, count($result), 'we expected ' . $count . ' appearances of ' . $xpath . ' . We found ' . count($result) . '. Full response: ' . $server->httpResponse->body); + + } + + } + + /** + * @depends testSimple + */ + function testExpand() { + + $xml = '<?xml version="1.0"?> +<d:expand-property xmlns:d="DAV:"> + <d:property name="href" namespace="http://sabredav.org/ns"> + <d:property name="displayname" /> + </d:property> +</d:expand-property>'; + + $serverVars = [ + 'REQUEST_METHOD' => 'REPORT', + 'HTTP_DEPTH' => '0', + 'REQUEST_URI' => '/node1', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody($xml); + + $server = $this->getServer(); + $server->httpRequest = $request; + + $server->exec(); + + $this->assertEquals(207, $server->httpResponse->status, 'Incorrect response status received. Full response body: ' . $server->httpResponse->body); + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + ], $server->httpResponse->getHeaders()); + + + $check = [ + '/d:multistatus', + '/d:multistatus/d:response' => 1, + '/d:multistatus/d:response/d:href' => 1, + '/d:multistatus/d:response/d:propstat' => 1, + '/d:multistatus/d:response/d:propstat/d:prop' => 1, + '/d:multistatus/d:response/d:propstat/d:prop/s:href' => 1, + '/d:multistatus/d:response/d:propstat/d:prop/s:href/d:response' => 1, + '/d:multistatus/d:response/d:propstat/d:prop/s:href/d:response/d:href' => 1, + '/d:multistatus/d:response/d:propstat/d:prop/s:href/d:response/d:propstat' => 1, + '/d:multistatus/d:response/d:propstat/d:prop/s:href/d:response/d:propstat/d:prop' => 1, + '/d:multistatus/d:response/d:propstat/d:prop/s:href/d:response/d:propstat/d:prop/d:displayname' => 1, + ]; + + $xml = simplexml_load_string($server->httpResponse->body); + $xml->registerXPathNamespace('d', 'DAV:'); + $xml->registerXPathNamespace('s', 'http://sabredav.org/ns'); + foreach ($check as $v1 => $v2) { + + $xpath = is_int($v1) ? $v2 : $v1; + + $result = $xml->xpath($xpath); + + $count = 1; + if (!is_int($v1)) $count = $v2; + + $this->assertEquals($count, count($result), 'we expected ' . $count . ' appearances of ' . $xpath . ' . We found ' . count($result) . ' Full response body: ' . $server->httpResponse->getBodyAsString()); + + } + + } + + /** + * @depends testSimple + */ + function testExpandHrefList() { + + $xml = '<?xml version="1.0"?> +<d:expand-property xmlns:d="DAV:"> + <d:property name="hreflist" namespace="http://sabredav.org/ns"> + <d:property name="displayname" /> + </d:property> +</d:expand-property>'; + + $serverVars = [ + 'REQUEST_METHOD' => 'REPORT', + 'HTTP_DEPTH' => '0', + 'REQUEST_URI' => '/node2', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody($xml); + + $server = $this->getServer(); + $server->httpRequest = $request; + + $server->exec(); + + $this->assertEquals(207, $server->httpResponse->status); + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + ], $server->httpResponse->getHeaders()); + + + $check = [ + '/d:multistatus', + '/d:multistatus/d:response' => 1, + '/d:multistatus/d:response/d:href' => 1, + '/d:multistatus/d:response/d:propstat' => 1, + '/d:multistatus/d:response/d:propstat/d:prop' => 1, + '/d:multistatus/d:response/d:propstat/d:prop/s:hreflist' => 1, + '/d:multistatus/d:response/d:propstat/d:prop/s:hreflist/d:response' => 2, + '/d:multistatus/d:response/d:propstat/d:prop/s:hreflist/d:response/d:href' => 2, + '/d:multistatus/d:response/d:propstat/d:prop/s:hreflist/d:response/d:propstat' => 2, + '/d:multistatus/d:response/d:propstat/d:prop/s:hreflist/d:response/d:propstat/d:prop' => 2, + '/d:multistatus/d:response/d:propstat/d:prop/s:hreflist/d:response/d:propstat/d:prop/d:displayname' => 2, + ]; + + $xml = simplexml_load_string($server->httpResponse->body); + $xml->registerXPathNamespace('d', 'DAV:'); + $xml->registerXPathNamespace('s', 'http://sabredav.org/ns'); + foreach ($check as $v1 => $v2) { + + $xpath = is_int($v1) ? $v2 : $v1; + + $result = $xml->xpath($xpath); + + $count = 1; + if (!is_int($v1)) $count = $v2; + + $this->assertEquals($count, count($result), 'we expected ' . $count . ' appearances of ' . $xpath . ' . We found ' . count($result)); + + } + + } + + /** + * @depends testExpand + */ + function testExpandDeep() { + + $xml = '<?xml version="1.0"?> +<d:expand-property xmlns:d="DAV:"> + <d:property name="hreflist" namespace="http://sabredav.org/ns"> + <d:property name="href" namespace="http://sabredav.org/ns"> + <d:property name="displayname" /> + </d:property> + <d:property name="displayname" /> + </d:property> +</d:expand-property>'; + + $serverVars = [ + 'REQUEST_METHOD' => 'REPORT', + 'HTTP_DEPTH' => '0', + 'REQUEST_URI' => '/node2', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody($xml); + + $server = $this->getServer(); + $server->httpRequest = $request; + + $server->exec(); + + $this->assertEquals(207, $server->httpResponse->status); + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + ], $server->httpResponse->getHeaders()); + + + $check = [ + '/d:multistatus', + '/d:multistatus/d:response' => 1, + '/d:multistatus/d:response/d:href' => 1, + '/d:multistatus/d:response/d:propstat' => 1, + '/d:multistatus/d:response/d:propstat/d:prop' => 1, + '/d:multistatus/d:response/d:propstat/d:prop/s:hreflist' => 1, + '/d:multistatus/d:response/d:propstat/d:prop/s:hreflist/d:response' => 2, + '/d:multistatus/d:response/d:propstat/d:prop/s:hreflist/d:response/d:href' => 2, + '/d:multistatus/d:response/d:propstat/d:prop/s:hreflist/d:response/d:propstat' => 3, + '/d:multistatus/d:response/d:propstat/d:prop/s:hreflist/d:response/d:propstat/d:prop' => 3, + '/d:multistatus/d:response/d:propstat/d:prop/s:hreflist/d:response/d:propstat/d:prop/d:displayname' => 2, + '/d:multistatus/d:response/d:propstat/d:prop/s:hreflist/d:response/d:propstat/d:prop/s:href' => 2, + '/d:multistatus/d:response/d:propstat/d:prop/s:hreflist/d:response/d:propstat/d:prop/s:href/d:response' => 1, + '/d:multistatus/d:response/d:propstat/d:prop/s:hreflist/d:response/d:propstat/d:prop/s:href/d:response/d:href' => 1, + '/d:multistatus/d:response/d:propstat/d:prop/s:hreflist/d:response/d:propstat/d:prop/s:href/d:response/d:propstat' => 1, + '/d:multistatus/d:response/d:propstat/d:prop/s:hreflist/d:response/d:propstat/d:prop/s:href/d:response/d:propstat/d:prop' => 1, + '/d:multistatus/d:response/d:propstat/d:prop/s:hreflist/d:response/d:propstat/d:prop/s:href/d:response/d:propstat/d:prop/d:displayname' => 1, + ]; + + $xml = simplexml_load_string($server->httpResponse->body); + $xml->registerXPathNamespace('d', 'DAV:'); + $xml->registerXPathNamespace('s', 'http://sabredav.org/ns'); + foreach ($check as $v1 => $v2) { + + $xpath = is_int($v1) ? $v2 : $v1; + + $result = $xml->xpath($xpath); + + $count = 1; + if (!is_int($v1)) $count = $v2; + + $this->assertEquals($count, count($result), 'we expected ' . $count . ' appearances of ' . $xpath . ' . We found ' . count($result)); + + } + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/FS/CollectionTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/FS/CollectionTest.php new file mode 100644 index 00000000000..af18e7cc057 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/FS/CollectionTest.php @@ -0,0 +1,44 @@ +<?php + +namespace Sabre\DAVACL\FS; + +class CollectionTest extends FileTest { + + function setUp() { + + $this->path = SABRE_TEMPDIR; + $this->sut = new Collection($this->path, $this->acl, $this->owner); + + } + + function tearDown() { + + \Sabre\TestUtil::clearTempDir(); + + } + + function testGetChildFile() { + + file_put_contents(SABRE_TEMPDIR . '/file.txt', 'hello'); + $child = $this->sut->getChild('file.txt'); + $this->assertInstanceOf('Sabre\\DAVACL\\FS\\File', $child); + + $this->assertEquals('file.txt', $child->getName()); + $this->assertEquals($this->acl, $child->getACL()); + $this->assertEquals($this->owner, $child->getOwner()); + + } + + function testGetChildDirectory() { + + mkdir(SABRE_TEMPDIR . '/dir'); + $child = $this->sut->getChild('dir'); + $this->assertInstanceOf('Sabre\\DAVACL\\FS\\Collection', $child); + + $this->assertEquals('dir', $child->getName()); + $this->assertEquals($this->acl, $child->getACL()); + $this->assertEquals($this->owner, $child->getOwner()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/FS/FileTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/FS/FileTest.php new file mode 100644 index 00000000000..f57b2fa1d16 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/FS/FileTest.php @@ -0,0 +1,73 @@ +<?php + +namespace Sabre\DAVACL\FS; + +class FileTest extends \PHPUnit_Framework_TestCase { + + /** + * System under test + * + * @var File + */ + protected $sut; + + protected $path = 'foo'; + protected $acl = [ + [ + 'privilege' => '{DAV:}read', + 'principal' => '{DAV:}authenticated', + ] + ]; + + protected $owner = 'principals/evert'; + + function setUp() { + + $this->sut = new File($this->path, $this->acl, $this->owner); + + } + + function testGetOwner() { + + $this->assertEquals( + $this->owner, + $this->sut->getOwner() + ); + + } + + function testGetGroup() { + + $this->assertNull( + $this->sut->getGroup() + ); + + } + + function testGetACL() { + + $this->assertEquals( + $this->acl, + $this->sut->getACL() + ); + + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + */ + function testSetAcl() { + + $this->sut->setACL([]); + + } + + function testGetSupportedPrivilegeSet() { + + $this->assertNull( + $this->sut->getSupportedPrivilegeSet() + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/FS/HomeCollectionTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/FS/HomeCollectionTest.php new file mode 100644 index 00000000000..87cfc83e92c --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/FS/HomeCollectionTest.php @@ -0,0 +1,116 @@ +<?php + +namespace Sabre\DAVACL\FS; + +use Sabre\DAVACL\PrincipalBackend\Mock as PrincipalBackend; + +class HomeCollectionTest extends \PHPUnit_Framework_TestCase { + + /** + * System under test + * + * @var HomeCollection + */ + protected $sut; + + protected $path; + protected $name = 'thuis'; + + function setUp() { + + $principalBackend = new PrincipalBackend(); + + $this->path = SABRE_TEMPDIR . '/home'; + + $this->sut = new HomeCollection($principalBackend, $this->path); + $this->sut->collectionName = $this->name; + + + } + + function tearDown() { + + \Sabre\TestUtil::clearTempDir(); + + } + + function testGetName() { + + $this->assertEquals( + $this->name, + $this->sut->getName() + ); + + } + + function testGetChild() { + + $child = $this->sut->getChild('user1'); + $this->assertInstanceOf('Sabre\\DAVACL\\FS\\Collection', $child); + $this->assertEquals('user1', $child->getName()); + + $owner = 'principals/user1'; + $acl = [ + [ + 'privilege' => '{DAV:}all', + 'principal' => '{DAV:}owner', + 'protected' => true, + ], + ]; + + $this->assertEquals($acl, $child->getACL()); + $this->assertEquals($owner, $child->getOwner()); + + } + + function testGetOwner() { + + $this->assertNull( + $this->sut->getOwner() + ); + + } + + function testGetGroup() { + + $this->assertNull( + $this->sut->getGroup() + ); + + } + + function testGetACL() { + + $acl = [ + [ + 'principal' => '{DAV:}authenticated', + 'privilege' => '{DAV:}read', + 'protected' => true, + ] + ]; + + $this->assertEquals( + $acl, + $this->sut->getACL() + ); + + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + */ + function testSetAcl() { + + $this->sut->setACL([]); + + } + + function testGetSupportedPrivilegeSet() { + + $this->assertNull( + $this->sut->getSupportedPrivilegeSet() + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/MockACLNode.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/MockACLNode.php new file mode 100644 index 00000000000..2d9744e293f --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/MockACLNode.php @@ -0,0 +1,55 @@ +<?php + +namespace Sabre\DAVACL; + +use Sabre\DAV; + +class MockACLNode extends DAV\Node implements IACL { + + public $name; + public $acl; + + function __construct($name, array $acl = []) { + + $this->name = $name; + $this->acl = $acl; + + } + + function getName() { + + return $this->name; + + } + + function getOwner() { + + return null; + + } + + function getGroup() { + + return null; + + } + + function getACL() { + + return $this->acl; + + } + + function setACL(array $acl) { + + $this->acl = $acl; + + } + + function getSupportedPrivilegeSet() { + + return null; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/MockPrincipal.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/MockPrincipal.php new file mode 100644 index 00000000000..934906802ed --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/MockPrincipal.php @@ -0,0 +1,64 @@ +<?php + +namespace Sabre\DAVACL; + +use Sabre\DAV; + +class MockPrincipal extends DAV\Node implements IPrincipal { + + public $name; + public $principalUrl; + public $groupMembership = []; + public $groupMemberSet = []; + + function __construct($name, $principalUrl, array $groupMembership = [], array $groupMemberSet = []) { + + $this->name = $name; + $this->principalUrl = $principalUrl; + $this->groupMembership = $groupMembership; + $this->groupMemberSet = $groupMemberSet; + + } + + function getName() { + + return $this->name; + + } + + function getDisplayName() { + + return $this->getName(); + + } + + function getAlternateUriSet() { + + return []; + + } + + function getPrincipalUrl() { + + return $this->principalUrl; + + } + + function getGroupMemberSet() { + + return $this->groupMemberSet; + + } + + function getGroupMemberShip() { + + return $this->groupMembership; + + } + + function setGroupMemberSet(array $groupMemberSet) { + + $this->groupMemberSet = $groupMemberSet; + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PluginAdminTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PluginAdminTest.php new file mode 100644 index 00000000000..8552448f54b --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PluginAdminTest.php @@ -0,0 +1,79 @@ +<?php + +namespace Sabre\DAVACL; + +use Sabre\DAV; +use Sabre\HTTP; + +require_once 'Sabre/DAVACL/MockACLNode.php'; +require_once 'Sabre/HTTP/ResponseMock.php'; + +class PluginAdminTest extends \PHPUnit_Framework_TestCase { + + public $server; + + function setUp() { + + $principalBackend = new PrincipalBackend\Mock(); + + $tree = [ + new MockACLNode('adminonly', []), + new PrincipalCollection($principalBackend), + ]; + + $this->server = new DAV\Server($tree); + $this->server->sapi = new HTTP\SapiMock(); + $plugin = new DAV\Auth\Plugin(new DAV\Auth\Backend\Mock()); + $this->server->addPlugin($plugin); + } + + function testNoAdminAccess() { + + $plugin = new Plugin(); + $this->server->addPlugin($plugin); + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'OPTIONS', + 'HTTP_DEPTH' => 1, + 'REQUEST_URI' => '/adminonly', + ]); + + $response = new HTTP\ResponseMock(); + + $this->server->httpRequest = $request; + $this->server->httpResponse = $response; + + $this->server->exec(); + + $this->assertEquals(403, $response->status); + + } + + /** + * @depends testNoAdminAccess + */ + function testAdminAccess() { + + $plugin = new Plugin(); + $plugin->adminPrincipals = [ + 'principals/admin', + ]; + $this->server->addPlugin($plugin); + + $request = HTTP\Sapi::createFromServerArray([ + 'REQUEST_METHOD' => 'OPTIONS', + 'HTTP_DEPTH' => 1, + 'REQUEST_URI' => '/adminonly', + ]); + + $response = new HTTP\ResponseMock(); + + $this->server->httpRequest = $request; + $this->server->httpResponse = $response; + + $this->server->exec(); + + $this->assertEquals(200, $response->status); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PluginPropertiesTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PluginPropertiesTest.php new file mode 100644 index 00000000000..fb42efba719 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PluginPropertiesTest.php @@ -0,0 +1,415 @@ +<?php + +namespace Sabre\DAVACL; + +use Sabre\DAV; +use Sabre\HTTP; + +class PluginPropertiesTest extends \PHPUnit_Framework_TestCase { + + function testPrincipalCollectionSet() { + + $plugin = new Plugin(); + $plugin->allowUnauthenticatedAccess = false; + $plugin->setDefaultACL([ + [ + 'principal' => '{DAV:}all', + 'privilege' => '{DAV:}all', + ], + ]); + //Anyone can do anything + $plugin->principalCollectionSet = [ + 'principals1', + 'principals2', + ]; + + $requestedProperties = [ + '{DAV:}principal-collection-set', + ]; + + $server = new DAV\Server(new DAV\SimpleCollection('root')); + $server->addPlugin($plugin); + + $result = $server->getPropertiesForPath('', $requestedProperties); + $result = $result[0]; + + $this->assertEquals(1, count($result[200])); + $this->assertArrayHasKey('{DAV:}principal-collection-set', $result[200]); + $this->assertInstanceOf('Sabre\\DAV\\Xml\\Property\\Href', $result[200]['{DAV:}principal-collection-set']); + + $expected = [ + 'principals1/', + 'principals2/', + ]; + + + $this->assertEquals($expected, $result[200]['{DAV:}principal-collection-set']->getHrefs()); + + + } + + function testCurrentUserPrincipal() { + + $fakeServer = new DAV\Server(); + $plugin = new DAV\Auth\Plugin(new DAV\Auth\Backend\Mock()); + $fakeServer->addPlugin($plugin); + $plugin = new Plugin(); + $plugin->setDefaultACL([ + [ + 'principal' => '{DAV:}all', + 'privilege' => '{DAV:}all', + ], + ]); + $fakeServer->addPlugin($plugin); + + + $requestedProperties = [ + '{DAV:}current-user-principal', + ]; + + $result = $fakeServer->getPropertiesForPath('', $requestedProperties); + $result = $result[0]; + + $this->assertEquals(1, count($result[200])); + $this->assertArrayHasKey('{DAV:}current-user-principal', $result[200]); + $this->assertInstanceOf('Sabre\DAVACL\Xml\Property\Principal', $result[200]['{DAV:}current-user-principal']); + $this->assertEquals(Xml\Property\Principal::UNAUTHENTICATED, $result[200]['{DAV:}current-user-principal']->getType()); + + // This will force the login + $fakeServer->emit('beforeMethod', [$fakeServer->httpRequest, $fakeServer->httpResponse]); + + $result = $fakeServer->getPropertiesForPath('', $requestedProperties); + $result = $result[0]; + + $this->assertEquals(1, count($result[200])); + $this->assertArrayHasKey('{DAV:}current-user-principal', $result[200]); + $this->assertInstanceOf('Sabre\DAVACL\Xml\Property\Principal', $result[200]['{DAV:}current-user-principal']); + $this->assertEquals(Xml\Property\Principal::HREF, $result[200]['{DAV:}current-user-principal']->getType()); + $this->assertEquals('principals/admin/', $result[200]['{DAV:}current-user-principal']->getHref()); + + } + + function testSupportedPrivilegeSet() { + + $plugin = new Plugin(); + $plugin->allowUnauthenticatedAccess = false; + $plugin->setDefaultACL([ + [ + 'principal' => '{DAV:}all', + 'privilege' => '{DAV:}all', + ], + ]); + $server = new DAV\Server(); + $server->addPlugin($plugin); + + $requestedProperties = [ + '{DAV:}supported-privilege-set', + ]; + + $result = $server->getPropertiesForPath('', $requestedProperties); + $result = $result[0]; + + $this->assertEquals(1, count($result[200])); + $this->assertArrayHasKey('{DAV:}supported-privilege-set', $result[200]); + $this->assertInstanceOf('Sabre\\DAVACL\\Xml\\Property\\SupportedPrivilegeSet', $result[200]['{DAV:}supported-privilege-set']); + + $server = new DAV\Server(); + + $prop = $result[200]['{DAV:}supported-privilege-set']; + $result = $server->xml->write('{DAV:}root', $prop); + + $xpaths = [ + '/d:root' => 1, + '/d:root/d:supported-privilege' => 1, + '/d:root/d:supported-privilege/d:privilege' => 1, + '/d:root/d:supported-privilege/d:privilege/d:all' => 1, + '/d:root/d:supported-privilege/d:abstract' => 0, + '/d:root/d:supported-privilege/d:supported-privilege' => 2, + '/d:root/d:supported-privilege/d:supported-privilege/d:privilege' => 2, + '/d:root/d:supported-privilege/d:supported-privilege/d:privilege/d:read' => 1, + '/d:root/d:supported-privilege/d:supported-privilege/d:privilege/d:write' => 1, + '/d:root/d:supported-privilege/d:supported-privilege/d:supported-privilege' => 7, + '/d:root/d:supported-privilege/d:supported-privilege/d:supported-privilege/d:privilege' => 7, + '/d:root/d:supported-privilege/d:supported-privilege/d:supported-privilege/d:privilege/d:read-acl' => 1, + '/d:root/d:supported-privilege/d:supported-privilege/d:supported-privilege/d:privilege/d:read-current-user-privilege-set' => 1, + '/d:root/d:supported-privilege/d:supported-privilege/d:supported-privilege/d:privilege/d:write-content' => 1, + '/d:root/d:supported-privilege/d:supported-privilege/d:supported-privilege/d:privilege/d:write-properties' => 1, + '/d:root/d:supported-privilege/d:supported-privilege/d:supported-privilege/d:privilege/d:bind' => 1, + '/d:root/d:supported-privilege/d:supported-privilege/d:supported-privilege/d:privilege/d:unbind' => 1, + '/d:root/d:supported-privilege/d:supported-privilege/d:supported-privilege/d:privilege/d:unlock' => 1, + '/d:root/d:supported-privilege/d:supported-privilege/d:supported-privilege/d:abstract' => 0, + ]; + + + // reloading because php dom sucks + $dom2 = new \DOMDocument('1.0', 'utf-8'); + $dom2->loadXML($result); + + $dxpath = new \DOMXPath($dom2); + $dxpath->registerNamespace('d', 'DAV:'); + foreach ($xpaths as $xpath => $count) { + + $this->assertEquals($count, $dxpath->query($xpath)->length, 'Looking for : ' . $xpath . ', we could only find ' . $dxpath->query($xpath)->length . ' elements, while we expected ' . $count . ' Full XML: ' . $result); + + } + + } + + function testACL() { + + $plugin = new Plugin(); + $plugin->allowUnauthenticatedAccess = false; + $plugin->setDefaultACL([ + [ + 'principal' => '{DAV:}all', + 'privilege' => '{DAV:}all', + ], + ]); + + $nodes = [ + new MockACLNode('foo', [ + [ + 'principal' => 'principals/admin', + 'privilege' => '{DAV:}read', + ] + ]), + new DAV\SimpleCollection('principals', [ + $principal = new MockPrincipal('admin', 'principals/admin'), + ]), + + ]; + + $server = new DAV\Server($nodes); + $server->addPlugin($plugin); + $authPlugin = new DAV\Auth\Plugin(new DAV\Auth\Backend\Mock()); + $server->addPlugin($authPlugin); + + // Force login + $authPlugin->beforeMethod(new HTTP\Request(), new HTTP\Response()); + + $requestedProperties = [ + '{DAV:}acl', + ]; + + $result = $server->getPropertiesForPath('foo', $requestedProperties); + $result = $result[0]; + + $this->assertEquals(1, count($result[200]), 'The {DAV:}acl property did not return from the list. Full list: ' . print_r($result, true)); + $this->assertArrayHasKey('{DAV:}acl', $result[200]); + $this->assertInstanceOf('Sabre\\DAVACL\\Xml\Property\\Acl', $result[200]['{DAV:}acl']); + + } + + function testACLRestrictions() { + + $plugin = new Plugin(); + $plugin->allowUnauthenticatedAccess = false; + + $nodes = [ + new MockACLNode('foo', [ + [ + 'principal' => 'principals/admin', + 'privilege' => '{DAV:}read', + ] + ]), + new DAV\SimpleCollection('principals', [ + $principal = new MockPrincipal('admin', 'principals/admin'), + ]), + + ]; + + $server = new DAV\Server($nodes); + $server->addPlugin($plugin); + $authPlugin = new DAV\Auth\Plugin(new DAV\Auth\Backend\Mock()); + $server->addPlugin($authPlugin); + + // Force login + $authPlugin->beforeMethod(new HTTP\Request(), new HTTP\Response()); + + $requestedProperties = [ + '{DAV:}acl-restrictions', + ]; + + $result = $server->getPropertiesForPath('foo', $requestedProperties); + $result = $result[0]; + + $this->assertEquals(1, count($result[200]), 'The {DAV:}acl-restrictions property did not return from the list. Full list: ' . print_r($result, true)); + $this->assertArrayHasKey('{DAV:}acl-restrictions', $result[200]); + $this->assertInstanceOf('Sabre\\DAVACL\\Xml\\Property\\AclRestrictions', $result[200]['{DAV:}acl-restrictions']); + + } + + function testAlternateUriSet() { + + $tree = [ + new DAV\SimpleCollection('principals', [ + $principal = new MockPrincipal('user', 'principals/user'), + ]) + ]; + + $fakeServer = new DAV\Server($tree); + //$plugin = new DAV\Auth\Plugin(new DAV\Auth\MockBackend()) + //$fakeServer->addPlugin($plugin); + $plugin = new Plugin(); + $plugin->allowUnauthenticatedAccess = false; + $plugin->setDefaultACL([ + [ + 'principal' => '{DAV:}all', + 'privilege' => '{DAV:}all', + ], + ]); + $fakeServer->addPlugin($plugin); + + $requestedProperties = [ + '{DAV:}alternate-URI-set', + ]; + $result = $fakeServer->getPropertiesForPath('principals/user', $requestedProperties); + $result = $result[0]; + + $this->assertTrue(isset($result[200])); + $this->assertTrue(isset($result[200]['{DAV:}alternate-URI-set'])); + $this->assertInstanceOf('Sabre\\DAV\\Xml\\Property\\Href', $result[200]['{DAV:}alternate-URI-set']); + + $this->assertEquals([], $result[200]['{DAV:}alternate-URI-set']->getHrefs()); + + } + + function testPrincipalURL() { + + $tree = [ + new DAV\SimpleCollection('principals', [ + $principal = new MockPrincipal('user', 'principals/user'), + ]), + ]; + + $fakeServer = new DAV\Server($tree); + //$plugin = new DAV\Auth\Plugin(new DAV\Auth\MockBackend()); + //$fakeServer->addPlugin($plugin); + $plugin = new Plugin(); + $plugin->allowUnauthenticatedAccess = false; + $plugin->setDefaultACL([ + [ + 'principal' => '{DAV:}all', + 'privilege' => '{DAV:}all', + ], + ]); + $fakeServer->addPlugin($plugin); + + $requestedProperties = [ + '{DAV:}principal-URL', + ]; + + $result = $fakeServer->getPropertiesForPath('principals/user', $requestedProperties); + $result = $result[0]; + + $this->assertTrue(isset($result[200])); + $this->assertTrue(isset($result[200]['{DAV:}principal-URL'])); + $this->assertInstanceOf('Sabre\\DAV\\Xml\\Property\\Href', $result[200]['{DAV:}principal-URL']); + + $this->assertEquals('principals/user/', $result[200]['{DAV:}principal-URL']->getHref()); + + } + + function testGroupMemberSet() { + + $tree = [ + new DAV\SimpleCollection('principals', [ + $principal = new MockPrincipal('user', 'principals/user'), + ]), + ]; + + $fakeServer = new DAV\Server($tree); + //$plugin = new DAV\Auth\Plugin(new DAV\Auth\MockBackend()); + //$fakeServer->addPlugin($plugin); + $plugin = new Plugin(); + $plugin->allowUnauthenticatedAccess = false; + $plugin->setDefaultACL([ + [ + 'principal' => '{DAV:}all', + 'privilege' => '{DAV:}all', + ], + ]); + $fakeServer->addPlugin($plugin); + + $requestedProperties = [ + '{DAV:}group-member-set', + ]; + + $result = $fakeServer->getPropertiesForPath('principals/user', $requestedProperties); + $result = $result[0]; + + $this->assertTrue(isset($result[200])); + $this->assertTrue(isset($result[200]['{DAV:}group-member-set'])); + $this->assertInstanceOf('Sabre\\DAV\\Xml\\Property\\Href', $result[200]['{DAV:}group-member-set']); + + $this->assertEquals([], $result[200]['{DAV:}group-member-set']->getHrefs()); + + } + + function testGroupMemberShip() { + + $tree = [ + new DAV\SimpleCollection('principals', [ + $principal = new MockPrincipal('user', 'principals/user'), + ]), + ]; + + $fakeServer = new DAV\Server($tree); + $plugin = new Plugin(); + $plugin->allowUnauthenticatedAccess = false; + $fakeServer->addPlugin($plugin); + $plugin->setDefaultACL([ + [ + 'principal' => '{DAV:}all', + 'privilege' => '{DAV:}all', + ], + ]); + + $requestedProperties = [ + '{DAV:}group-membership', + ]; + + $result = $fakeServer->getPropertiesForPath('principals/user', $requestedProperties); + $result = $result[0]; + + $this->assertTrue(isset($result[200])); + $this->assertTrue(isset($result[200]['{DAV:}group-membership'])); + $this->assertInstanceOf('Sabre\\DAV\\Xml\\Property\\Href', $result[200]['{DAV:}group-membership']); + + $this->assertEquals([], $result[200]['{DAV:}group-membership']->getHrefs()); + + } + + function testGetDisplayName() { + + $tree = [ + new DAV\SimpleCollection('principals', [ + $principal = new MockPrincipal('user', 'principals/user'), + ]), + ]; + + $fakeServer = new DAV\Server($tree); + $plugin = new Plugin(); + $plugin->allowUnauthenticatedAccess = false; + $fakeServer->addPlugin($plugin); + $plugin->setDefaultACL([ + [ + 'principal' => '{DAV:}all', + 'privilege' => '{DAV:}all', + ], + ]); + + $requestedProperties = [ + '{DAV:}displayname', + ]; + + $result = $fakeServer->getPropertiesForPath('principals/user', $requestedProperties); + $result = $result[0]; + + $this->assertTrue(isset($result[200])); + $this->assertTrue(isset($result[200]['{DAV:}displayname'])); + + $this->assertEquals('user', $result[200]['{DAV:}displayname']); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PluginUpdatePropertiesTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PluginUpdatePropertiesTest.php new file mode 100644 index 00000000000..0147e6a6177 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PluginUpdatePropertiesTest.php @@ -0,0 +1,116 @@ +<?php + +namespace Sabre\DAVACL; + +use Sabre\DAV; + +class PluginUpdatePropertiesTest extends \PHPUnit_Framework_TestCase { + + function testUpdatePropertiesPassthrough() { + + $tree = [ + new DAV\SimpleCollection('foo'), + ]; + $server = new DAV\Server($tree); + $server->addPlugin(new DAV\Auth\Plugin()); + $server->addPlugin(new Plugin()); + + $result = $server->updateProperties('foo', [ + '{DAV:}foo' => 'bar', + ]); + + $expected = [ + '{DAV:}foo' => 403, + ]; + + $this->assertEquals($expected, $result); + + } + + function testRemoveGroupMembers() { + + $tree = [ + new MockPrincipal('foo', 'foo'), + ]; + $server = new DAV\Server($tree); + $plugin = new Plugin(); + $plugin->allowUnauthenticatedAccess = false; + $server->addPlugin($plugin); + + $result = $server->updateProperties('foo', [ + '{DAV:}group-member-set' => null, + ]); + + $expected = [ + '{DAV:}group-member-set' => 204 + ]; + + $this->assertEquals($expected, $result); + $this->assertEquals([], $tree[0]->getGroupMemberSet()); + + } + + function testSetGroupMembers() { + + $tree = [ + new MockPrincipal('foo', 'foo'), + ]; + $server = new DAV\Server($tree); + $plugin = new Plugin(); + $plugin->allowUnauthenticatedAccess = false; + $server->addPlugin($plugin); + + $result = $server->updateProperties('foo', [ + '{DAV:}group-member-set' => new DAV\Xml\Property\Href(['/bar', '/baz'], true), + ]); + + $expected = [ + '{DAV:}group-member-set' => 200 + ]; + + $this->assertEquals($expected, $result); + $this->assertEquals(['bar', 'baz'], $tree[0]->getGroupMemberSet()); + + } + + /** + * @expectedException Sabre\DAV\Exception + */ + function testSetBadValue() { + + $tree = [ + new MockPrincipal('foo', 'foo'), + ]; + $server = new DAV\Server($tree); + $plugin = new Plugin(); + $plugin->allowUnauthenticatedAccess = false; + $server->addPlugin($plugin); + + $result = $server->updateProperties('foo', [ + '{DAV:}group-member-set' => new \StdClass(), + ]); + + } + + function testSetBadNode() { + + $tree = [ + new DAV\SimpleCollection('foo'), + ]; + $server = new DAV\Server($tree); + $plugin = new Plugin(); + $plugin->allowUnauthenticatedAccess = false; + $server->addPlugin($plugin); + + $result = $server->updateProperties('foo', [ + '{DAV:}group-member-set' => new DAV\Xml\Property\Href(['/bar', '/baz'], false), + ]); + + $expected = [ + '{DAV:}group-member-set' => 403, + ]; + + $this->assertEquals($expected, $result); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/AbstractPDOTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/AbstractPDOTest.php new file mode 100644 index 00000000000..9fef3018df9 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/AbstractPDOTest.php @@ -0,0 +1,217 @@ +<?php + +namespace Sabre\DAVACL\PrincipalBackend; + +use Sabre\DAV; +use Sabre\HTTP; + +abstract class AbstractPDOTest extends \PHPUnit_Framework_TestCase { + + use DAV\DbTestHelperTrait; + + function setUp() { + + $this->dropTables(['principals', 'groupmembers']); + $this->createSchema('principals'); + + $pdo = $this->getPDO(); + + $pdo->query("INSERT INTO principals (uri,email,displayname) VALUES ('principals/user','user@example.org','User')"); + $pdo->query("INSERT INTO principals (uri,email,displayname) VALUES ('principals/group','group@example.org','Group')"); + + $pdo->query("INSERT INTO groupmembers (principal_id,member_id) VALUES (5,4)"); + + } + + + function testConstruct() { + + $pdo = $this->getPDO(); + $backend = new PDO($pdo); + $this->assertTrue($backend instanceof PDO); + + } + + /** + * @depends testConstruct + */ + function testGetPrincipalsByPrefix() { + + $pdo = $this->getPDO(); + $backend = new PDO($pdo); + + $expected = [ + [ + 'uri' => 'principals/admin', + '{http://sabredav.org/ns}email-address' => 'admin@example.org', + '{DAV:}displayname' => 'Administrator', + ], + [ + 'uri' => 'principals/user', + '{http://sabredav.org/ns}email-address' => 'user@example.org', + '{DAV:}displayname' => 'User', + ], + [ + 'uri' => 'principals/group', + '{http://sabredav.org/ns}email-address' => 'group@example.org', + '{DAV:}displayname' => 'Group', + ], + ]; + + $this->assertEquals($expected, $backend->getPrincipalsByPrefix('principals')); + $this->assertEquals([], $backend->getPrincipalsByPrefix('foo')); + + } + + /** + * @depends testConstruct + */ + function testGetPrincipalByPath() { + + $pdo = $this->getPDO(); + $backend = new PDO($pdo); + + $expected = [ + 'id' => 4, + 'uri' => 'principals/user', + '{http://sabredav.org/ns}email-address' => 'user@example.org', + '{DAV:}displayname' => 'User', + ]; + + $this->assertEquals($expected, $backend->getPrincipalByPath('principals/user')); + $this->assertEquals(null, $backend->getPrincipalByPath('foo')); + + } + + function testGetGroupMemberSet() { + + $pdo = $this->getPDO(); + $backend = new PDO($pdo); + $expected = ['principals/user']; + + $this->assertEquals($expected, $backend->getGroupMemberSet('principals/group')); + + } + + function testGetGroupMembership() { + + $pdo = $this->getPDO(); + $backend = new PDO($pdo); + $expected = ['principals/group']; + + $this->assertEquals($expected, $backend->getGroupMembership('principals/user')); + + } + + function testSetGroupMemberSet() { + + $pdo = $this->getPDO(); + + // Start situation + $backend = new PDO($pdo); + $this->assertEquals(['principals/user'], $backend->getGroupMemberSet('principals/group')); + + // Removing all principals + $backend->setGroupMemberSet('principals/group', []); + $this->assertEquals([], $backend->getGroupMemberSet('principals/group')); + + // Adding principals again + $backend->setGroupMemberSet('principals/group', ['principals/user']); + $this->assertEquals(['principals/user'], $backend->getGroupMemberSet('principals/group')); + + + } + + function testSearchPrincipals() { + + $pdo = $this->getPDO(); + + $backend = new PDO($pdo); + + $result = $backend->searchPrincipals('principals', ['{DAV:}blabla' => 'foo']); + $this->assertEquals([], $result); + + $result = $backend->searchPrincipals('principals', ['{DAV:}displayname' => 'ou']); + $this->assertEquals(['principals/group'], $result); + + $result = $backend->searchPrincipals('principals', ['{DAV:}displayname' => 'UsEr', '{http://sabredav.org/ns}email-address' => 'USER@EXAMPLE']); + $this->assertEquals(['principals/user'], $result); + + $result = $backend->searchPrincipals('mom', ['{DAV:}displayname' => 'UsEr', '{http://sabredav.org/ns}email-address' => 'USER@EXAMPLE']); + $this->assertEquals([], $result); + + } + + function testUpdatePrincipal() { + + $pdo = $this->getPDO(); + $backend = new PDO($pdo); + + $propPatch = new DAV\PropPatch([ + '{DAV:}displayname' => 'pietje', + ]); + + $backend->updatePrincipal('principals/user', $propPatch); + $result = $propPatch->commit(); + + $this->assertTrue($result); + + $this->assertEquals([ + 'id' => 4, + 'uri' => 'principals/user', + '{DAV:}displayname' => 'pietje', + '{http://sabredav.org/ns}email-address' => 'user@example.org', + ], $backend->getPrincipalByPath('principals/user')); + + } + + function testUpdatePrincipalUnknownField() { + + $pdo = $this->getPDO(); + $backend = new PDO($pdo); + + $propPatch = new DAV\PropPatch([ + '{DAV:}displayname' => 'pietje', + '{DAV:}unknown' => 'foo', + ]); + + $backend->updatePrincipal('principals/user', $propPatch); + $result = $propPatch->commit(); + + $this->assertFalse($result); + + $this->assertEquals([ + '{DAV:}displayname' => 424, + '{DAV:}unknown' => 403 + ], $propPatch->getResult()); + + $this->assertEquals([ + 'id' => '4', + 'uri' => 'principals/user', + '{DAV:}displayname' => 'User', + '{http://sabredav.org/ns}email-address' => 'user@example.org', + ], $backend->getPrincipalByPath('principals/user')); + + } + + function testFindByUriUnknownScheme() { + + $pdo = $this->getPDO(); + $backend = new PDO($pdo); + $this->assertNull($backend->findByUri('http://foo', 'principals')); + + } + + + function testFindByUri() { + + $pdo = $this->getPDO(); + $backend = new PDO($pdo); + $this->assertEquals( + 'principals/user', + $backend->findByUri('mailto:user@example.org', 'principals') + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/Mock.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/Mock.php new file mode 100644 index 00000000000..1464f4c26ab --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/Mock.php @@ -0,0 +1,168 @@ +<?php + +namespace Sabre\DAVACL\PrincipalBackend; + +class Mock extends AbstractBackend { + + public $groupMembers = []; + public $principals; + + function __construct(array $principals = null) { + + $this->principals = $principals; + + if (is_null($principals)) { + + $this->principals = [ + [ + 'uri' => 'principals/user1', + '{DAV:}displayname' => 'User 1', + '{http://sabredav.org/ns}email-address' => 'user1.sabredav@sabredav.org', + '{http://sabredav.org/ns}vcard-url' => 'addressbooks/user1/book1/vcard1.vcf', + ], + [ + 'uri' => 'principals/admin', + '{DAV:}displayname' => 'Admin', + ], + [ + 'uri' => 'principals/user2', + '{DAV:}displayname' => 'User 2', + '{http://sabredav.org/ns}email-address' => 'user2.sabredav@sabredav.org', + ], + ]; + + } + + } + + function getPrincipalsByPrefix($prefix) { + + $prefix = trim($prefix, '/'); + if ($prefix) $prefix .= '/'; + $return = []; + + foreach ($this->principals as $principal) { + + if ($prefix && strpos($principal['uri'], $prefix) !== 0) continue; + + $return[] = $principal; + + } + + return $return; + + } + + function addPrincipal(array $principal) { + + $this->principals[] = $principal; + + } + + function getPrincipalByPath($path) { + + foreach ($this->getPrincipalsByPrefix('principals') as $principal) { + if ($principal['uri'] === $path) return $principal; + } + + } + + function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') { + + $matches = []; + foreach ($this->getPrincipalsByPrefix($prefixPath) as $principal) { + + foreach ($searchProperties as $key => $value) { + + if (!isset($principal[$key])) { + continue 2; + } + if (mb_stripos($principal[$key], $value, 0, 'UTF-8') === false) { + continue 2; + } + + // We have a match for this searchProperty! + if ($test === 'allof') { + continue; + } else { + break; + } + + } + $matches[] = $principal['uri']; + + } + return $matches; + + } + + function getGroupMemberSet($path) { + + return isset($this->groupMembers[$path]) ? $this->groupMembers[$path] : []; + + } + + function getGroupMembership($path) { + + $membership = []; + foreach ($this->groupMembers as $group => $members) { + if (in_array($path, $members)) $membership[] = $group; + } + return $membership; + + } + + function setGroupMemberSet($path, array $members) { + + $this->groupMembers[$path] = $members; + + } + + /** + * 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 + * @param \Sabre\DAV\PropPatch $propPatch + */ + function updatePrincipal($path, \Sabre\DAV\PropPatch $propPatch) { + + $value = null; + foreach ($this->principals as $principalIndex => $value) { + if ($value['uri'] === $path) { + $principal = $value; + break; + } + } + if (!$principal) return; + + $propPatch->handleRemaining(function($mutations) use ($principal, $principalIndex) { + + foreach ($mutations as $prop => $value) { + + if (is_null($value) && isset($principal[$prop])) { + unset($principal[$prop]); + } else { + $principal[$prop] = $value; + } + + } + + $this->principals[$principalIndex] = $principal; + + return true; + + }); + + } + + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/PDOMySQLTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/PDOMySQLTest.php new file mode 100644 index 00000000000..8779eb69f53 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/PDOMySQLTest.php @@ -0,0 +1,9 @@ +<?php + +namespace Sabre\DAVACL\PrincipalBackend; + +class PDOMySQLTest extends AbstractPDOTest { + + public $driver = 'mysql'; + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/PDOPgSqlTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/PDOPgSqlTest.php new file mode 100644 index 00000000000..302616e785c --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/PDOPgSqlTest.php @@ -0,0 +1,9 @@ +<?php + +namespace Sabre\DAVACL\PrincipalBackend; + +class PDOPgSqlTest extends AbstractPDOTest { + + public $driver = 'pgsql'; + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/PDOSqliteTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/PDOSqliteTest.php new file mode 100644 index 00000000000..48454981d54 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalBackend/PDOSqliteTest.php @@ -0,0 +1,9 @@ +<?php + +namespace Sabre\DAVACL\PrincipalBackend; + +class PDOSqliteTest extends AbstractPDOTest { + + public $driver = 'sqlite'; + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalCollectionTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalCollectionTest.php new file mode 100644 index 00000000000..bcf78821bc9 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalCollectionTest.php @@ -0,0 +1,57 @@ +<?php + +namespace Sabre\DAVACL; + +class PrincipalCollectionTest extends \PHPUnit_Framework_TestCase { + + function testBasic() { + + $backend = new PrincipalBackend\Mock(); + $pc = new PrincipalCollection($backend); + $this->assertTrue($pc instanceof PrincipalCollection); + + $this->assertEquals('principals', $pc->getName()); + + } + + /** + * @depends testBasic + */ + function testGetChildren() { + + $backend = new PrincipalBackend\Mock(); + $pc = new PrincipalCollection($backend); + + $children = $pc->getChildren(); + $this->assertTrue(is_array($children)); + + foreach ($children as $child) { + $this->assertTrue($child instanceof IPrincipal); + } + + } + + /** + * @depends testBasic + * @expectedException Sabre\DAV\Exception\MethodNotAllowed + */ + function testGetChildrenDisable() { + + $backend = new PrincipalBackend\Mock(); + $pc = new PrincipalCollection($backend); + $pc->disableListing = true; + + $children = $pc->getChildren(); + + } + + function testFindByUri() { + + $backend = new PrincipalBackend\Mock(); + $pc = new PrincipalCollection($backend); + $this->assertEquals('principals/user1', $pc->findByUri('mailto:user1.sabredav@sabredav.org')); + $this->assertNull($pc->findByUri('mailto:fake.user.sabredav@sabredav.org')); + $this->assertNull($pc->findByUri('')); + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalMatchTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalMatchTest.php new file mode 100644 index 00000000000..427e37972db --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalMatchTest.php @@ -0,0 +1,123 @@ +<?php + +namespace Sabre\DAVACL; + +use Sabre\HTTP\Request; + +class PrincipalMatchTest extends \Sabre\DAVServerTest { + + public $setupACL = true; + public $autoLogin = 'user1'; + + function testPrincipalMatch() { + + $xml = <<<XML +<?xml version="1.0"?> +<principal-match xmlns="DAV:"> + <self /> +</principal-match> +XML; + + $request = new Request('REPORT', '/principals', ['Content-Type' => 'application/xml']); + $request->setBody($xml); + + $response = $this->request($request, 207); + + $expected = <<<XML +<?xml version="1.0"?> +<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns"> + <d:status>HTTP/1.1 200 OK</d:status> + <d:href>/principals/user1</d:href> + <d:propstat> + <d:prop/> + <d:status>HTTP/1.1 418 I'm a teapot</d:status> + </d:propstat> +</d:multistatus> +XML; + + $this->assertXmlStringEqualsXmlString( + $expected, + $response->getBodyAsString() + ); + + } + + function testPrincipalMatchProp() { + + $xml = <<<XML +<?xml version="1.0"?> +<principal-match xmlns="DAV:"> + <self /> + <prop> + <resourcetype /> + </prop> +</principal-match> +XML; + + $request = new Request('REPORT', '/principals', ['Content-Type' => 'application/xml']); + $request->setBody($xml); + + $response = $this->request($request, 207); + + $expected = <<<XML +<?xml version="1.0"?> +<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns"> + <d:status>HTTP/1.1 200 OK</d:status> + <d:href>/principals/user1/</d:href> + <d:propstat> + <d:prop> + <d:resourcetype><d:principal/></d:resourcetype> + </d:prop> + <d:status>HTTP/1.1 200 OK</d:status> + </d:propstat> +</d:multistatus> +XML; + + $this->assertXmlStringEqualsXmlString( + $expected, + $response->getBodyAsString() + ); + + } + + function testPrincipalMatchPrincipalProperty() { + + $xml = <<<XML +<?xml version="1.0"?> +<principal-match xmlns="DAV:"> + <principal-property> + <principal-URL /> + </principal-property> + <prop> + <resourcetype /> + </prop> +</principal-match> +XML; + + $request = new Request('REPORT', '/principals', ['Content-Type' => 'application/xml']); + $request->setBody($xml); + + $response = $this->request($request, 207); + + $expected = <<<XML +<?xml version="1.0"?> +<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns"> + <d:status>HTTP/1.1 200 OK</d:status> + <d:href>/principals/user1/</d:href> + <d:propstat> + <d:prop> + <d:resourcetype><d:principal/></d:resourcetype> + </d:prop> + <d:status>HTTP/1.1 200 OK</d:status> + </d:propstat> +</d:multistatus> +XML; + + $this->assertXmlStringEqualsXmlString( + $expected, + $response->getBodyAsString() + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalPropertySearchTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalPropertySearchTest.php new file mode 100644 index 00000000000..60e156d9a0a --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalPropertySearchTest.php @@ -0,0 +1,397 @@ +<?php + +namespace Sabre\DAVACL; + +use Sabre\DAV; +use Sabre\HTTP; + +require_once 'Sabre/HTTP/ResponseMock.php'; + +class PrincipalPropertySearchTest extends \PHPUnit_Framework_TestCase { + + function getServer() { + + $backend = new PrincipalBackend\Mock(); + + $dir = new DAV\SimpleCollection('root'); + $principals = new PrincipalCollection($backend); + $dir->addChild($principals); + + $fakeServer = new DAV\Server($dir); + $fakeServer->sapi = new HTTP\SapiMock(); + $fakeServer->httpResponse = new HTTP\ResponseMock(); + $fakeServer->debugExceptions = true; + $plugin = new MockPlugin(); + $plugin->allowAccessToNodesWithoutACL = true; + $plugin->allowUnauthenticatedAccess = false; + + $this->assertTrue($plugin instanceof Plugin); + $fakeServer->addPlugin($plugin); + $this->assertEquals($plugin, $fakeServer->getPlugin('acl')); + + return $fakeServer; + + } + + function testDepth1() { + + $xml = '<?xml version="1.0"?> +<d:principal-property-search xmlns:d="DAV:"> + <d:property-search> + <d:prop> + <d:displayname /> + </d:prop> + <d:match>user</d:match> + </d:property-search> + <d:prop> + <d:displayname /> + <d:getcontentlength /> + </d:prop> +</d:principal-property-search>'; + + $serverVars = [ + 'REQUEST_METHOD' => 'REPORT', + 'HTTP_DEPTH' => '1', + 'REQUEST_URI' => '/principals', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody($xml); + + $server = $this->getServer(); + $server->httpRequest = $request; + + $server->exec(); + + $this->assertEquals(400, $server->httpResponse->getStatus(), $server->httpResponse->getBodyAsString()); + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + ], $server->httpResponse->getHeaders()); + + } + + + function testUnknownSearchField() { + + $xml = '<?xml version="1.0"?> +<d:principal-property-search xmlns:d="DAV:"> + <d:property-search> + <d:prop> + <d:yourmom /> + </d:prop> + <d:match>user</d:match> + </d:property-search> + <d:prop> + <d:displayname /> + <d:getcontentlength /> + </d:prop> +</d:principal-property-search>'; + + $serverVars = [ + 'REQUEST_METHOD' => 'REPORT', + 'HTTP_DEPTH' => '0', + 'REQUEST_URI' => '/principals', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody($xml); + + $server = $this->getServer(); + $server->httpRequest = $request; + + $server->exec(); + + $this->assertEquals(207, $server->httpResponse->getStatus(), "Full body: " . $server->httpResponse->getBodyAsString()); + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + 'Vary' => ['Brief,Prefer'], + ], $server->httpResponse->getHeaders()); + + } + + function testCorrect() { + + $xml = '<?xml version="1.0"?> +<d:principal-property-search xmlns:d="DAV:"> + <d:apply-to-principal-collection-set /> + <d:property-search> + <d:prop> + <d:displayname /> + </d:prop> + <d:match>user</d:match> + </d:property-search> + <d:prop> + <d:displayname /> + <d:getcontentlength /> + </d:prop> +</d:principal-property-search>'; + + $serverVars = [ + 'REQUEST_METHOD' => 'REPORT', + 'HTTP_DEPTH' => '0', + 'REQUEST_URI' => '/', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody($xml); + + $server = $this->getServer(); + $server->httpRequest = $request; + + $server->exec(); + + $this->assertEquals(207, $server->httpResponse->status, $server->httpResponse->body); + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + 'Vary' => ['Brief,Prefer'], + ], $server->httpResponse->getHeaders()); + + + $check = [ + '/d:multistatus', + '/d:multistatus/d:response' => 2, + '/d:multistatus/d:response/d:href' => 2, + '/d:multistatus/d:response/d:propstat' => 4, + '/d:multistatus/d:response/d:propstat/d:prop' => 4, + '/d:multistatus/d:response/d:propstat/d:prop/d:displayname' => 2, + '/d:multistatus/d:response/d:propstat/d:prop/d:getcontentlength' => 2, + '/d:multistatus/d:response/d:propstat/d:status' => 4, + ]; + + $xml = simplexml_load_string($server->httpResponse->body); + $xml->registerXPathNamespace('d', 'DAV:'); + foreach ($check as $v1 => $v2) { + + $xpath = is_int($v1) ? $v2 : $v1; + + $result = $xml->xpath($xpath); + + $count = 1; + if (!is_int($v1)) $count = $v2; + + $this->assertEquals($count, count($result), 'we expected ' . $count . ' appearances of ' . $xpath . ' . We found ' . count($result) . '. Full response body: ' . $server->httpResponse->body); + + } + + } + + function testAND() { + + $xml = '<?xml version="1.0"?> +<d:principal-property-search xmlns:d="DAV:"> + <d:apply-to-principal-collection-set /> + <d:property-search> + <d:prop> + <d:displayname /> + </d:prop> + <d:match>user</d:match> + </d:property-search> + <d:property-search> + <d:prop> + <d:foo /> + </d:prop> + <d:match>bar</d:match> + </d:property-search> + <d:prop> + <d:displayname /> + <d:getcontentlength /> + </d:prop> +</d:principal-property-search>'; + + $serverVars = [ + 'REQUEST_METHOD' => 'REPORT', + 'HTTP_DEPTH' => '0', + 'REQUEST_URI' => '/', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody($xml); + + $server = $this->getServer(); + $server->httpRequest = $request; + + $server->exec(); + + $this->assertEquals(207, $server->httpResponse->status, $server->httpResponse->body); + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + 'Vary' => ['Brief,Prefer'], + ], $server->httpResponse->getHeaders()); + + + $check = [ + '/d:multistatus', + '/d:multistatus/d:response' => 0, + '/d:multistatus/d:response/d:href' => 0, + '/d:multistatus/d:response/d:propstat' => 0, + '/d:multistatus/d:response/d:propstat/d:prop' => 0, + '/d:multistatus/d:response/d:propstat/d:prop/d:displayname' => 0, + '/d:multistatus/d:response/d:propstat/d:prop/d:getcontentlength' => 0, + '/d:multistatus/d:response/d:propstat/d:status' => 0, + ]; + + $xml = simplexml_load_string($server->httpResponse->body); + $xml->registerXPathNamespace('d', 'DAV:'); + foreach ($check as $v1 => $v2) { + + $xpath = is_int($v1) ? $v2 : $v1; + + $result = $xml->xpath($xpath); + + $count = 1; + if (!is_int($v1)) $count = $v2; + + $this->assertEquals($count, count($result), 'we expected ' . $count . ' appearances of ' . $xpath . ' . We found ' . count($result) . '. Full response body: ' . $server->httpResponse->body); + + } + + } + function testOR() { + + $xml = '<?xml version="1.0"?> +<d:principal-property-search xmlns:d="DAV:" test="anyof"> + <d:apply-to-principal-collection-set /> + <d:property-search> + <d:prop> + <d:displayname /> + </d:prop> + <d:match>user</d:match> + </d:property-search> + <d:property-search> + <d:prop> + <d:foo /> + </d:prop> + <d:match>bar</d:match> + </d:property-search> + <d:prop> + <d:displayname /> + <d:getcontentlength /> + </d:prop> +</d:principal-property-search>'; + + $serverVars = [ + 'REQUEST_METHOD' => 'REPORT', + 'HTTP_DEPTH' => '0', + 'REQUEST_URI' => '/', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody($xml); + + $server = $this->getServer(); + $server->httpRequest = $request; + + $server->exec(); + + $this->assertEquals(207, $server->httpResponse->status, $server->httpResponse->body); + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + 'Vary' => ['Brief,Prefer'], + ], $server->httpResponse->getHeaders()); + + + $check = [ + '/d:multistatus', + '/d:multistatus/d:response' => 2, + '/d:multistatus/d:response/d:href' => 2, + '/d:multistatus/d:response/d:propstat' => 4, + '/d:multistatus/d:response/d:propstat/d:prop' => 4, + '/d:multistatus/d:response/d:propstat/d:prop/d:displayname' => 2, + '/d:multistatus/d:response/d:propstat/d:prop/d:getcontentlength' => 2, + '/d:multistatus/d:response/d:propstat/d:status' => 4, + ]; + + $xml = simplexml_load_string($server->httpResponse->body); + $xml->registerXPathNamespace('d', 'DAV:'); + foreach ($check as $v1 => $v2) { + + $xpath = is_int($v1) ? $v2 : $v1; + + $result = $xml->xpath($xpath); + + $count = 1; + if (!is_int($v1)) $count = $v2; + + $this->assertEquals($count, count($result), 'we expected ' . $count . ' appearances of ' . $xpath . ' . We found ' . count($result) . '. Full response body: ' . $server->httpResponse->body); + + } + + } + function testWrongUri() { + + $xml = '<?xml version="1.0"?> +<d:principal-property-search xmlns:d="DAV:"> + <d:property-search> + <d:prop> + <d:displayname /> + </d:prop> + <d:match>user</d:match> + </d:property-search> + <d:prop> + <d:displayname /> + <d:getcontentlength /> + </d:prop> +</d:principal-property-search>'; + + $serverVars = [ + 'REQUEST_METHOD' => 'REPORT', + 'HTTP_DEPTH' => '0', + 'REQUEST_URI' => '/', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody($xml); + + $server = $this->getServer(); + $server->httpRequest = $request; + + $server->exec(); + + $this->assertEquals(207, $server->httpResponse->status, $server->httpResponse->body); + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + 'Vary' => ['Brief,Prefer'], + ], $server->httpResponse->getHeaders()); + + + $check = [ + '/d:multistatus', + '/d:multistatus/d:response' => 0, + ]; + + $xml = simplexml_load_string($server->httpResponse->body); + $xml->registerXPathNamespace('d', 'DAV:'); + foreach ($check as $v1 => $v2) { + + $xpath = is_int($v1) ? $v2 : $v1; + + $result = $xml->xpath($xpath); + + $count = 1; + if (!is_int($v1)) $count = $v2; + + $this->assertEquals($count, count($result), 'we expected ' . $count . ' appearances of ' . $xpath . ' . We found ' . count($result) . '. Full response body: ' . $server->httpResponse->body); + + } + + } +} + +class MockPlugin extends Plugin { + + function getCurrentUserPrivilegeSet($node) { + + return [ + '{DAV:}read', + '{DAV:}write', + ]; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalSearchPropertySetTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalSearchPropertySetTest.php new file mode 100644 index 00000000000..fa1314d108c --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalSearchPropertySetTest.php @@ -0,0 +1,140 @@ +<?php + +namespace Sabre\DAVACL; + +use Sabre\DAV; +use Sabre\HTTP; + +require_once 'Sabre/HTTP/ResponseMock.php'; + +class PrincipalSearchPropertySetTest extends \PHPUnit_Framework_TestCase { + + function getServer() { + + $backend = new PrincipalBackend\Mock(); + + $dir = new DAV\SimpleCollection('root'); + $principals = new PrincipalCollection($backend); + $dir->addChild($principals); + + $fakeServer = new DAV\Server($dir); + $fakeServer->sapi = new HTTP\SapiMock(); + $fakeServer->httpResponse = new HTTP\ResponseMock(); + $plugin = new Plugin(); + $plugin->allowUnauthenticatedAccess = false; + $this->assertTrue($plugin instanceof Plugin); + $fakeServer->addPlugin($plugin); + $this->assertEquals($plugin, $fakeServer->getPlugin('acl')); + + return $fakeServer; + + } + + function testDepth1() { + + $xml = '<?xml version="1.0"?> +<d:principal-search-property-set xmlns:d="DAV:" />'; + + $serverVars = [ + 'REQUEST_METHOD' => 'REPORT', + 'HTTP_DEPTH' => '1', + 'REQUEST_URI' => '/principals', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody($xml); + + $server = $this->getServer(); + $server->httpRequest = $request; + + $server->exec(); + + $this->assertEquals(400, $server->httpResponse->status); + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + ], $server->httpResponse->getHeaders()); + + } + + function testDepthIncorrectXML() { + + $xml = '<?xml version="1.0"?> +<d:principal-search-property-set xmlns:d="DAV:"><d:ohell /></d:principal-search-property-set>'; + + $serverVars = [ + 'REQUEST_METHOD' => 'REPORT', + 'HTTP_DEPTH' => '0', + 'REQUEST_URI' => '/principals', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody($xml); + + $server = $this->getServer(); + $server->httpRequest = $request; + + $server->exec(); + + $this->assertEquals(400, $server->httpResponse->status, $server->httpResponse->body); + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + ], $server->httpResponse->getHeaders()); + + } + + function testCorrect() { + + $xml = '<?xml version="1.0"?> +<d:principal-search-property-set xmlns:d="DAV:"/>'; + + $serverVars = [ + 'REQUEST_METHOD' => 'REPORT', + 'HTTP_DEPTH' => '0', + 'REQUEST_URI' => '/principals', + ]; + + $request = HTTP\Sapi::createFromServerArray($serverVars); + $request->setBody($xml); + + $server = $this->getServer(); + $server->httpRequest = $request; + + $server->exec(); + + $this->assertEquals(200, $server->httpResponse->status, $server->httpResponse->body); + $this->assertEquals([ + 'X-Sabre-Version' => [DAV\Version::VERSION], + 'Content-Type' => ['application/xml; charset=utf-8'], + ], $server->httpResponse->getHeaders()); + + + $check = [ + '/d:principal-search-property-set', + '/d:principal-search-property-set/d:principal-search-property' => 2, + '/d:principal-search-property-set/d:principal-search-property/d:prop' => 2, + '/d:principal-search-property-set/d:principal-search-property/d:prop/d:displayname' => 1, + '/d:principal-search-property-set/d:principal-search-property/d:prop/s:email-address' => 1, + '/d:principal-search-property-set/d:principal-search-property/d:description' => 2, + ]; + + $xml = simplexml_load_string($server->httpResponse->body); + $xml->registerXPathNamespace('d', 'DAV:'); + $xml->registerXPathNamespace('s', 'http://sabredav.org/ns'); + foreach ($check as $v1 => $v2) { + + $xpath = is_int($v1) ? $v2 : $v1; + + $result = $xml->xpath($xpath); + + $count = 1; + if (!is_int($v1)) $count = $v2; + + $this->assertEquals($count, count($result), 'we expected ' . $count . ' appearances of ' . $xpath . ' . We found ' . count($result) . '. Full response body: ' . $server->httpResponse->body); + + } + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalTest.php new file mode 100644 index 00000000000..20622ad1757 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/PrincipalTest.php @@ -0,0 +1,208 @@ +<?php + +namespace Sabre\DAVACL; + +use Sabre\DAV; +use Sabre\HTTP; + +class PrincipalTest extends \PHPUnit_Framework_TestCase { + + function testConstruct() { + + $principalBackend = new PrincipalBackend\Mock(); + $principal = new Principal($principalBackend, ['uri' => 'principals/admin']); + $this->assertTrue($principal instanceof Principal); + + } + + /** + * @expectedException Sabre\DAV\Exception + */ + function testConstructNoUri() { + + $principalBackend = new PrincipalBackend\Mock(); + $principal = new Principal($principalBackend, []); + + } + + function testGetName() { + + $principalBackend = new PrincipalBackend\Mock(); + $principal = new Principal($principalBackend, ['uri' => 'principals/admin']); + $this->assertEquals('admin', $principal->getName()); + + } + + function testGetDisplayName() { + + $principalBackend = new PrincipalBackend\Mock(); + $principal = new Principal($principalBackend, ['uri' => 'principals/admin']); + $this->assertEquals('admin', $principal->getDisplayname()); + + $principal = new Principal($principalBackend, [ + 'uri' => 'principals/admin', + '{DAV:}displayname' => 'Mr. Admin' + ]); + $this->assertEquals('Mr. Admin', $principal->getDisplayname()); + + } + + function testGetProperties() { + + $principalBackend = new PrincipalBackend\Mock(); + $principal = new Principal($principalBackend, [ + 'uri' => 'principals/admin', + '{DAV:}displayname' => 'Mr. Admin', + '{http://www.example.org/custom}custom' => 'Custom', + '{http://sabredav.org/ns}email-address' => 'admin@example.org', + ]); + + $keys = [ + '{DAV:}displayname', + '{http://www.example.org/custom}custom', + '{http://sabredav.org/ns}email-address', + ]; + $props = $principal->getProperties($keys); + + foreach ($keys as $key) $this->assertArrayHasKey($key, $props); + + $this->assertEquals('Mr. Admin', $props['{DAV:}displayname']); + + $this->assertEquals('admin@example.org', $props['{http://sabredav.org/ns}email-address']); + } + + function testUpdateProperties() { + + $principalBackend = new PrincipalBackend\Mock(); + $principal = new Principal($principalBackend, ['uri' => 'principals/admin']); + + $propPatch = new DAV\PropPatch(['{DAV:}yourmom' => 'test']); + + $result = $principal->propPatch($propPatch); + $result = $propPatch->commit(); + $this->assertTrue($result); + + } + + function testGetPrincipalUrl() { + + $principalBackend = new PrincipalBackend\Mock(); + $principal = new Principal($principalBackend, ['uri' => 'principals/admin']); + $this->assertEquals('principals/admin', $principal->getPrincipalUrl()); + + } + + function testGetAlternateUriSet() { + + $principalBackend = new PrincipalBackend\Mock(); + $principal = new Principal($principalBackend, [ + 'uri' => 'principals/admin', + '{DAV:}displayname' => 'Mr. Admin', + '{http://www.example.org/custom}custom' => 'Custom', + '{http://sabredav.org/ns}email-address' => 'admin@example.org', + '{DAV:}alternate-URI-set' => [ + 'mailto:admin+1@example.org', + 'mailto:admin+2@example.org', + 'mailto:admin@example.org', + ], + ]); + + $expected = [ + 'mailto:admin+1@example.org', + 'mailto:admin+2@example.org', + 'mailto:admin@example.org', + ]; + + $this->assertEquals($expected, $principal->getAlternateUriSet()); + + } + function testGetAlternateUriSetEmpty() { + + $principalBackend = new PrincipalBackend\Mock(); + $principal = new Principal($principalBackend, [ + 'uri' => 'principals/admin', + ]); + + $expected = []; + + $this->assertEquals($expected, $principal->getAlternateUriSet()); + + } + + function testGetGroupMemberSet() { + + $principalBackend = new PrincipalBackend\Mock(); + $principal = new Principal($principalBackend, ['uri' => 'principals/admin']); + $this->assertEquals([], $principal->getGroupMemberSet()); + + } + function testGetGroupMembership() { + + $principalBackend = new PrincipalBackend\Mock(); + $principal = new Principal($principalBackend, ['uri' => 'principals/admin']); + $this->assertEquals([], $principal->getGroupMembership()); + + } + + function testSetGroupMemberSet() { + + $principalBackend = new PrincipalBackend\Mock(); + $principal = new Principal($principalBackend, ['uri' => 'principals/admin']); + $principal->setGroupMemberSet(['principals/foo']); + + $this->assertEquals([ + 'principals/admin' => ['principals/foo'], + ], $principalBackend->groupMembers); + + } + + function testGetOwner() { + + $principalBackend = new PrincipalBackend\Mock(); + $principal = new Principal($principalBackend, ['uri' => 'principals/admin']); + $this->assertEquals('principals/admin', $principal->getOwner()); + + } + + function testGetGroup() { + + $principalBackend = new PrincipalBackend\Mock(); + $principal = new Principal($principalBackend, ['uri' => 'principals/admin']); + $this->assertNull($principal->getGroup()); + + } + + function testGetACl() { + + $principalBackend = new PrincipalBackend\Mock(); + $principal = new Principal($principalBackend, ['uri' => 'principals/admin']); + $this->assertEquals([ + [ + 'privilege' => '{DAV:}all', + 'principal' => '{DAV:}owner', + 'protected' => true, + ] + ], $principal->getACL()); + + } + + /** + * @expectedException \Sabre\DAV\Exception\Forbidden + */ + function testSetACl() { + + $principalBackend = new PrincipalBackend\Mock(); + $principal = new Principal($principalBackend, ['uri' => 'principals/admin']); + $principal->setACL([]); + + } + + function testGetSupportedPrivilegeSet() { + + $principalBackend = new PrincipalBackend\Mock(); + $principal = new Principal($principalBackend, ['uri' => 'principals/admin']); + $this->assertNull($principal->getSupportedPrivilegeSet()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/SimplePluginTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/SimplePluginTest.php new file mode 100644 index 00000000000..2de0ba6a853 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/SimplePluginTest.php @@ -0,0 +1,321 @@ +<?php + +namespace Sabre\DAVACL; + +use Sabre\DAV; +use Sabre\HTTP; + +require_once 'Sabre/DAVACL/MockPrincipal.php'; +require_once 'Sabre/DAVACL/MockACLNode.php'; + +class SimplePluginTest extends \PHPUnit_Framework_TestCase { + + function testValues() { + + $aclPlugin = new Plugin(); + $this->assertEquals('acl', $aclPlugin->getPluginName()); + $this->assertEquals( + ['access-control', 'calendarserver-principal-property-search'], + $aclPlugin->getFeatures() + ); + + $this->assertEquals( + [ + '{DAV:}expand-property', + '{DAV:}principal-match', + '{DAV:}principal-property-search', + '{DAV:}principal-search-property-set' + ], + $aclPlugin->getSupportedReportSet('')); + + $this->assertEquals(['ACL'], $aclPlugin->getMethods('')); + + + $this->assertEquals( + 'acl', + $aclPlugin->getPluginInfo()['name'] + ); + } + + function testGetFlatPrivilegeSet() { + + $expected = [ + '{DAV:}all' => [ + 'privilege' => '{DAV:}all', + 'abstract' => false, + 'aggregates' => [ + '{DAV:}read', + '{DAV:}write', + ], + 'concrete' => '{DAV:}all', + ], + '{DAV:}read' => [ + 'privilege' => '{DAV:}read', + 'abstract' => false, + 'aggregates' => [ + '{DAV:}read-acl', + '{DAV:}read-current-user-privilege-set', + ], + 'concrete' => '{DAV:}read', + ], + '{DAV:}read-acl' => [ + 'privilege' => '{DAV:}read-acl', + 'abstract' => false, + 'aggregates' => [], + 'concrete' => '{DAV:}read-acl', + ], + '{DAV:}read-current-user-privilege-set' => [ + 'privilege' => '{DAV:}read-current-user-privilege-set', + 'abstract' => false, + 'aggregates' => [], + 'concrete' => '{DAV:}read-current-user-privilege-set', + ], + '{DAV:}write' => [ + 'privilege' => '{DAV:}write', + 'abstract' => false, + 'aggregates' => [ + '{DAV:}write-properties', + '{DAV:}write-content', + '{DAV:}unlock', + '{DAV:}bind', + '{DAV:}unbind', + ], + 'concrete' => '{DAV:}write', + ], + '{DAV:}write-properties' => [ + 'privilege' => '{DAV:}write-properties', + 'abstract' => false, + 'aggregates' => [], + 'concrete' => '{DAV:}write-properties', + ], + '{DAV:}write-content' => [ + 'privilege' => '{DAV:}write-content', + 'abstract' => false, + 'aggregates' => [], + 'concrete' => '{DAV:}write-content', + ], + '{DAV:}unlock' => [ + 'privilege' => '{DAV:}unlock', + 'abstract' => false, + 'aggregates' => [], + 'concrete' => '{DAV:}unlock', + ], + '{DAV:}bind' => [ + 'privilege' => '{DAV:}bind', + 'abstract' => false, + 'aggregates' => [], + 'concrete' => '{DAV:}bind', + ], + '{DAV:}unbind' => [ + 'privilege' => '{DAV:}unbind', + 'abstract' => false, + 'aggregates' => [], + 'concrete' => '{DAV:}unbind', + ], + + ]; + + $plugin = new Plugin(); + $plugin->allowUnauthenticatedAccess = false; + $server = new DAV\Server(); + $server->addPlugin($plugin); + $this->assertEquals($expected, $plugin->getFlatPrivilegeSet('')); + + } + + function testCurrentUserPrincipalsNotLoggedIn() { + + $acl = new Plugin(); + $acl->allowUnauthenticatedAccess = false; + $server = new DAV\Server(); + $server->addPlugin($acl); + + $this->assertEquals([], $acl->getCurrentUserPrincipals()); + + } + + function testCurrentUserPrincipalsSimple() { + + $tree = [ + + new DAV\SimpleCollection('principals', [ + new MockPrincipal('admin', 'principals/admin'), + ]) + + ]; + + $acl = new Plugin(); + $acl->allowUnauthenticatedAccess = false; + $server = new DAV\Server($tree); + $server->addPlugin($acl); + + $auth = new DAV\Auth\Plugin(new DAV\Auth\Backend\Mock()); + $server->addPlugin($auth); + + //forcing login + $auth->beforeMethod(new HTTP\Request(), new HTTP\Response()); + + $this->assertEquals(['principals/admin'], $acl->getCurrentUserPrincipals()); + + } + + function testCurrentUserPrincipalsGroups() { + + $tree = [ + + new DAV\SimpleCollection('principals', [ + new MockPrincipal('admin', 'principals/admin', ['principals/administrators', 'principals/everyone']), + new MockPrincipal('administrators', 'principals/administrators', ['principals/groups'], ['principals/admin']), + new MockPrincipal('everyone', 'principals/everyone', [], ['principals/admin']), + new MockPrincipal('groups', 'principals/groups', [], ['principals/administrators']), + ]) + + ]; + + $acl = new Plugin(); + $acl->allowUnauthenticatedAccess = false; + $server = new DAV\Server($tree); + $server->addPlugin($acl); + + $auth = new DAV\Auth\Plugin(new DAV\Auth\Backend\Mock()); + $server->addPlugin($auth); + + //forcing login + $auth->beforeMethod(new HTTP\Request(), new HTTP\Response()); + + $expected = [ + 'principals/admin', + 'principals/administrators', + 'principals/everyone', + 'principals/groups', + ]; + + $this->assertEquals($expected, $acl->getCurrentUserPrincipals()); + + // The second one should trigger the cache and be identical + $this->assertEquals($expected, $acl->getCurrentUserPrincipals()); + + } + + function testGetACL() { + + $acl = [ + [ + 'principal' => 'principals/admin', + 'privilege' => '{DAV:}read', + ], + [ + 'principal' => 'principals/admin', + 'privilege' => '{DAV:}write', + ], + ]; + + + $tree = [ + new MockACLNode('foo', $acl), + ]; + + $server = new DAV\Server($tree); + $aclPlugin = new Plugin(); + $aclPlugin->allowUnauthenticatedAccess = false; + $server->addPlugin($aclPlugin); + + $this->assertEquals($acl, $aclPlugin->getACL('foo')); + + } + + function testGetCurrentUserPrivilegeSet() { + + $acl = [ + [ + 'principal' => 'principals/admin', + 'privilege' => '{DAV:}read', + ], + [ + 'principal' => 'principals/user1', + 'privilege' => '{DAV:}read', + ], + [ + 'principal' => 'principals/admin', + 'privilege' => '{DAV:}write', + ], + ]; + + + $tree = [ + new MockACLNode('foo', $acl), + + new DAV\SimpleCollection('principals', [ + new MockPrincipal('admin', 'principals/admin'), + ]), + + ]; + + $server = new DAV\Server($tree); + $aclPlugin = new Plugin(); + $aclPlugin->allowUnauthenticatedAccess = false; + $server->addPlugin($aclPlugin); + + $auth = new DAV\Auth\Plugin(new DAV\Auth\Backend\Mock()); + $server->addPlugin($auth); + + //forcing login + $auth->beforeMethod(new HTTP\Request(), new HTTP\Response()); + + $expected = [ + '{DAV:}write', + '{DAV:}write-properties', + '{DAV:}write-content', + '{DAV:}unlock', + '{DAV:}write-acl', + '{DAV:}read', + '{DAV:}read-acl', + '{DAV:}read-current-user-privilege-set', + ]; + + $this->assertEquals($expected, $aclPlugin->getCurrentUserPrivilegeSet('foo')); + + } + + function testCheckPrivileges() { + + $acl = [ + [ + 'principal' => 'principals/admin', + 'privilege' => '{DAV:}read', + ], + [ + 'principal' => 'principals/user1', + 'privilege' => '{DAV:}read', + ], + [ + 'principal' => 'principals/admin', + 'privilege' => '{DAV:}write', + ], + ]; + + + $tree = [ + new MockACLNode('foo', $acl), + + new DAV\SimpleCollection('principals', [ + new MockPrincipal('admin', 'principals/admin'), + ]), + + ]; + + $server = new DAV\Server($tree); + $aclPlugin = new Plugin(); + $aclPlugin->allowUnauthenticatedAccess = false; + $server->addPlugin($aclPlugin); + + $auth = new DAV\Auth\Plugin(new DAV\Auth\Backend\Mock()); + $server->addPlugin($auth); + + //forcing login + //$auth->beforeMethod('GET','/'); + + $this->assertFalse($aclPlugin->checkPrivileges('foo', ['{DAV:}read'], Plugin::R_PARENT, false)); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Xml/Property/ACLTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Xml/Property/ACLTest.php new file mode 100644 index 00000000000..7b9853fe5fe --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Xml/Property/ACLTest.php @@ -0,0 +1,342 @@ +<?php + +namespace Sabre\DAVACL\Xml\Property; + +use Sabre\DAV; +use Sabre\DAV\Browser\HtmlOutputHelper; +use Sabre\HTTP; + +class ACLTest extends \PHPUnit_Framework_TestCase { + + function testConstruct() { + + $acl = new Acl([]); + $this->assertInstanceOf('Sabre\DAVACL\Xml\Property\ACL', $acl); + + } + + function testSerializeEmpty() { + + $acl = new Acl([]); + $xml = (new DAV\Server())->xml->write('{DAV:}root', $acl); + + $expected = '<?xml version="1.0"?> +<d:root xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" />'; + + $this->assertXmlStringEqualsXmlString($expected, $xml); + + } + + function testSerialize() { + + $privileges = [ + [ + 'principal' => 'principals/evert', + 'privilege' => '{DAV:}write', + ], + [ + 'principal' => 'principals/foo', + 'privilege' => '{DAV:}read', + 'protected' => true, + ], + ]; + + $acl = new Acl($privileges); + $xml = (new DAV\Server())->xml->write('{DAV:}root', $acl, '/'); + + $expected = '<?xml version="1.0"?> +<d:root xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns"> + <d:ace> + <d:principal> + <d:href>/principals/evert/</d:href> + </d:principal> + <d:grant> + <d:privilege> + <d:write/> + </d:privilege> + </d:grant> + </d:ace> + <d:ace> + <d:principal> + <d:href>/principals/foo/</d:href> + </d:principal> + <d:grant> + <d:privilege> + <d:read/> + </d:privilege> + </d:grant> + <d:protected/> + </d:ace> +</d:root> +'; + $this->assertXmlStringEqualsXmlString($expected, $xml); + + } + + function testSerializeSpecialPrincipals() { + + $privileges = [ + [ + 'principal' => '{DAV:}authenticated', + 'privilege' => '{DAV:}write', + ], + [ + 'principal' => '{DAV:}unauthenticated', + 'privilege' => '{DAV:}write', + ], + [ + 'principal' => '{DAV:}all', + 'privilege' => '{DAV:}write', + ], + + ]; + + $acl = new Acl($privileges); + $xml = (new DAV\Server())->xml->write('{DAV:}root', $acl, '/'); + + $expected = '<?xml version="1.0"?> +<d:root xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns"> + <d:ace> + <d:principal> + <d:authenticated/> + </d:principal> + <d:grant> + <d:privilege> + <d:write/> + </d:privilege> + </d:grant> + </d:ace> + <d:ace> + <d:principal> + <d:unauthenticated/> + </d:principal> + <d:grant> + <d:privilege> + <d:write/> + </d:privilege> + </d:grant> + </d:ace> + <d:ace> + <d:principal> + <d:all/> + </d:principal> + <d:grant> + <d:privilege> + <d:write/> + </d:privilege> + </d:grant> + </d:ace> +</d:root> +'; + $this->assertXmlStringEqualsXmlString($expected, $xml); + + } + + function testUnserialize() { + + $source = '<?xml version="1.0"?> +<d:root xmlns:d="DAV:"> + <d:ace> + <d:principal> + <d:href>/principals/evert/</d:href> + </d:principal> + <d:grant> + <d:privilege> + <d:write/> + </d:privilege> + </d:grant> + </d:ace> + <d:ace> + <d:principal> + <d:href>/principals/foo/</d:href> + </d:principal> + <d:grant> + <d:privilege> + <d:read/> + </d:privilege> + </d:grant> + <d:protected/> + </d:ace> +</d:root> +'; + + $reader = new \Sabre\Xml\Reader(); + $reader->elementMap['{DAV:}root'] = 'Sabre\DAVACL\Xml\Property\Acl'; + $reader->xml($source); + + $result = $reader->parse(); + $result = $result['value']; + + $this->assertInstanceOf('Sabre\\DAVACL\\Xml\\Property\\Acl', $result); + + $expected = [ + [ + 'principal' => '/principals/evert/', + 'protected' => false, + 'privilege' => '{DAV:}write', + ], + [ + 'principal' => '/principals/foo/', + 'protected' => true, + 'privilege' => '{DAV:}read', + ], + ]; + + $this->assertEquals($expected, $result->getPrivileges()); + + + } + + /** + * @expectedException Sabre\DAV\Exception\BadRequest + */ + function testUnserializeNoPrincipal() { + + $source = '<?xml version="1.0"?> +<d:root xmlns:d="DAV:"> + <d:ace> + <d:grant> + <d:privilege> + <d:write/> + </d:privilege> + </d:grant> + </d:ace> +</d:root> +'; + + + $reader = new \Sabre\Xml\Reader(); + $reader->elementMap['{DAV:}root'] = 'Sabre\DAVACL\Xml\Property\Acl'; + $reader->xml($source); + + $result = $reader->parse(); + + } + + function testUnserializeOtherPrincipal() { + + $source = '<?xml version="1.0"?> +<d:root xmlns:d="DAV:"> + <d:ace> + <d:grant> + <d:privilege> + <d:write/> + </d:privilege> + </d:grant> + <d:principal><d:authenticated /></d:principal> + </d:ace> + <d:ace> + <d:grant> + <d:ignoreme /> + <d:privilege> + <d:write/> + </d:privilege> + </d:grant> + <d:principal><d:unauthenticated /></d:principal> + </d:ace> + <d:ace> + <d:grant> + <d:privilege> + <d:write/> + </d:privilege> + </d:grant> + <d:principal><d:all /></d:principal> + </d:ace> +</d:root> +'; + + $reader = new \Sabre\Xml\Reader(); + $reader->elementMap['{DAV:}root'] = 'Sabre\DAVACL\Xml\Property\Acl'; + $reader->xml($source); + + $result = $reader->parse(); + $result = $result['value']; + + $this->assertInstanceOf('Sabre\\DAVACL\\Xml\\Property\\Acl', $result); + + $expected = [ + [ + 'principal' => '{DAV:}authenticated', + 'protected' => false, + 'privilege' => '{DAV:}write', + ], + [ + 'principal' => '{DAV:}unauthenticated', + 'protected' => false, + 'privilege' => '{DAV:}write', + ], + [ + 'principal' => '{DAV:}all', + 'protected' => false, + 'privilege' => '{DAV:}write', + ], + ]; + + $this->assertEquals($expected, $result->getPrivileges()); + + } + + /** + * @expectedException Sabre\DAV\Exception\NotImplemented + */ + function testUnserializeDeny() { + + $source = '<?xml version="1.0"?> +<d:root xmlns:d="DAV:"> + <d:ignore-me /> + <d:ace> + <d:deny> + <d:privilege> + <d:write/> + </d:privilege> + </d:deny> + <d:principal><d:href>/principals/evert</d:href></d:principal> + </d:ace> +</d:root> +'; + + $reader = new \Sabre\Xml\Reader(); + $reader->elementMap['{DAV:}root'] = 'Sabre\DAVACL\Xml\Property\Acl'; + $reader->xml($source); + + $result = $reader->parse(); + + } + + function testToHtml() { + + $privileges = [ + [ + 'principal' => 'principals/evert', + 'privilege' => '{DAV:}write', + ], + [ + 'principal' => 'principals/foo', + 'privilege' => '{http://example.org/ns}read', + 'protected' => true, + ], + [ + 'principal' => '{DAV:}authenticated', + 'privilege' => '{DAV:}write', + ], + ]; + + $acl = new Acl($privileges); + $html = new HtmlOutputHelper( + '/base/', + ['DAV:' => 'd'] + ); + + $expected = + '<table>' . + '<tr><th>Principal</th><th>Privilege</th><th></th></tr>' . + '<tr><td><a href="/base/principals/evert">/base/principals/evert</a></td><td><span title="{DAV:}write">d:write</span></td><td></td></tr>' . + '<tr><td><a href="/base/principals/foo">/base/principals/foo</a></td><td><span title="{http://example.org/ns}read">{http://example.org/ns}read</span></td><td>(protected)</td></tr>' . + '<tr><td><span title="{DAV:}authenticated">d:authenticated</span></td><td><span title="{DAV:}write">d:write</span></td><td></td></tr>' . + '</table>'; + + $this->assertEquals($expected, $acl->toHtml($html)); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Xml/Property/AclRestrictionsTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Xml/Property/AclRestrictionsTest.php new file mode 100644 index 00000000000..6d8b83a1285 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Xml/Property/AclRestrictionsTest.php @@ -0,0 +1,30 @@ +<?php + +namespace Sabre\DAVACL\Xml\Property; + +use Sabre\DAV; +use Sabre\HTTP; + +class AclRestrictionsTest extends \PHPUnit_Framework_TestCase { + + function testConstruct() { + + $prop = new AclRestrictions(); + $this->assertInstanceOf('Sabre\DAVACL\Xml\Property\AclRestrictions', $prop); + + } + + function testSerialize() { + + $prop = new AclRestrictions(); + $xml = (new DAV\Server())->xml->write('{DAV:}root', $prop); + + $expected = '<?xml version="1.0"?> +<d:root xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns"><d:grant-only/><d:no-invert/></d:root>'; + + $this->assertXmlStringEqualsXmlString($expected, $xml); + + } + + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Xml/Property/CurrentUserPrivilegeSetTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Xml/Property/CurrentUserPrivilegeSetTest.php new file mode 100644 index 00000000000..d6e6b2d193f --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Xml/Property/CurrentUserPrivilegeSetTest.php @@ -0,0 +1,86 @@ +<?php + +namespace Sabre\DAVACL\Xml\Property; + +use Sabre\DAV; +use Sabre\DAV\Browser\HtmlOutputHelper; +use Sabre\HTTP; +use Sabre\Xml\Reader; + +class CurrentUserPrivilegeSetTest extends \PHPUnit_Framework_TestCase { + + function testSerialize() { + + $privileges = [ + '{DAV:}read', + '{DAV:}write', + ]; + $prop = new CurrentUserPrivilegeSet($privileges); + $xml = (new DAV\Server())->xml->write('{DAV:}root', $prop); + + $expected = <<<XML +<d:root xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns"> + <d:privilege> + <d:read /> + </d:privilege> + <d:privilege> + <d:write /> + </d:privilege> +</d:root> +XML; + + + $this->assertXmlStringEqualsXmlString($expected, $xml); + + } + + function testUnserialize() { + + $source = '<?xml version="1.0"?> +<d:root xmlns:d="DAV:"> + <d:privilege> + <d:write-properties /> + </d:privilege> + <d:ignoreme /> + <d:privilege> + <d:read /> + </d:privilege> +</d:root> +'; + + $result = $this->parse($source); + $this->assertTrue($result->has('{DAV:}read')); + $this->assertTrue($result->has('{DAV:}write-properties')); + $this->assertFalse($result->has('{DAV:}bind')); + + } + + function parse($xml) { + + $reader = new Reader(); + $reader->elementMap['{DAV:}root'] = 'Sabre\\DAVACL\\Xml\\Property\\CurrentUserPrivilegeSet'; + $reader->xml($xml); + $result = $reader->parse(); + return $result['value']; + + } + + function testToHtml() { + + $privileges = ['{DAV:}read', '{DAV:}write']; + + $prop = new CurrentUserPrivilegeSet($privileges); + $html = new HtmlOutputHelper( + '/base/', + ['DAV:' => 'd'] + ); + + $expected = + '<span title="{DAV:}read">d:read</span>, ' . + '<span title="{DAV:}write">d:write</span>'; + + $this->assertEquals($expected, $prop->toHtml($html)); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Xml/Property/PrincipalTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Xml/Property/PrincipalTest.php new file mode 100644 index 00000000000..876d1073aaa --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Xml/Property/PrincipalTest.php @@ -0,0 +1,191 @@ +<?php + +namespace Sabre\DAVACL\Xml\Property; + +use Sabre\DAV; +use Sabre\DAV\Browser\HtmlOutputHelper; +use Sabre\HTTP; +use Sabre\Xml\Reader; + +class PrincipalTest extends \PHPUnit_Framework_TestCase { + + function testSimple() { + + $principal = new Principal(Principal::UNAUTHENTICATED); + $this->assertEquals(Principal::UNAUTHENTICATED, $principal->getType()); + $this->assertNull($principal->getHref()); + + $principal = new Principal(Principal::AUTHENTICATED); + $this->assertEquals(Principal::AUTHENTICATED, $principal->getType()); + $this->assertNull($principal->getHref()); + + $principal = new Principal(Principal::HREF, 'admin'); + $this->assertEquals(Principal::HREF, $principal->getType()); + $this->assertEquals('admin/', $principal->getHref()); + + } + + /** + * @depends testSimple + * @expectedException Sabre\DAV\Exception + */ + function testNoHref() { + + $principal = new Principal(Principal::HREF); + + } + + /** + * @depends testSimple + */ + function testSerializeUnAuthenticated() { + + $prin = new Principal(Principal::UNAUTHENTICATED); + + $xml = (new DAV\Server())->xml->write('{DAV:}principal', $prin); + + $this->assertXmlStringEqualsXmlString(' +<d:principal xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns"> +<d:unauthenticated/> +</d:principal>', $xml); + + } + + + /** + * @depends testSerializeUnAuthenticated + */ + function testSerializeAuthenticated() { + + $prin = new Principal(Principal::AUTHENTICATED); + $xml = (new DAV\Server())->xml->write('{DAV:}principal', $prin); + + $this->assertXmlStringEqualsXmlString(' +<d:principal xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns"> +<d:authenticated/> +</d:principal>', $xml); + + } + + + /** + * @depends testSerializeUnAuthenticated + */ + function testSerializeHref() { + + $prin = new Principal(Principal::HREF, 'principals/admin'); + $xml = (new DAV\Server())->xml->write('{DAV:}principal', $prin, '/'); + + $this->assertXmlStringEqualsXmlString(' +<d:principal xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns"> +<d:href>/principals/admin/</d:href> +</d:principal>', $xml); + + } + + function testUnserializeHref() { + + $xml = '<?xml version="1.0"?> +<d:principal xmlns:d="DAV:">' . +'<d:href>/principals/admin</d:href>' . +'</d:principal>'; + + $principal = $this->parse($xml); + $this->assertEquals(Principal::HREF, $principal->getType()); + $this->assertEquals('/principals/admin/', $principal->getHref()); + + } + + function testUnserializeAuthenticated() { + + $xml = '<?xml version="1.0"?> +<d:principal xmlns:d="DAV:">' . +' <d:authenticated />' . +'</d:principal>'; + + $principal = $this->parse($xml); + $this->assertEquals(Principal::AUTHENTICATED, $principal->getType()); + + } + + function testUnserializeUnauthenticated() { + + $xml = '<?xml version="1.0"?> +<d:principal xmlns:d="DAV:">' . +' <d:unauthenticated />' . +'</d:principal>'; + + $principal = $this->parse($xml); + $this->assertEquals(Principal::UNAUTHENTICATED, $principal->getType()); + + } + + /** + * @expectedException Sabre\DAV\Exception\BadRequest + */ + function testUnserializeUnknown() { + + $xml = '<?xml version="1.0"?> +<d:principal xmlns:d="DAV:">' . +' <d:foo />' . +'</d:principal>'; + + $this->parse($xml); + + } + + function parse($xml) { + + $reader = new Reader(); + $reader->elementMap['{DAV:}principal'] = 'Sabre\\DAVACL\\Xml\\Property\\Principal'; + $reader->xml($xml); + $result = $reader->parse(); + return $result['value']; + + } + + /** + * @depends testSimple + * @dataProvider htmlProvider + */ + function testToHtml($principal, $output) { + + $html = $principal->toHtml(new HtmlOutputHelper('/', [])); + + $this->assertXmlStringEqualsXmlString( + $output, + $html + ); + + } + + /** + * Provides data for the html tests + * + * @return array + */ + function htmlProvider() { + + return [ + [ + new Principal(Principal::UNAUTHENTICATED), + '<em>unauthenticated</em>', + ], + [ + new Principal(Principal::AUTHENTICATED), + '<em>authenticated</em>', + ], + [ + new Principal(Principal::ALL), + '<em>all</em>', + ], + [ + new Principal(Principal::HREF, 'principals/admin'), + '<a href="/principals/admin/">/principals/admin/</a>', + ], + + ]; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Xml/Property/SupportedPrivilegeSetTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Xml/Property/SupportedPrivilegeSetTest.php new file mode 100644 index 00000000000..749d349fc7d --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Xml/Property/SupportedPrivilegeSetTest.php @@ -0,0 +1,103 @@ +<?php + +namespace Sabre\DAVACL\Xml\Property; + +use Sabre\DAV; +use Sabre\DAV\Browser\HtmlOutputHelper; +use Sabre\HTTP; + +class SupportedPrivilegeSetTest extends \PHPUnit_Framework_TestCase { + + function testSimple() { + + $prop = new SupportedPrivilegeSet([ + 'privilege' => '{DAV:}all', + ]); + $this->assertInstanceOf('Sabre\DAVACL\Xml\Property\SupportedPrivilegeSet', $prop); + + } + + + /** + * @depends testSimple + */ + function testSerializeSimple() { + + $prop = new SupportedPrivilegeSet([]); + + $xml = (new DAV\Server())->xml->write('{DAV:}supported-privilege-set', $prop); + + $this->assertXmlStringEqualsXmlString(' +<d:supported-privilege-set xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns"> + <d:supported-privilege> + <d:privilege> + <d:all/> + </d:privilege> + </d:supported-privilege> +</d:supported-privilege-set>', $xml); + + } + + /** + * @depends testSimple + */ + function testSerializeAggregate() { + + $prop = new SupportedPrivilegeSet([ + '{DAV:}read' => [], + '{DAV:}write' => [ + 'description' => 'booh', + ] + ]); + + $xml = (new DAV\Server())->xml->write('{DAV:}supported-privilege-set', $prop); + + $this->assertXmlStringEqualsXmlString(' +<d:supported-privilege-set xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns"> + <d:supported-privilege> + <d:privilege> + <d:all/> + </d:privilege> + <d:supported-privilege> + <d:privilege> + <d:read/> + </d:privilege> + </d:supported-privilege> + <d:supported-privilege> + <d:privilege> + <d:write/> + </d:privilege> + <d:description>booh</d:description> + </d:supported-privilege> + </d:supported-privilege> +</d:supported-privilege-set>', $xml); + + } + + function testToHtml() { + + $prop = new SupportedPrivilegeSet([ + '{DAV:}read' => [], + '{DAV:}write' => [ + 'description' => 'booh', + ], + ]); + $html = new HtmlOutputHelper( + '/base/', + ['DAV:' => 'd'] + ); + + $expected = <<<HTML +<ul class="tree"><li><span title="{DAV:}all">d:all</span> +<ul> +<li><span title="{DAV:}read">d:read</span></li> +<li><span title="{DAV:}write">d:write</span> booh</li> +</ul></li> +</ul> + +HTML; + + $this->assertEquals($expected, $prop->toHtml($html)); + + } +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Xml/Request/AclPrincipalPropSetReportTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Xml/Request/AclPrincipalPropSetReportTest.php new file mode 100644 index 00000000000..bae682f2155 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Xml/Request/AclPrincipalPropSetReportTest.php @@ -0,0 +1,30 @@ +<?php + +namespace Sabre\DAVACL\Xml\Request; + +class AclPrincipalPropSetReportTest extends \Sabre\DAV\Xml\XmlTest { + + protected $elementMap = [ + + '{DAV:}acl-principal-prop-set' => 'Sabre\DAVACL\Xml\Request\AclPrincipalPropSetReport', + + ]; + + function testDeserialize() { + + $xml = <<<XML +<?xml version="1.0" encoding="utf-8" ?> +<D:acl-principal-prop-set xmlns:D="DAV:"> + <D:prop> + <D:displayname/> + </D:prop> +</D:acl-principal-prop-set> +XML; + + $result = $this->parse($xml); + + $this->assertEquals(['{DAV:}displayname'], $result['value']->properties); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Xml/Request/PrincipalMatchReportTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Xml/Request/PrincipalMatchReportTest.php new file mode 100644 index 00000000000..1431ab34939 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVACL/Xml/Request/PrincipalMatchReportTest.php @@ -0,0 +1,51 @@ +<?php + +namespace Sabre\DAVACL\Xml\Request; + +class PrincipalMatchReportTest extends \Sabre\DAV\Xml\XmlTest { + + protected $elementMap = [ + + '{DAV:}principal-match' => 'Sabre\DAVACL\Xml\Request\PrincipalMatchReport', + + ]; + + function testDeserialize() { + + $xml = <<<XML +<?xml version="1.0" encoding="utf-8" ?> + <D:principal-match xmlns:D="DAV:"> + <D:principal-property> + <D:owner/> + </D:principal-property> + </D:principal-match> +XML; + + $result = $this->parse($xml); + + $this->assertEquals(PrincipalMatchReport::PRINCIPAL_PROPERTY, $result['value']->type); + $this->assertEquals('{DAV:}owner', $result['value']->principalProperty); + + } + + function testDeserializeSelf() { + + $xml = <<<XML +<?xml version="1.0" encoding="utf-8" ?> + <D:principal-match xmlns:D="DAV:"> + <D:self /> + <D:prop> + <D:foo /> + </D:prop> + </D:principal-match> +XML; + + $result = $this->parse($xml); + + $this->assertEquals(PrincipalMatchReport::SELF, $result['value']->type); + $this->assertNull($result['value']->principalProperty); + $this->assertEquals(['{DAV:}foo'], $result['value']->properties); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVServerTest.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVServerTest.php new file mode 100644 index 00000000000..35f240d23fa --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/DAVServerTest.php @@ -0,0 +1,306 @@ +<?php + +namespace Sabre; + +use Sabre\HTTP\Request; +use Sabre\HTTP\Response; +use Sabre\HTTP\Sapi; + +/** + * This class may be used as a basis for other webdav-related unittests. + * + * This class is supposed to provide a reasonably big framework to quickly get + * a testing environment running. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +abstract class DAVServerTest extends \PHPUnit_Framework_TestCase { + + protected $setupCalDAV = false; + protected $setupCardDAV = false; + protected $setupACL = false; + protected $setupCalDAVSharing = false; + protected $setupCalDAVScheduling = false; + protected $setupCalDAVSubscriptions = false; + protected $setupCalDAVICSExport = false; + protected $setupLocks = false; + protected $setupFiles = false; + protected $setupSharing = false; + protected $setupPropertyStorage = false; + + /** + * An array with calendars. Every calendar should have + * - principaluri + * - uri + */ + protected $caldavCalendars = []; + protected $caldavCalendarObjects = []; + + protected $carddavAddressBooks = []; + protected $carddavCards = []; + + /** + * @var Sabre\DAV\Server + */ + protected $server; + protected $tree = []; + + protected $caldavBackend; + protected $carddavBackend; + protected $principalBackend; + protected $locksBackend; + protected $propertyStorageBackend; + + /** + * @var Sabre\CalDAV\Plugin + */ + protected $caldavPlugin; + + /** + * @var Sabre\CardDAV\Plugin + */ + protected $carddavPlugin; + + /** + * @var Sabre\DAVACL\Plugin + */ + protected $aclPlugin; + + /** + * @var Sabre\CalDAV\SharingPlugin + */ + protected $caldavSharingPlugin; + + /** + * CalDAV scheduling plugin + * + * @var CalDAV\Schedule\Plugin + */ + protected $caldavSchedulePlugin; + + /** + * @var Sabre\DAV\Auth\Plugin + */ + protected $authPlugin; + + /** + * @var Sabre\DAV\Locks\Plugin + */ + protected $locksPlugin; + + /** + * Sharing plugin. + * + * @var \Sabre\DAV\Sharing\Plugin + */ + protected $sharingPlugin; + + /* + * @var Sabre\DAV\PropertyStorage\Plugin + */ + protected $propertyStoragePlugin; + + /** + * If this string is set, we will automatically log in the user with this + * name. + */ + protected $autoLogin = null; + + function setUp() { + + $this->initializeEverything(); + + } + + function initializeEverything() { + + $this->setUpBackends(); + $this->setUpTree(); + + $this->server = new DAV\Server($this->tree); + $this->server->sapi = new HTTP\SapiMock(); + $this->server->debugExceptions = true; + + if ($this->setupCalDAV) { + $this->caldavPlugin = new CalDAV\Plugin(); + $this->server->addPlugin($this->caldavPlugin); + } + if ($this->setupCalDAVSharing || $this->setupSharing) { + $this->sharingPlugin = new DAV\Sharing\Plugin(); + $this->server->addPlugin($this->sharingPlugin); + } + if ($this->setupCalDAVSharing) { + $this->caldavSharingPlugin = new CalDAV\SharingPlugin(); + $this->server->addPlugin($this->caldavSharingPlugin); + } + if ($this->setupCalDAVScheduling) { + $this->caldavSchedulePlugin = new CalDAV\Schedule\Plugin(); + $this->server->addPlugin($this->caldavSchedulePlugin); + } + if ($this->setupCalDAVSubscriptions) { + $this->server->addPlugin(new CalDAV\Subscriptions\Plugin()); + } + if ($this->setupCalDAVICSExport) { + $this->caldavICSExportPlugin = new CalDAV\ICSExportPlugin(); + $this->server->addPlugin($this->caldavICSExportPlugin); + } + if ($this->setupCardDAV) { + $this->carddavPlugin = new CardDAV\Plugin(); + $this->server->addPlugin($this->carddavPlugin); + } + if ($this->setupLocks) { + $this->locksPlugin = new DAV\Locks\Plugin( + $this->locksBackend + ); + $this->server->addPlugin($this->locksPlugin); + } + if ($this->setupPropertyStorage) { + $this->propertyStoragePlugin = new DAV\PropertyStorage\Plugin( + $this->propertyStorageBackend + ); + $this->server->addPlugin($this->propertyStoragePlugin); + } + if ($this->autoLogin) { + $this->autoLogin($this->autoLogin); + } + if ($this->setupACL) { + $this->aclPlugin = new DAVACL\Plugin(); + if (!$this->autoLogin) { + $this->aclPlugin->allowUnauthenticatedAccess = false; + } + $this->aclPlugin->adminPrincipals = ['principals/admin']; + $this->server->addPlugin($this->aclPlugin); + } + + } + + /** + * Makes a request, and returns a response object. + * + * You can either pass an instance of Sabre\HTTP\Request, or an array, + * which will then be used as the _SERVER array. + * + * If $expectedStatus is set, we'll compare it with the HTTP status of + * the returned response. If it doesn't match, we'll immediately fail + * the test. + * + * @param array|\Sabre\HTTP\Request $request + * @param int $expectedStatus + * @return \Sabre\HTTP\Response + */ + function request($request, $expectedStatus = null) { + + if (is_array($request)) { + $request = HTTP\Request::createFromServerArray($request); + } + $response = new HTTP\ResponseMock(); + + $this->server->httpRequest = $request; + $this->server->httpResponse = $response; + $this->server->exec(); + + if ($expectedStatus) { + $responseBody = $expectedStatus !== $response->getStatus() ? $response->getBodyAsString() : ''; + $this->assertEquals($expectedStatus, $response->getStatus(), 'Incorrect HTTP status received for request. Response body: ' . $responseBody); + } + return $this->server->httpResponse; + + } + + /** + * This function takes a username and sets the server in a state where + * this user is logged in, and no longer requires an authentication check. + * + * @param string $userName + */ + function autoLogin($userName) { + $authBackend = new DAV\Auth\Backend\Mock(); + $authBackend->setPrincipal('principals/' . $userName); + $this->authPlugin = new DAV\Auth\Plugin($authBackend); + + // If the auth plugin already exists, we're removing its hooks: + if ($oldAuth = $this->server->getPlugin('auth')) { + $this->server->removeListener('beforeMethod', [$oldAuth, 'beforeMethod']); + } + $this->server->addPlugin($this->authPlugin); + + // This will trigger the actual login procedure + $this->authPlugin->beforeMethod(new Request(), new Response()); + } + + /** + * Override this to provide your own Tree for your test-case. + */ + function setUpTree() { + + if ($this->setupCalDAV) { + $this->tree[] = new CalDAV\CalendarRoot( + $this->principalBackend, + $this->caldavBackend + ); + } + if ($this->setupCardDAV) { + $this->tree[] = new CardDAV\AddressBookRoot( + $this->principalBackend, + $this->carddavBackend + ); + } + + if ($this->setupCalDAV) { + $this->tree[] = new CalDAV\Principal\Collection( + $this->principalBackend + ); + } elseif ($this->setupCardDAV || $this->setupACL) { + $this->tree[] = new DAVACL\PrincipalCollection( + $this->principalBackend + ); + } + if ($this->setupFiles) { + + $this->tree[] = new DAV\Mock\Collection('files'); + + } + + } + + function setUpBackends() { + + if ($this->setupCalDAVSharing && is_null($this->caldavBackend)) { + $this->caldavBackend = new CalDAV\Backend\MockSharing($this->caldavCalendars, $this->caldavCalendarObjects); + } + if ($this->setupCalDAVSubscriptions && is_null($this->caldavBackend)) { + $this->caldavBackend = new CalDAV\Backend\MockSubscriptionSupport($this->caldavCalendars, $this->caldavCalendarObjects); + } + if ($this->setupCalDAV && is_null($this->caldavBackend)) { + if ($this->setupCalDAVScheduling) { + $this->caldavBackend = new CalDAV\Backend\MockScheduling($this->caldavCalendars, $this->caldavCalendarObjects); + } else { + $this->caldavBackend = new CalDAV\Backend\Mock($this->caldavCalendars, $this->caldavCalendarObjects); + } + } + if ($this->setupCardDAV && is_null($this->carddavBackend)) { + $this->carddavBackend = new CardDAV\Backend\Mock($this->carddavAddressBooks, $this->carddavCards); + } + if ($this->setupCardDAV || $this->setupCalDAV || $this->setupACL) { + $this->principalBackend = new DAVACL\PrincipalBackend\Mock(); + } + if ($this->setupLocks) { + $this->locksBackend = new DAV\Locks\Backend\Mock(); + } + if ($this->setupPropertyStorage) { + $this->propertyStorageBackend = new DAV\PropertyStorage\Backend\Mock(); + } + + } + + + function assertHttpStatus($expectedStatus, HTTP\Request $req) { + + $resp = $this->request($req); + $this->assertEquals((int)$expectedStatus, (int)$resp->status, 'Incorrect HTTP status received: ' . $resp->body); + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/HTTP/ResponseMock.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/HTTP/ResponseMock.php new file mode 100644 index 00000000000..eb486bf5b89 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/HTTP/ResponseMock.php @@ -0,0 +1,22 @@ +<?php + +namespace Sabre\HTTP; + +/** + * HTTP Response Mock object + * + * This class exists to make the transition to sabre/http easier. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class ResponseMock extends Response { + + /** + * Making these public. + */ + public $body; + public $status; + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/HTTP/SapiMock.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/HTTP/SapiMock.php new file mode 100644 index 00000000000..e2888a9da79 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/HTTP/SapiMock.php @@ -0,0 +1,30 @@ +<?php + +namespace Sabre\HTTP; + +/** + * HTTP Response Mock object + * + * This class exists to make the transition to sabre/http easier. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class SapiMock extends Sapi { + + static $sent = 0; + + /** + * Overriding this so nothing is ever echo'd. + * + * @param ResponseInterface $response + * @return void + */ + static function sendResponse(ResponseInterface $response) { + + self::$sent++; + + } + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/Sabre/TestUtil.php b/htdocs/includes/sabre/sabre/dav/tests/Sabre/TestUtil.php new file mode 100644 index 00000000000..9df94915fb2 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/Sabre/TestUtil.php @@ -0,0 +1,71 @@ +<?php + +namespace Sabre; + +class TestUtil { + + /** + * This function deletes all the contents of the temporary directory. + * + * @return void + */ + static function clearTempDir() { + + self::deleteTree(SABRE_TEMPDIR, false); + + } + + + private static function deleteTree($path, $deleteRoot = true) { + + foreach (scandir($path) as $node) { + + if ($node == '.' || $node == '..') continue; + $myPath = $path . '/' . $node; + if (is_file($myPath)) { + unlink($myPath); + } else { + self::deleteTree($myPath); + } + + } + if ($deleteRoot) { + rmdir($path); + } + + } + + static function getMySQLDB() { + + try { + $pdo = new \PDO(SABRE_MYSQLDSN, SABRE_MYSQLUSER, SABRE_MYSQLPASS); + $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + return $pdo; + } catch (\PDOException $e) { + return null; + } + + } + + static function getSQLiteDB() { + + $pdo = new \PDO('sqlite:' . SABRE_TEMPDIR . '/pdobackend'); + $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + return $pdo; + + } + + static function getPgSqlDB() { + + //try { + $pdo = new \PDO(SABRE_PGSQLDSN); + $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + return $pdo; + //} catch (\PDOException $e) { + // return null; + //} + + } + + +} diff --git a/htdocs/includes/sabre/sabre/dav/tests/bootstrap.php b/htdocs/includes/sabre/sabre/dav/tests/bootstrap.php new file mode 100644 index 00000000000..26eb32aa284 --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/bootstrap.php @@ -0,0 +1,38 @@ +<?php + +set_include_path(__DIR__ . '/../lib/' . PATH_SEPARATOR . __DIR__ . PATH_SEPARATOR . get_include_path()); + +$autoLoader = include __DIR__ . '/../vendor/autoload.php'; + +// SabreDAV tests auto loading +$autoLoader->add('Sabre\\', __DIR__); +// VObject tests auto loading +$autoLoader->addPsr4('Sabre\\VObject\\', __DIR__ . '/../vendor/sabre/vobject/tests/VObject'); +$autoLoader->addPsr4('Sabre\\Xml\\', __DIR__ . '/../vendor/sabre/xml/tests/Sabre/Xml'); + +date_default_timezone_set('UTC'); + +$config = [ + 'SABRE_TEMPDIR' => dirname(__FILE__) . '/temp/', + 'SABRE_HASSQLITE' => in_array('sqlite', PDO::getAvailableDrivers()), + 'SABRE_HASMYSQL' => in_array('mysql', PDO::getAvailableDrivers()), + 'SABRE_HASPGSQL' => in_array('pgsql', PDO::getAvailableDrivers()), + 'SABRE_MYSQLDSN' => 'mysql:host=127.0.0.1;dbname=sabredav_test', + 'SABRE_MYSQLUSER' => 'sabredav', + 'SABRE_MYSQLPASS' => '', + 'SABRE_PGSQLDSN' => 'pgsql:host=localhost;dbname=sabredav_test;user=sabredav;password=sabredav', +]; + +if (file_exists(__DIR__ . '/config.user.php')) { + include __DIR__ . '/config.user.php'; + foreach ($userConfig as $key => $value) { + $config[$key] = $value; + } +} + +foreach ($config as $key => $value) { + if (!defined($key)) define($key, $value); +} + +if (!file_exists(SABRE_TEMPDIR)) mkdir(SABRE_TEMPDIR); +if (file_exists('.sabredav')) unlink('.sabredav'); diff --git a/htdocs/includes/sabre/sabre/dav/tests/phpcs/ruleset.xml b/htdocs/includes/sabre/sabre/dav/tests/phpcs/ruleset.xml new file mode 100644 index 00000000000..ec2c4c84b1d --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/phpcs/ruleset.xml @@ -0,0 +1,57 @@ +<?xml version="1.0"?> +<ruleset name="sabre.php"> + <description>sabre.io codesniffer ruleset</description> + + <!-- Include the whole PSR-1 standard --> + <rule ref="PSR1" /> + + <!-- All PHP files MUST use the Unix LF (linefeed) line ending. --> + <rule ref="Generic.Files.LineEndings"> + <properties> + <property name="eolChar" value="\n"/> + </properties> + </rule> + + <!-- The closing ?> tag MUST be omitted from files containing only PHP. --> + <rule ref="Zend.Files.ClosingTag"/> + + <!-- There MUST NOT be trailing whitespace at the end of non-blank lines. --> + <rule ref="Squiz.WhiteSpace.SuperfluousWhitespace"> + <properties> + <property name="ignoreBlankLines" value="true"/> + </properties> + </rule> + + <!-- There MUST NOT be more than one statement per line. --> + <rule ref="Generic.Formatting.DisallowMultipleStatements"/> + + <rule ref="Generic.WhiteSpace.ScopeIndent"> + <properties> + <property name="ignoreIndentationTokens" type="array" value="T_COMMENT,T_DOC_COMMENT"/> + </properties> + </rule> + <rule ref="Generic.WhiteSpace.DisallowTabIndent"/> + + <!-- PHP keywords MUST be in lower case. --> + <rule ref="Generic.PHP.LowerCaseKeyword"/> + + <!-- The PHP constants true, false, and null MUST be in lower case. --> + <rule ref="Generic.PHP.LowerCaseConstant"/> + + <!-- <rule ref="Squiz.Scope.MethodScope"/> --> + <rule ref="Squiz.WhiteSpace.ScopeKeywordSpacing"/> + + <!-- In the argument list, there MUST NOT be a space before each comma, and there MUST be one space after each comma. --> + <!-- + <rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing"> + <properties> + <property name="equalsSpacing" value="1"/> + </properties> + </rule> + <rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing.SpacingAfterHint"> + <severity>0</severity> + </rule> + --> + <rule ref="PEAR.WhiteSpace.ScopeClosingBrace"/> + +</ruleset> diff --git a/htdocs/includes/sabre/sabre/dav/tests/phpunit.xml.dist b/htdocs/includes/sabre/sabre/dav/tests/phpunit.xml.dist new file mode 100644 index 00000000000..453fabb82ad --- /dev/null +++ b/htdocs/includes/sabre/sabre/dav/tests/phpunit.xml.dist @@ -0,0 +1,46 @@ +<?xml version="1.0"?> +<phpunit + colors="true" + bootstrap="bootstrap.php" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" + beStrictAboutTestsThatDoNotTestAnything="true" + beStrictAboutOutputDuringTests="true" + beStrictAboutTestSize="true"> + + <testsuite name="sabre-event"> + <directory>../vendor/sabre/event/tests/</directory> + </testsuite> + <testsuite name="sabre-uri"> + <directory>../vendor/sabre/uri/tests/</directory> + </testsuite> + <testsuite name="sabre-xml"> + <directory>../vendor/sabre/xml/tests/Sabre/Xml/</directory> + </testsuite> + <testsuite name="sabre-http"> + <directory>../vendor/sabre/http/tests/HTTP</directory> + </testsuite> + <testsuite name="sabre-vobject"> + <directory>../vendor/sabre/vobject/tests/VObject</directory> + </testsuite> + + <testsuite name="sabre-dav"> + <directory>Sabre/DAV</directory> + </testsuite> + <testsuite name="sabre-davacl"> + <directory>Sabre/DAVACL</directory> + </testsuite> + <testsuite name="sabre-caldav"> + <directory>Sabre/CalDAV</directory> + </testsuite> + <testsuite name="sabre-carddav"> + <directory>Sabre/CardDAV</directory> + </testsuite> + + <filter> + <whitelist addUncoveredFilesFromWhitelist="true"> + <directory suffix=".php">../lib/</directory> + </whitelist> + </filter> +</phpunit> diff --git a/htdocs/includes/sabre/sabre/event/.gitignore b/htdocs/includes/sabre/sabre/event/.gitignore new file mode 100644 index 00000000000..d06a78164db --- /dev/null +++ b/htdocs/includes/sabre/sabre/event/.gitignore @@ -0,0 +1,14 @@ +#composer +vendor +composer.lock + +#binaries +bin/sabre-cs-fixer +bin/php-cs-fixer +bin/phpunit + +#vim lock files +.*.swp + +#development stuff +tests/cov diff --git a/htdocs/includes/sabre/sabre/event/.travis.yml b/htdocs/includes/sabre/sabre/event/.travis.yml new file mode 100644 index 00000000000..b6719f591f7 --- /dev/null +++ b/htdocs/includes/sabre/sabre/event/.travis.yml @@ -0,0 +1,26 @@ +language: php +php: + - 5.5 + - 5.6 + - 7 + - hhvm + +matrix: + allow_failures: + - php: hhvm + +env: + matrix: + - LOWEST_DEPS="" + - LOWEST_DEPS="--prefer-lowest" + +before_script: + - composer update --prefer-source $LOWEST_DEPS + +script: + - ./bin/phpunit + - ./bin/sabre-cs-fixer fix . --dry-run --diff + +sudo: false + +cache: vendor diff --git a/htdocs/includes/sabre/sabre/event/CHANGELOG.md b/htdocs/includes/sabre/sabre/event/CHANGELOG.md new file mode 100644 index 00000000000..9d6d7cfaaf8 --- /dev/null +++ b/htdocs/includes/sabre/sabre/event/CHANGELOG.md @@ -0,0 +1,78 @@ +ChangeLog +========= + +3.0.0 (2015-11-05) +------------------ + +* Now requires PHP 5.5! +* `Promise::all()` is moved to `Promise\all()`. +* Aside from the `Promise\all()` function, there's now also `Promise\race()`. +* `Promise\reject()` and `Promise\resolve()` have also been added. +* Now 100% compatible with the Ecmascript 6 Promise. + + +3.0.0-alpha1 (2015-10-23) +------------------------- + +* This package now requires PHP 5.5. +* #26: Added an event loop implementation. Also knows as the Reactor Pattern. +* Renamed `Promise::error` to `Promise::otherwise` to be consistent with + ReactPHP and Guzzle. The `error` method is kept for BC but will be removed + in a future version. +* #27: Support for Promise-based coroutines via the `Sabre\Event\coroutine` + function. +* BC Break: Promises now use the EventLoop to run "then"-events in a separate + execution context. In practise that means you need to run the event loop to + wait for any `then`/`otherwise` callbacks to trigger. +* Promises now have a `wait()` method. Allowing you to make a promise + synchronous and simply wait for a result (or exception) to happen. + + +2.0.2 (2015-05-19) +------------------ + +* This release has no functional changes. It's just been brought up to date + with the latest coding standards. + + +2.0.1 (2014-10-06) +------------------ + +* Fixed: `$priority` was ignored in `EventEmitter::once` method. +* Fixed: Breaking the event chain was not possible in `EventEmitter::once`. + + +2.0.0 (2014-06-21) +------------------ + +* Added: When calling emit, it's now possible to specify a callback that will be + triggered after each method handled. This is dubbed the 'continueCallback' and + can be used to implement strategy patterns. +* Added: Promise object! +* Changed: EventEmitter::listeners now returns just the callbacks for an event, + and no longer returns the list by reference. The list is now automatically + sorted by priority. +* Update: Speed improvements. +* Updated: It's now possible to remove all listeners for every event. +* Changed: Now uses psr-4 autoloading. + + +1.0.1 (2014-06-12) +------------------ + +* hhvm compatible! +* Fixed: Issue #4. Compatiblitiy for PHP < 5.4.14. + + +1.0.0 (2013-07-19) +------------------ + +* Added: removeListener, removeAllListeners +* Added: once, to only listen to an event emitting once. +* Added README.md. + + +0.0.1-alpha (2013-06-29) +------------------------ + +* First version! diff --git a/htdocs/includes/sabre/sabre/event/LICENSE b/htdocs/includes/sabre/sabre/event/LICENSE new file mode 100644 index 00000000000..9a495cef0a1 --- /dev/null +++ b/htdocs/includes/sabre/sabre/event/LICENSE @@ -0,0 +1,27 @@ +Copyright (C) 2013-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/htdocs/includes/sabre/sabre/event/README.md b/htdocs/includes/sabre/sabre/event/README.md new file mode 100644 index 00000000000..364906fd496 --- /dev/null +++ b/htdocs/includes/sabre/sabre/event/README.md @@ -0,0 +1,50 @@ +sabre/event +=========== + +A lightweight library for event-based development in PHP. + +This library provides the following event-based concepts: + +1. EventEmitter. +2. Promises. +3. An event loop. +4. Co-routines. + +Full documentation can be found on [the website][1]. + +Installation +------------ + +Make sure you have [composer][3] installed, and then run: + + composer require sabre/event "~3.0.0" + +This package requires PHP 5.5. The 2.0 branch is still maintained as well, and +supports PHP 5.4. + +Build status +------------ + +| branch | status | +| ------ | ------ | +| master | [![Build Status](https://travis-ci.org/fruux/sabre-event.svg?branch=master)](https://travis-ci.org/fruux/sabre-event) | +| 2.0 | [![Build Status](https://travis-ci.org/fruux/sabre-event.svg?branch=2.0)](https://travis-ci.org/fruux/sabre-event) | +| 1.0 | [![Build Status](https://travis-ci.org/fruux/sabre-event.svg?branch=1.0)](https://travis-ci.org/fruux/sabre-event) | +| php53 | [![Build Status](https://travis-ci.org/fruux/sabre-event.svg?branch=php53)](https://travis-ci.org/fruux/sabre-event) | + + +Questions? +---------- + +Head over to the [sabre/dav mailinglist][4], or you can also just open a ticket +on [GitHub][5]. + +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://sabre.io/event/ +[3]: http://getcomposer.org/ +[4]: http://groups.google.com/group/sabredav-discuss +[5]: https://github.com/fruux/sabre-event/issues/ diff --git a/htdocs/includes/sabre/sabre/event/bin/.empty b/htdocs/includes/sabre/sabre/event/bin/.empty new file mode 100644 index 00000000000..e69de29bb2d diff --git a/htdocs/includes/sabre/sabre/event/composer.json b/htdocs/includes/sabre/sabre/event/composer.json new file mode 100644 index 00000000000..9a11b01aaa4 --- /dev/null +++ b/htdocs/includes/sabre/sabre/event/composer.json @@ -0,0 +1,47 @@ +{ + "name": "sabre/event", + "description": "sabre/event is a library for lightweight event-based programming", + "keywords": [ + "Events", + "EventEmitter", + "Promise", + "Hooks", + "Plugin", + "Signal", + "Async" + ], + "homepage": "http://sabre.io/event/", + "license": "BSD-3-Clause", + "require": { + "php": ">=5.5" + }, + "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" + ] + }, + "require-dev": { + "sabre/cs": "~0.0.4", + "phpunit/phpunit" : "*" + }, + "config" : { + "bin-dir" : "bin/" + } +} diff --git a/htdocs/includes/sabre/sabre/event/examples/promise.php b/htdocs/includes/sabre/sabre/event/examples/promise.php new file mode 100644 index 00000000000..b40227e15dd --- /dev/null +++ b/htdocs/includes/sabre/sabre/event/examples/promise.php @@ -0,0 +1,100 @@ +#!/usr/bin/env php +<?php + +use Sabre\Event\Promise; +use Sabre\Event\Loop; +use function Sabre\Event\coroutine; + +require __DIR__ . '/../vendor/autoload.php'; + +/** + * This example shows demonstrates the Promise api. + */ + + +/* Creating a new promise */ +$promise = new Promise(); + +/* After 2 seconds we fulfill it */ +Loop\setTimeout(function() use ($promise) { + + echo "Step 1\n"; + $promise->fulfill("hello"); + +}, 2); + + +/* Callback chain */ + +$result = $promise + ->then(function($value) { + + echo "Step 2\n"; + // Immediately returning a new value. + return $value . " world"; + + }) + ->then(function($value) { + + echo "Step 3\n"; + // This 'then' returns a new promise which we resolve later. + $promise = new Promise(); + + // Resolving after 2 seconds + Loop\setTimeout(function() use ($promise, $value) { + + $promise->fulfill($value . ", how are ya?"); + + }, 2); + + return $promise; + }) + ->then(function($value) { + + echo "Step 4\n"; + // This is the final event handler. + return $value . " you rock!"; + + }) + // Making all async calls synchronous by waiting for the final result. + ->wait(); + +echo $result, "\n"; + +/* Now an identical example, this time with coroutines. */ + +$result = coroutine(function() { + + $promise = new Promise(); + + /* After 2 seconds we fulfill it */ + Loop\setTimeout(function() use ($promise) { + + echo "Step 1\n"; + $promise->fulfill("hello"); + + }, 2); + + $value = (yield $promise); + + echo "Step 2\n"; + $value .= ' world'; + + echo "Step 3\n"; + $promise = new Promise(); + Loop\setTimeout(function() use ($promise, $value) { + + $promise->fulfill($value . ", how are ya?"); + + }, 2); + + $value = (yield $promise); + + echo "Step 4\n"; + + // This is the final event handler. + yield $value . " you rock!"; + +})->wait(); + +echo $result, "\n"; diff --git a/htdocs/includes/sabre/sabre/event/examples/tail.php b/htdocs/includes/sabre/sabre/event/examples/tail.php new file mode 100644 index 00000000000..d4e82206b52 --- /dev/null +++ b/htdocs/includes/sabre/sabre/event/examples/tail.php @@ -0,0 +1,28 @@ +#!/usr/bin/env php +<?php + +/** + * This example can be used to logfile processing and basically wraps the tail + * command. + * + * The benefit of using this, is that it allows you to tail multiple logs at + * the same time + * + * To stop this application, hit CTRL-C + */ +if ($argc < 2) { + echo "Usage: " . $argv[0] . " filename\n"; + exit(1); +} + +require __DIR__ . '/../vendor/autoload.php'; + +$tail = popen('tail -fn0 ' . escapeshellarg($argv[1]), 'r'); + +\Sabre\Event\Loop\addReadStream($tail, function() use ($tail) { + + echo fread($tail, 4096); + +}); + +$loop->run(); diff --git a/htdocs/includes/sabre/sabre/event/lib/EventEmitter.php b/htdocs/includes/sabre/sabre/event/lib/EventEmitter.php new file mode 100644 index 00000000000..1bb1c3cf92b --- /dev/null +++ b/htdocs/includes/sabre/sabre/event/lib/EventEmitter.php @@ -0,0 +1,18 @@ +<?php + +namespace Sabre\Event; + +/** + * EventEmitter object. + * + * Instantiate this class, or subclass it for easily creating event emitters. + * + * @copyright Copyright (C) 2013-2015 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class EventEmitter implements EventEmitterInterface { + + use EventEmitterTrait; + +} diff --git a/htdocs/includes/sabre/sabre/event/lib/EventEmitterInterface.php b/htdocs/includes/sabre/sabre/event/lib/EventEmitterInterface.php new file mode 100644 index 00000000000..0e2be2cefb5 --- /dev/null +++ b/htdocs/includes/sabre/sabre/event/lib/EventEmitterInterface.php @@ -0,0 +1,100 @@ +<?php + +namespace Sabre\Event; + +/** + * Event Emitter Interface + * + * Anything that accepts listeners and emits events should implement this + * interface. + * + * @copyright Copyright (C) 2013-2015 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +interface EventEmitterInterface { + + /** + * Subscribe to an event. + * + * @param string $eventName + * @param callable $callBack + * @param int $priority + * @return void + */ + function on($eventName, callable $callBack, $priority = 100); + + /** + * Subscribe to an event exactly once. + * + * @param string $eventName + * @param callable $callBack + * @param int $priority + * @return void + */ + function once($eventName, callable $callBack, $priority = 100); + + /** + * Emits an event. + * + * This method will return true if 0 or more listeners were succesfully + * 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. + * + * @param string $eventName + * @param array $arguments + * @param callback $continueCallBack + * @return bool + */ + function emit($eventName, array $arguments = [], callable $continueCallBack = null); + + /** + * 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. + * + * @param string $eventName + * @return callable[] + */ + function listeners($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. + * + * @param string $eventName + * @param callable $listener + * @return bool + */ + function removeListener($eventName, callable $listener); + + /** + * 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. + * + * @param string $eventName + * @return void + */ + function removeAllListeners($eventName = null); + +} diff --git a/htdocs/includes/sabre/sabre/event/lib/EventEmitterTrait.php b/htdocs/includes/sabre/sabre/event/lib/EventEmitterTrait.php new file mode 100644 index 00000000000..257629faee2 --- /dev/null +++ b/htdocs/includes/sabre/sabre/event/lib/EventEmitterTrait.php @@ -0,0 +1,211 @@ +<?php + +namespace Sabre\Event; + +/** + * Event Emitter Trait + * + * This trait contains all the basic functions to implement an + * EventEmitterInterface. + * + * Using the trait + interface allows you to add EventEmitter capabilities + * without having to change your base-class. + * + * @copyright Copyright (C) 2013-2015 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +trait EventEmitterTrait { + + /** + * The list of listeners + * + * @var array + */ + protected $listeners = []; + + /** + * Subscribe to an event. + * + * @param string $eventName + * @param callable $callBack + * @param int $priority + * @return void + */ + function on($eventName, callable $callBack, $priority = 100) { + + if (!isset($this->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. + * + * @param string $eventName + * @param callable $callBack + * @param int $priority + * @return void + */ + function once($eventName, callable $callBack, $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 succesfully + * 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. + * + * @param string $eventName + * @param array $arguments + * @param callback $continueCallBack + * @return bool + */ + function emit($eventName, array $arguments = [], callable $continueCallBack = null) { + + if (is_null($continueCallBack)) { + + foreach ($this->listeners($eventName) as $listener) { + + $result = call_user_func_array($listener, $arguments); + if ($result === false) { + return false; + } + } + + } else { + + $listeners = $this->listeners($eventName); + $counter = count($listeners); + + foreach ($listeners as $listener) { + + $counter--; + $result = call_user_func_array($listener, $arguments); + if ($result === false) { + 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. + * + * @param string $eventName + * @return callable[] + */ + function listeners($eventName) { + + 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. + * + * @param string $eventName + * @param callable $listener + * @return bool + */ + function removeListener($eventName, callable $listener) { + + 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. + * + * @param string $eventName + * @return void + */ + function removeAllListeners($eventName = null) { + + if (!is_null($eventName)) { + unset($this->listeners[$eventName]); + } else { + $this->listeners = []; + } + + } + +} diff --git a/htdocs/includes/sabre/sabre/event/lib/Loop/Loop.php b/htdocs/includes/sabre/sabre/event/lib/Loop/Loop.php new file mode 100644 index 00000000000..86ee7c8b084 --- /dev/null +++ b/htdocs/includes/sabre/sabre/event/lib/Loop/Loop.php @@ -0,0 +1,386 @@ +<?php + +namespace Sabre\Event\Loop; + +/** + * A simple eventloop implementation. + * + * This eventloop supports: + * * nextTick + * * setTimeout for delayed functions + * * setInterval for repeating functions + * * stream events using stream_select + * + * @copyright Copyright (C) 2007-2015 fruux GmbH. (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Loop { + + /** + * Executes a function after x seconds. + * + * @param callable $cb + * @param float $timeout timeout in seconds + * @return void + */ + function setTimeout(callable $cb, $timeout) { + + $triggerTime = microtime(true) + ($timeout); + + if (!$this->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 ($index === 0) { + 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. + * + * @param callable $cb + * @param float $timeout + * @return array + */ + function setInterval(callable $cb, $timeout) { + + $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 internval. + * + * @param array $intervalId + * @return void + */ + function clearInterval($intervalId) { + + $intervalId[1] = false; + + } + + /** + * Runs a function immediately at the next iteration of the loop. + * + * @param callable $cb + * @return void + */ + 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 + * @param callable $cb + * @return void + */ + 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 + * @param callable $cb + * @return void + */ + function addWriteStream($stream, callable $cb) { + + $this->writeStreams[(int)$stream] = $stream; + $this->writeCallbacks[(int)$stream] = $cb; + + } + + /** + * Stop watching a stream for reads. + * + * @param resource $stream + * @return void + */ + function removeReadStream($stream) { + + unset( + $this->readStreams[(int)$stream], + $this->readCallbacks[(int)$stream] + ); + + } + + /** + * Stop watching a stream for writes. + * + * @param resource $stream + * @return void + */ + function removeWriteStream($stream) { + + unset( + $this->writeStreams[(int)$stream], + $this->writeCallbacks[(int)$stream] + ); + + } + + + /** + * Runs the loop. + * + * This function will run continiously, until there's no more events to + * handle. + * + * @return void + */ + 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. + * + * @param bool $block + * @return bool + */ + function tick($block = false) { + + $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 + * + * @return void + */ + 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 + */ + 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 $timer[0] - microtime(true); + } + + } + + /** + * Runs all pending stream events. + * + * @param float $timeout + */ + protected function runStreams($timeout) { + + if ($this->readStreams || $this->writeStreams) { + + $read = $this->readStreams; + $write = $this->writeStreams; + $except = null; + if (stream_select($read, $write, $except, null, $timeout)) { + + // 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($timeout !== null ? $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 callback[] + */ + protected $readCallbacks = []; + + /** + * List of write callbacks, indexed by stream id. + * + * @var callback[] + */ + protected $writeCallbacks = []; + + +} diff --git a/htdocs/includes/sabre/sabre/event/lib/Loop/functions.php b/htdocs/includes/sabre/sabre/event/lib/Loop/functions.php new file mode 100644 index 00000000000..56c5bc8c74a --- /dev/null +++ b/htdocs/includes/sabre/sabre/event/lib/Loop/functions.php @@ -0,0 +1,183 @@ +<?php + +namespace Sabre\Event\Loop; + +/** + * Executes a function after x seconds. + * + * @param callable $cb + * @param float $timeout timeout in seconds + * @return void + */ +function setTimeout(callable $cb, $timeout) { + + instance()->setTimeout($cb, $timeout); + +} + +/** + * Executes a function every x seconds. + * + * The value this function returns can be used to stop the interval with + * clearInterval. + * + * @param callable $cb + * @param float $timeout + * @return array + */ +function setInterval(callable $cb, $timeout) { + + return instance()->setInterval($cb, $timeout); + +} + +/** + * Stops a running internval. + * + * @param array $intervalId + * @return void + */ +function clearInterval($intervalId) { + + instance()->clearInterval($intervalId); + +} + +/** + * Runs a function immediately at the next iteration of the loop. + * + * @param callable $cb + * @return void + */ +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 + * @param callable $cb + * @return void + */ +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 + * @param callable $cb + * @return void + */ +function addWriteStream($stream, callable $cb) { + + instance()->addWriteStream($stream, $cb); + +} + +/** + * Stop watching a stream for reads. + * + * @param resource $stream + * @return void + */ +function removeReadStream($stream) { + + instance()->removeReadStream($stream); + +} + +/** + * Stop watching a stream for writes. + * + * @param resource $stream + * @return void + */ +function removeWriteStream($stream) { + + instance()->removeWriteStream($stream); + +} + + +/** + * Runs the loop. + * + * This function will run continiously, until there's no more events to + * handle. + * + * @return void + */ +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. + * + * @param bool $block + * @return bool + */ +function tick($block = false) { + + return instance()->tick($block); + +} + +/** + * Stops a running eventloop + * + * @return void + */ +function stop() { + + instance()->stop(); + +} + +/** + * Retrieves or sets the global Loop object. + * + * @param Loop $newLoop + */ +function instance(Loop $newLoop = null) { + + static $loop; + if ($newLoop) { + $loop = $newLoop; + } elseif (!$loop) { + $loop = new Loop(); + } + return $loop; + +} diff --git a/htdocs/includes/sabre/sabre/event/lib/Promise.php b/htdocs/includes/sabre/sabre/event/lib/Promise.php new file mode 100644 index 00000000000..1c874c1bda3 --- /dev/null +++ b/htdocs/includes/sabre/sabre/event/lib/Promise.php @@ -0,0 +1,320 @@ +<?php + +namespace Sabre\Event; + +use Exception; + +/** + * An implementation of the Promise pattern. + * + * A promise represents the result of an asynchronous operation. + * At any given point a promise can be in one of three states: + * + * 1. Pending (the promise does not have a result yet). + * 2. Fulfilled (the asynchronous operation has completed with a result). + * 3. Rejected (the asynchronous operation has completed with an error). + * + * To get a callback when the operation has finished, use the `then` method. + * + * @copyright Copyright (C) 2013-2015 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Promise { + + /** + * The asynchronous operation is pending. + */ + const PENDING = 0; + + /** + * The asynchronous operation has completed, and has a result. + */ + const FULFILLED = 1; + + /** + * The asynchronous operation has completed with an error. + */ + const REJECTED = 2; + + /** + * The current state of this promise. + * + * @var int + */ + public $state = self::PENDING; + + /** + * Creates the promise. + * + * The passed argument is the executor. The executor is automatically + * called with two arguments. + * + * Each are callbacks that map to $this->fulfill and $this->reject. + * Using the executor is optional. + * + * @param callable $executor + */ + 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. + * + * @param callable $onFulfilled + * @param callable $onRejected + * @return Promise + */ + function then(callable $onFulfilled = null, callable $onRejected = null) { + + // 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 teh 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. + * + * @param callable $onRejected + * @return Promise + */ + function otherwise(callable $onRejected) { + + return $this->then(null, $onRejected); + + } + + /** + * Marks this promise as fulfilled and sets its return value. + * + * @param mixed $value + * @return void + */ + function fulfill($value = null) { + if ($this->state !== self::PENDING) { + 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 it's rejection reason. + * + * While it's possible to use any PHP value as the reason, it's highly + * recommended to use an Exception for this. + * + * @param mixed $reason + * @return void + */ + function reject($reason = null) { + if ($this->state !== self::PENDING) { + 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 exection 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. + * + * @throws Exception + * @return mixed + */ + function wait() { + + $hasEvents = true; + while ($this->state === self::PENDING) { + + 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 ($this->state === self::FULFILLED) { + // 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. + $reason = $this->value; + if ($reason instanceof Exception) { + throw $reason; + } elseif (is_scalar($reason)) { + throw new Exception($reason); + } else { + $type = is_object($reason) ? get_class($reason) : gettype($reason); + throw new Exception('Promise was rejected with reason of type: ' . $type); + } + } + + + } + + + /** + * 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. + * + * @var mixed + */ + protected $value = null; + + /** + * 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. + * + * @param Promise $subPromise + * @param callable $callBack + * @return void + */ + 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 (Exception $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 ($this->state === self::FULFILLED) { + $subPromise->fulfill($this->value); + } else { + $subPromise->reject($this->value); + } + } + }); + } + + /** + * Alias for 'otherwise'. + * + * This function is now deprecated and will be removed in a future version. + * + * @param callable $onRejected + * @deprecated + * @return Promise + */ + function error(callable $onRejected) { + + return $this->otherwise($onRejected); + + } + + /** + * Deprecated. + * + * Please use Sabre\Event\Promise::all + * + * @param Promise[] $promises + * @deprecated + * @return Promise + */ + static function all(array $promises) { + + return Promise\all($promises); + + } + +} diff --git a/htdocs/includes/sabre/sabre/event/lib/Promise/functions.php b/htdocs/includes/sabre/sabre/event/lib/Promise/functions.php new file mode 100644 index 00000000000..3604b8aaa95 --- /dev/null +++ b/htdocs/includes/sabre/sabre/event/lib/Promise/functions.php @@ -0,0 +1,135 @@ +<?php + +namespace Sabre\Event\Promise; + +use Sabre\Event\Promise; + +/** + * This file contains a set of functions that are useful for dealing with the + * Promise object. + * + * @copyright Copyright (C) 2013-2015 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ + + +/** + * This function takes an array of Promises, and returns a Promise that + * resolves when all of the given arguments have resolved. + * + * The returned Promise will resolve with a value that's an array of all the + * values the given promises have been resolved with. + * + * This array will be in the exact same order as the array of input promises. + * + * If any of the given Promises fails, the returned promise will immidiately + * fail with the first Promise that fails, and its reason. + * + * @param Promise[] $promises + * @return Promise + */ +function all(array $promises) { + + return new Promise(function($success, $fail) use ($promises) { + + $successCount = 0; + $completeResult = []; + + foreach ($promises as $promiseIndex => $subPromise) { + + $subPromise->then( + function($result) use ($promiseIndex, &$completeResult, &$successCount, $success, $promises) { + $completeResult[$promiseIndex] = $result; + $successCount++; + if ($successCount === count($promises)) { + $success($completeResult); + } + return $result; + } + )->error( + 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 + * @return Promise + */ +function race(array $promises) { + + 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. + * + * @param mixed $value + * @return Promise + */ +function resolve($value) { + + 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. + * + * @param mixed $reason + * @return Promise + */ +function reject($reason) { + + $promise = new Promise(); + $promise->reject($reason); + return $promise; + +} diff --git a/htdocs/includes/sabre/sabre/event/lib/PromiseAlreadyResolvedException.php b/htdocs/includes/sabre/sabre/event/lib/PromiseAlreadyResolvedException.php new file mode 100644 index 00000000000..86a6c5b3f1a --- /dev/null +++ b/htdocs/includes/sabre/sabre/event/lib/PromiseAlreadyResolvedException.php @@ -0,0 +1,15 @@ +<?php + +namespace Sabre\Event; + +/** + * This exception is thrown when the user tried to reject or fulfill a promise, + * after either of these actions were already performed. + * + * @copyright Copyright (C) 2013-2015 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class PromiseAlreadyResolvedException extends \LogicException { + +} diff --git a/htdocs/includes/sabre/sabre/event/lib/Version.php b/htdocs/includes/sabre/sabre/event/lib/Version.php new file mode 100644 index 00000000000..5de22193ff5 --- /dev/null +++ b/htdocs/includes/sabre/sabre/event/lib/Version.php @@ -0,0 +1,19 @@ +<?php + +namespace Sabre\Event; + +/** + * This class contains the version number for this package. + * + * @copyright Copyright (C) 2013-2015 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Version { + + /** + * Full version number + */ + const VERSION = '3.0.0'; + +} diff --git a/htdocs/includes/sabre/sabre/event/lib/coroutine.php b/htdocs/includes/sabre/sabre/event/lib/coroutine.php new file mode 100644 index 00000000000..19c0ba8a714 --- /dev/null +++ b/htdocs/includes/sabre/sabre/event/lib/coroutine.php @@ -0,0 +1,120 @@ +<?php + +namespace Sabre\Event; + +use Generator; +use Exception; + +/** + * Turn asynchronous promise-based code into something that looks synchronous + * again, through the use of generators. + * + * Example without coroutines: + * + * $promise = $httpClient->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(\Exception $reason) { + * echo "Failed because: $reason\n"; + * } + * + * }); + * + * @copyright Copyright (C) 2013-2015 fruux GmbH. All rights reserved. + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +function coroutine(callable $gen) { + + $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(); + + $lastYieldResult = null; + + /** + * So tempted to use the mythical y-combinator here, but it's not needed in + * PHP. + */ + $advanceGenerator = function() use (&$advanceGenerator, $generator, $promise, &$lastYieldResult) { + + while ($generator->valid()) { + + $yieldedValue = $generator->current(); + if ($yieldedValue instanceof Promise) { + $yieldedValue->then( + function($value) use ($generator, &$advanceGenerator, &$lastYieldResult) { + $lastYieldResult = $value; + $generator->send($value); + $advanceGenerator(); + }, + function($reason) use ($generator, $advanceGenerator) { + if ($reason instanceof Exception) { + $generator->throw($reason); + } elseif (is_scalar($reason)) { + $generator->throw(new Exception($reason)); + } else { + $type = is_object($reason) ? get_class($reason) : gettype($reason); + $generator->throw(new Exception('Promise was rejected with reason of type: ' . $type)); + } + $advanceGenerator(); + } + )->error(function($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. + $lastYieldResult = $yieldedValue; + $generator->send($yieldedValue); + } + + } + + // If the generator is at the end, and we didn't run into an exception, + // we can fullfill the promise with the last thing that was yielded to + // us. + if (!$generator->valid() && $promise->state === Promise::PENDING) { + $promise->fulfill($lastYieldResult); + } + + }; + + try { + $advanceGenerator(); + } catch (Exception $e) { + $promise->reject($e); + } + + return $promise; + +} diff --git a/htdocs/includes/sabre/sabre/event/phpunit.xml.dist b/htdocs/includes/sabre/sabre/event/phpunit.xml.dist new file mode 100644 index 00000000000..ccd59be9c2a --- /dev/null +++ b/htdocs/includes/sabre/sabre/event/phpunit.xml.dist @@ -0,0 +1,18 @@ +<phpunit + colors="true" + bootstrap="vendor/autoload.php" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" + strict="true" + > + <testsuite name="sabre-event"> + <directory>tests/</directory> + </testsuite> + + <filter> + <whitelist addUncoveredFilesFromWhitelist="true"> + <directory suffix=".php">./lib/</directory> + </whitelist> + </filter> +</phpunit> diff --git a/htdocs/includes/sabre/sabre/event/tests/ContinueCallbackTest.php b/htdocs/includes/sabre/sabre/event/tests/ContinueCallbackTest.php new file mode 100644 index 00000000000..c469913795f --- /dev/null +++ b/htdocs/includes/sabre/sabre/event/tests/ContinueCallbackTest.php @@ -0,0 +1,76 @@ +<?php + +namespace Sabre\Event; + +class ContinueCallbackTest extends \PHPUnit_Framework_TestCase { + + function testContinueCallBack() { + + $ee = new EventEmitter(); + + $handlerCounter = 0; + $bla = function() use (&$handlerCounter) { + $handlerCounter++; + }; + $ee->on('foo', $bla); + $ee->on('foo', $bla); + $ee->on('foo', $bla); + + $continueCounter = 0; + $r = $ee->emit('foo', [], function() use (&$continueCounter) { + $continueCounter++; + return true; + }); + $this->assertTrue($r); + $this->assertEquals(3, $handlerCounter); + $this->assertEquals(2, $continueCounter); + + } + + function testContinueCallBackBreak() { + + $ee = new EventEmitter(); + + $handlerCounter = 0; + $bla = function() use (&$handlerCounter) { + $handlerCounter++; + }; + $ee->on('foo', $bla); + $ee->on('foo', $bla); + $ee->on('foo', $bla); + + $continueCounter = 0; + $r = $ee->emit('foo', [], function() use (&$continueCounter) { + $continueCounter++; + return false; + }); + $this->assertTrue($r); + $this->assertEquals(1, $handlerCounter); + $this->assertEquals(1, $continueCounter); + + } + + function testContinueCallBackBreakByHandler() { + + $ee = new EventEmitter(); + + $handlerCounter = 0; + $bla = function() use (&$handlerCounter) { + $handlerCounter++; + return false; + }; + $ee->on('foo', $bla); + $ee->on('foo', $bla); + $ee->on('foo', $bla); + + $continueCounter = 0; + $r = $ee->emit('foo', [], function() use (&$continueCounter) { + $continueCounter++; + return false; + }); + $this->assertFalse($r); + $this->assertEquals(1, $handlerCounter); + $this->assertEquals(0, $continueCounter); + + } +} diff --git a/htdocs/includes/sabre/sabre/event/tests/CoroutineTest.php b/htdocs/includes/sabre/sabre/event/tests/CoroutineTest.php new file mode 100644 index 00000000000..6e4b666b042 --- /dev/null +++ b/htdocs/includes/sabre/sabre/event/tests/CoroutineTest.php @@ -0,0 +1,262 @@ +<?php + +namespace Sabre\Event; + +class CoroutineTest extends \PHPUnit_Framework_TestCase { + + /** + * @expectedException \InvalidArgumentException + */ + function testNonGenerator() { + + coroutine(function() {}); + + } + + function testBasicCoroutine() { + + $start = 0; + + coroutine(function() use (&$start) { + + $start += 1; + yield; + + }); + + $this->assertEquals(1, $start); + + } + + function testFulfilledPromise() { + + $start = 0; + $promise = new Promise(function($fulfill, $reject) { + $fulfill(2); + }); + + coroutine(function() use (&$start, $promise) { + + $start += 1; + $start += (yield $promise); + + }); + + Loop\run(); + $this->assertEquals(3, $start); + + } + + function testRejectedPromise() { + + $start = 0; + $promise = new Promise(function($fulfill, $reject) { + $reject(2); + }); + + coroutine(function() use (&$start, $promise) { + + $start += 1; + try { + $start += (yield $promise); + // This line is unreachable, but it's our control + $start += 4; + } catch (\Exception $e) { + $start += $e->getMessage(); + } + + }); + + Loop\run(); + $this->assertEquals(3, $start); + + } + + function testRejectedPromiseException() { + + $start = 0; + $promise = new Promise(function($fulfill, $reject) { + $reject(new \LogicException('2')); + }); + + coroutine(function() use (&$start, $promise) { + + $start += 1; + try { + $start += (yield $promise); + // This line is unreachable, but it's our control + $start += 4; + } catch (\LogicException $e) { + $start += $e->getMessage(); + } + + }); + + Loop\run(); + $this->assertEquals(3, $start); + + } + + function testRejectedPromiseArray() { + + $start = 0; + $promise = new Promise(function($fulfill, $reject) { + $reject([]); + }); + + coroutine(function() use (&$start, $promise) { + + $start += 1; + try { + $start += (yield $promise); + // This line is unreachable, but it's our control + $start += 4; + } catch (\Exception $e) { + $this->assertTrue(strpos($e->getMessage(), 'Promise was rejected with') === 0); + $start += 2; + } + + })->wait(); + + $this->assertEquals(3, $start); + + } + + function testFulfilledPromiseAsync() { + + $start = 0; + $promise = new Promise(); + coroutine(function() use (&$start, $promise) { + + $start += 1; + $start += (yield $promise); + + }); + Loop\run(); + + $this->assertEquals(1, $start); + + $promise->fulfill(2); + Loop\run(); + + $this->assertEquals(3, $start); + + } + + function testRejectedPromiseAsync() { + + $start = 0; + $promise = new Promise(); + coroutine(function() use (&$start, $promise) { + + $start += 1; + try { + $start += (yield $promise); + // This line is unreachable, but it's our control + $start += 4; + } catch (\Exception $e) { + $start += $e->getMessage(); + } + + }); + + $this->assertEquals(1, $start); + + $promise->reject(new \Exception(2)); + Loop\run(); + + $this->assertEquals(3, $start); + + } + + function testCoroutineException() { + + $start = 0; + coroutine(function() use (&$start) { + + $start += 1; + $start += (yield 2); + + throw new \Exception('4'); + + })->error(function($e) use (&$start) { + + $start += $e->getMessage(); + + }); + Loop\run(); + + $this->assertEquals(7, $start); + + } + + function testDeepException() { + + $start = 0; + $promise = new Promise(); + coroutine(function() use (&$start, $promise) { + + $start += 1; + $start += (yield $promise); + + })->error(function($e) use (&$start) { + + $start += $e->getMessage(); + + }); + + $this->assertEquals(1, $start); + + $promise->reject(new \Exception(2)); + Loop\run(); + + $this->assertEquals(3, $start); + + } + + function testResolveToLastYield() { + + $ok = false; + coroutine(function() { + + yield 1; + yield 2; + $hello = 'hi'; + + })->then(function($value) use (&$ok) { + $this->assertEquals(2, $value); + $ok = true; + })->error(function($reason) { + $this->fail($reason); + }); + Loop\run(); + + $this->assertTrue($ok); + + } + + function testResolveToLastYieldPromise() { + + $ok = false; + + $promise = new Promise(); + + coroutine(function() use ($promise) { + + yield 'fail'; + yield $promise; + $hello = 'hi'; + + })->then(function($value) use (&$ok) { + $ok = $value; + $this->fail($reason); + }); + + $promise->fulfill('omg it worked'); + Loop\run(); + + $this->assertEquals('omg it worked', $ok); + + } + +} diff --git a/htdocs/includes/sabre/sabre/event/tests/EventEmitterTest.php b/htdocs/includes/sabre/sabre/event/tests/EventEmitterTest.php new file mode 100644 index 00000000000..df08e9cd84a --- /dev/null +++ b/htdocs/includes/sabre/sabre/event/tests/EventEmitterTest.php @@ -0,0 +1,318 @@ +<?php + +namespace Sabre\Event; + +class EventEmitterTest extends \PHPUnit_Framework_TestCase { + + function testInit() { + + $ee = new EventEmitter(); + $this->assertInstanceOf('Sabre\\Event\\EventEmitter', $ee); + + } + + function testListeners() { + + $ee = new EventEmitter(); + + $callback1 = function() { }; + $callback2 = function() { }; + $ee->on('foo', $callback1, 200); + $ee->on('foo', $callback2, 100); + + $this->assertEquals([$callback2, $callback1], $ee->listeners('foo')); + + } + + /** + * @depends testInit + */ + function testHandleEvent() { + + $argResult = null; + + $ee = new EventEmitter(); + $ee->on('foo', function($arg) use (&$argResult) { + + $argResult = $arg; + + }); + + $this->assertTrue( + $ee->emit('foo', ['bar']) + ); + + $this->assertEquals('bar', $argResult); + + } + + /** + * @depends testHandleEvent + */ + function testCancelEvent() { + + $argResult = 0; + + $ee = new EventEmitter(); + $ee->on('foo', function($arg) use (&$argResult) { + + $argResult = 1; + return false; + + }); + $ee->on('foo', function($arg) use (&$argResult) { + + $argResult = 2; + + }); + + $this->assertFalse( + $ee->emit('foo', ['bar']) + ); + + $this->assertEquals(1, $argResult); + + } + + /** + * @depends testCancelEvent + */ + function testPriority() { + + $argResult = 0; + + $ee = new EventEmitter(); + $ee->on('foo', function($arg) use (&$argResult) { + + $argResult = 1; + return false; + + }); + $ee->on('foo', function($arg) use (&$argResult) { + + $argResult = 2; + return false; + + }, 1); + + $this->assertFalse( + $ee->emit('foo', ['bar']) + ); + + $this->assertEquals(2, $argResult); + + } + + /** + * @depends testPriority + */ + function testPriority2() { + + $result = []; + $ee = new EventEmitter(); + + $ee->on('foo', function() use (&$result) { + + $result[] = 'a'; + + }, 200); + $ee->on('foo', function() use (&$result) { + + $result[] = 'b'; + + }, 50); + $ee->on('foo', function() use (&$result) { + + $result[] = 'c'; + + }, 300); + $ee->on('foo', function() use (&$result) { + + $result[] = 'd'; + + }); + + $ee->emit('foo'); + $this->assertEquals(['b', 'd', 'a', 'c'], $result); + + } + + function testRemoveListener() { + + $result = false; + + $callBack = function() use (&$result) { + + $result = true; + + }; + + $ee = new EventEmitter(); + + $ee->on('foo', $callBack); + + $ee->emit('foo'); + $this->assertTrue($result); + $result = false; + + $this->assertTrue( + $ee->removeListener('foo', $callBack) + ); + + $ee->emit('foo'); + $this->assertFalse($result); + + } + + function testRemoveUnknownListener() { + + $result = false; + + $callBack = function() use (&$result) { + + $result = true; + + }; + + $ee = new EventEmitter(); + + $ee->on('foo', $callBack); + + $ee->emit('foo'); + $this->assertTrue($result); + $result = false; + + $this->assertFalse($ee->removeListener('bar', $callBack)); + + $ee->emit('foo'); + $this->assertTrue($result); + + } + + function testRemoveListenerTwice() { + + $result = false; + + $callBack = function() use (&$result) { + + $result = true; + + }; + + $ee = new EventEmitter(); + + $ee->on('foo', $callBack); + + $ee->emit('foo'); + $this->assertTrue($result); + $result = false; + + $this->assertTrue( + $ee->removeListener('foo', $callBack) + ); + $this->assertFalse( + $ee->removeListener('foo', $callBack) + ); + + $ee->emit('foo'); + $this->assertFalse($result); + + } + + function testRemoveAllListeners() { + + $result = false; + $callBack = function() use (&$result) { + + $result = true; + + }; + + $ee = new EventEmitter(); + $ee->on('foo', $callBack); + + $ee->emit('foo'); + $this->assertTrue($result); + $result = false; + + $ee->removeAllListeners('foo'); + + $ee->emit('foo'); + $this->assertFalse($result); + + } + + function testRemoveAllListenersNoArg() { + + $result = false; + + $callBack = function() use (&$result) { + + $result = true; + + }; + + + $ee = new EventEmitter(); + $ee->on('foo', $callBack); + + $ee->emit('foo'); + $this->assertTrue($result); + $result = false; + + $ee->removeAllListeners(); + + $ee->emit('foo'); + $this->assertFalse($result); + + } + + function testOnce() { + + $result = 0; + + $callBack = function() use (&$result) { + + $result++; + + }; + + $ee = new EventEmitter(); + $ee->once('foo', $callBack); + + $ee->emit('foo'); + $ee->emit('foo'); + + $this->assertEquals(1, $result); + + } + + /** + * @depends testCancelEvent + */ + function testPriorityOnce() { + + $argResult = 0; + + $ee = new EventEmitter(); + $ee->once('foo', function($arg) use (&$argResult) { + + $argResult = 1; + return false; + + }); + $ee->once('foo', function($arg) use (&$argResult) { + + $argResult = 2; + return false; + + }, 1); + + $this->assertFalse( + $ee->emit('foo', ['bar']) + ); + + $this->assertEquals(2, $argResult); + + } +} diff --git a/htdocs/includes/sabre/sabre/event/tests/Loop/FunctionsTest.php b/htdocs/includes/sabre/sabre/event/tests/Loop/FunctionsTest.php new file mode 100644 index 00000000000..08bf306c37f --- /dev/null +++ b/htdocs/includes/sabre/sabre/event/tests/Loop/FunctionsTest.php @@ -0,0 +1,160 @@ +<?php + +namespace Sabre\Event\Loop; + +class FunctionsTest extends \PHPUnit_Framework_TestCase { + + function setUp() { + + // Always creating a fresh loop object. + instance(new Loop()); + + } + + function tearDown() { + + // Removing the global loop object. + instance(null); + + } + + function testNextTick() { + + $check = 0; + nextTick(function() use (&$check) { + + $check++; + + }); + + run(); + + $this->assertEquals(1, $check); + + } + + function testTimeout() { + + $check = 0; + setTimeout(function() use (&$check) { + + $check++; + + }, 0.02); + + run(); + + $this->assertEquals(1, $check); + + } + + function testTimeoutOrder() { + + $check = []; + setTimeout(function() use (&$check) { + + $check[] = 'a'; + + }, 0.2); + setTimeout(function() use (&$check) { + + $check[] = 'b'; + + }, 0.1); + setTimeout(function() use (&$check) { + + $check[] = 'c'; + + }, 0.3); + + run(); + + $this->assertEquals(['b', 'a', 'c'], $check); + + } + + function testSetInterval() { + + $check = 0; + $intervalId = null; + $intervalId = setInterval(function() use (&$check, &$intervalId) { + + $check++; + if ($check > 5) { + clearInterval($intervalId); + } + + }, 0.02); + + run(); + $this->assertEquals(6, $check); + + } + + function testAddWriteStream() { + + $h = fopen('php://temp', 'r+'); + addWriteStream($h, function() use ($h) { + + fwrite($h, 'hello world'); + removeWriteStream($h); + + }); + run(); + rewind($h); + $this->assertEquals('hello world', stream_get_contents($h)); + + } + + function testAddReadStream() { + + $h = fopen('php://temp', 'r+'); + fwrite($h, 'hello world'); + rewind($h); + + $result = null; + + addReadStream($h, function() use ($h, &$result) { + + $result = fgets($h); + removeReadStream($h); + + }); + run(); + $this->assertEquals('hello world', $result); + + } + + function testStop() { + + $check = 0; + setTimeout(function() use (&$check) { + $check++; + }, 200); + + nextTick(function() { + stop(); + }); + run(); + + $this->assertEquals(0, $check); + + } + + function testTick() { + + $check = 0; + setTimeout(function() use (&$check) { + $check++; + }, 1); + + nextTick(function() use (&$check) { + $check++; + }); + tick(); + + $this->assertEquals(1, $check); + + } + +} diff --git a/htdocs/includes/sabre/sabre/event/tests/Loop/LoopTest.php b/htdocs/includes/sabre/sabre/event/tests/Loop/LoopTest.php new file mode 100644 index 00000000000..a9cf551bddb --- /dev/null +++ b/htdocs/includes/sabre/sabre/event/tests/Loop/LoopTest.php @@ -0,0 +1,180 @@ +<?php + +namespace Sabre\Event\Loop; + +class LoopTest extends \PHPUnit_Framework_TestCase { + + function testNextTick() { + + $loop = new Loop(); + $check = 0; + $loop->nextTick(function() use (&$check) { + + $check++; + + }); + + $loop->run(); + + $this->assertEquals(1, $check); + + } + + function testTimeout() { + + $loop = new Loop(); + $check = 0; + $loop->setTimeout(function() use (&$check) { + + $check++; + + }, 0.02); + + $loop->run(); + + $this->assertEquals(1, $check); + + } + + function testTimeoutOrder() { + + $loop = new Loop(); + $check = []; + $loop->setTimeout(function() use (&$check) { + + $check[] = 'a'; + + }, 0.2); + $loop->setTimeout(function() use (&$check) { + + $check[] = 'b'; + + }, 0.1); + $loop->setTimeout(function() use (&$check) { + + $check[] = 'c'; + + }, 0.3); + + $loop->run(); + + $this->assertEquals(['b', 'a', 'c'], $check); + + } + + function testSetInterval() { + + $loop = new Loop(); + $check = 0; + $intervalId = null; + $intervalId = $loop->setInterval(function() use (&$check, &$intervalId, $loop) { + + $check++; + if ($check > 5) { + $loop->clearInterval($intervalId); + } + + }, 0.02); + + $loop->run(); + $this->assertEquals(6, $check); + + } + + function testAddWriteStream() { + + $h = fopen('php://temp', 'r+'); + $loop = new Loop(); + $loop->addWriteStream($h, function() use ($h, $loop) { + + fwrite($h, 'hello world'); + $loop->removeWriteStream($h); + + }); + $loop->run(); + rewind($h); + $this->assertEquals('hello world', stream_get_contents($h)); + + } + + function testAddReadStream() { + + $h = fopen('php://temp', 'r+'); + fwrite($h, 'hello world'); + rewind($h); + + $loop = new Loop(); + + $result = null; + + $loop->addReadStream($h, function() use ($h, $loop, &$result) { + + $result = fgets($h); + $loop->removeReadStream($h); + + }); + $loop->run(); + $this->assertEquals('hello world', $result); + + } + + function testStop() { + + $check = 0; + $loop = new Loop(); + $loop->setTimeout(function() use (&$check) { + $check++; + }, 200); + + $loop->nextTick(function() use ($loop) { + $loop->stop(); + }); + $loop->run(); + + $this->assertEquals(0, $check); + + } + + function testTick() { + + $check = 0; + $loop = new Loop(); + $loop->setTimeout(function() use (&$check) { + $check++; + }, 1); + + $loop->nextTick(function() use ($loop, &$check) { + $check++; + }); + $loop->tick(); + + $this->assertEquals(1, $check); + + } + + /** + * Here we add a new nextTick function as we're in the middle of a current + * nextTick. + */ + function testNextTickStacking() { + + $loop = new Loop(); + $check = 0; + $loop->nextTick(function() use (&$check, $loop) { + + $loop->nextTick(function() use (&$check) { + + $check++; + + }); + $check++; + + }); + + $loop->run(); + + $this->assertEquals(2, $check); + + } + +} diff --git a/htdocs/includes/sabre/sabre/event/tests/Promise/FunctionsTest.php b/htdocs/includes/sabre/sabre/event/tests/Promise/FunctionsTest.php new file mode 100644 index 00000000000..51e47ae297b --- /dev/null +++ b/htdocs/includes/sabre/sabre/event/tests/Promise/FunctionsTest.php @@ -0,0 +1,184 @@ +<?php + +namespace Sabre\Event\Promise; + +use Sabre\Event\Loop; +use Sabre\Event\Promise; + +class FunctionsTest extends \PHPUnit_Framework_TestCase { + + function testAll() { + + $promise1 = new Promise(); + $promise2 = new Promise(); + + $finalValue = 0; + Promise\all([$promise1, $promise2])->then(function($value) use (&$finalValue) { + + $finalValue = $value; + + }); + + $promise1->fulfill(1); + Loop\run(); + $this->assertEquals(0, $finalValue); + + $promise2->fulfill(2); + Loop\run(); + $this->assertEquals([1, 2], $finalValue); + + } + + function testAllReject() { + + $promise1 = new Promise(); + $promise2 = new Promise(); + + $finalValue = 0; + Promise\all([$promise1, $promise2])->then( + function($value) use (&$finalValue) { + $finalValue = 'foo'; + return 'test'; + }, + function($value) use (&$finalValue) { + $finalValue = $value; + } + ); + + $promise1->reject(1); + Loop\run(); + $this->assertEquals(1, $finalValue); + $promise2->reject(2); + Loop\run(); + $this->assertEquals(1, $finalValue); + + } + + function testAllRejectThenResolve() { + + $promise1 = new Promise(); + $promise2 = new Promise(); + + $finalValue = 0; + Promise\all([$promise1, $promise2])->then( + function($value) use (&$finalValue) { + $finalValue = 'foo'; + return 'test'; + }, + function($value) use (&$finalValue) { + $finalValue = $value; + } + ); + + $promise1->reject(1); + Loop\run(); + $this->assertEquals(1, $finalValue); + $promise2->fulfill(2); + Loop\run(); + $this->assertEquals(1, $finalValue); + + } + + function testRace() { + + $promise1 = new Promise(); + $promise2 = new Promise(); + + $finalValue = 0; + Promise\race([$promise1, $promise2])->then( + function($value) use (&$finalValue) { + $finalValue = $value; + }, + function($value) use (&$finalValue) { + $finalValue = $value; + } + ); + + $promise1->fulfill(1); + Loop\run(); + $this->assertEquals(1, $finalValue); + $promise2->fulfill(2); + Loop\run(); + $this->assertEquals(1, $finalValue); + + } + + function testRaceReject() { + + $promise1 = new Promise(); + $promise2 = new Promise(); + + $finalValue = 0; + Promise\race([$promise1, $promise2])->then( + function($value) use (&$finalValue) { + $finalValue = $value; + }, + function($value) use (&$finalValue) { + $finalValue = $value; + } + ); + + $promise1->reject(1); + Loop\run(); + $this->assertEquals(1, $finalValue); + $promise2->reject(2); + Loop\run(); + $this->assertEquals(1, $finalValue); + + } + + function testResolve() { + + $finalValue = 0; + + $promise = resolve(1); + $promise->then(function($value) use (&$finalValue) { + + $finalValue = $value; + + }); + + $this->assertEquals(0, $finalValue); + Loop\run(); + $this->assertEquals(1, $finalValue); + + } + + /** + * @expectedException \Exception + */ + function testResolvePromise() { + + $finalValue = 0; + + $promise = new Promise(); + $promise->reject(new \Exception('uh oh')); + + $newPromise = resolve($promise); + $newPromise->wait(); + + } + + function testReject() { + + $finalValue = 0; + + $promise = reject(1); + $promise->then(function($value) use (&$finalValue) { + + $finalValue = 'im broken'; + + }, function($reason) use (&$finalValue) { + + $finalValue = $reason; + + }); + + $this->assertEquals(0, $finalValue); + Loop\run(); + $this->assertEquals(1, $finalValue); + + } + + +} diff --git a/htdocs/includes/sabre/sabre/event/tests/Promise/PromiseTest.php b/htdocs/includes/sabre/sabre/event/tests/Promise/PromiseTest.php new file mode 100644 index 00000000000..69838397881 --- /dev/null +++ b/htdocs/includes/sabre/sabre/event/tests/Promise/PromiseTest.php @@ -0,0 +1,341 @@ +<?php + +namespace Sabre\Event\Promise; + +use Sabre\Event\Loop; +use Sabre\Event\Promise; + +class PromiseTest extends \PHPUnit_Framework_TestCase { + + function testSuccess() { + + $finalValue = 0; + $promise = new Promise(); + $promise->fulfill(1); + + $promise->then(function($value) use (&$finalValue) { + $finalValue = $value + 2; + }); + Loop\run(); + + $this->assertEquals(3, $finalValue); + + } + + function testFail() { + + $finalValue = 0; + $promise = new Promise(); + $promise->reject(1); + + $promise->then(null, function($value) use (&$finalValue) { + $finalValue = $value + 2; + }); + Loop\run(); + + $this->assertEquals(3, $finalValue); + + } + + function testChain() { + + $finalValue = 0; + $promise = new Promise(); + $promise->fulfill(1); + + $promise->then(function($value) use (&$finalValue) { + $finalValue = $value + 2; + return $finalValue; + })->then(function($value) use (&$finalValue) { + $finalValue = $value + 4; + return $finalValue; + }); + Loop\run(); + + $this->assertEquals(7, $finalValue); + + } + function testChainPromise() { + + $finalValue = 0; + $promise = new Promise(); + $promise->fulfill(1); + + $subPromise = new Promise(); + + $promise->then(function($value) use ($subPromise) { + return $subPromise; + })->then(function($value) use (&$finalValue) { + $finalValue = $value + 4; + return $finalValue; + }); + + $subPromise->fulfill(2); + Loop\run(); + + $this->assertEquals(6, $finalValue); + + } + + function testPendingResult() { + + $finalValue = 0; + $promise = new Promise(); + + $promise->then(function($value) use (&$finalValue) { + $finalValue = $value + 2; + }); + + $promise->fulfill(4); + Loop\run(); + + $this->assertEquals(6, $finalValue); + + } + + function testPendingFail() { + + $finalValue = 0; + $promise = new Promise(); + + $promise->then(null, function($value) use (&$finalValue) { + $finalValue = $value + 2; + }); + + $promise->reject(4); + Loop\run(); + + $this->assertEquals(6, $finalValue); + + } + + function testExecutorSuccess() { + + $promise = (new Promise(function($success, $fail) { + + $success('hi'); + + }))->then(function($result) use (&$realResult) { + + $realResult = $result; + + }); + Loop\run(); + + $this->assertEquals('hi', $realResult); + + } + + function testExecutorFail() { + + $promise = (new Promise(function($success, $fail) { + + $fail('hi'); + + }))->then(function($result) use (&$realResult) { + + $realResult = 'incorrect'; + + }, function($reason) use (&$realResult) { + + $realResult = $reason; + + }); + Loop\run(); + + $this->assertEquals('hi', $realResult); + + } + + /** + * @expectedException \Sabre\Event\PromiseAlreadyResolvedException + */ + function testFulfillTwice() { + + $promise = new Promise(); + $promise->fulfill(1); + $promise->fulfill(1); + + } + + /** + * @expectedException \Sabre\Event\PromiseAlreadyResolvedException + */ + function testRejectTwice() { + + $promise = new Promise(); + $promise->reject(1); + $promise->reject(1); + + } + + function testFromFailureHandler() { + + $ok = 0; + $promise = new Promise(); + $promise->otherwise(function($reason) { + + $this->assertEquals('foo', $reason); + throw new \Exception('hi'); + + })->then(function() use (&$ok) { + + $ok = -1; + + }, function() use (&$ok) { + + $ok = 1; + + }); + + $this->assertEquals(0, $ok); + $promise->reject('foo'); + Loop\run(); + + $this->assertEquals(1, $ok); + + } + + function testAll() { + + $promise1 = new Promise(); + $promise2 = new Promise(); + + $finalValue = 0; + Promise::all([$promise1, $promise2])->then(function($value) use (&$finalValue) { + + $finalValue = $value; + + }); + + $promise1->fulfill(1); + Loop\run(); + $this->assertEquals(0, $finalValue); + + $promise2->fulfill(2); + Loop\run(); + $this->assertEquals([1, 2], $finalValue); + + } + + function testAllReject() { + + $promise1 = new Promise(); + $promise2 = new Promise(); + + $finalValue = 0; + Promise::all([$promise1, $promise2])->then( + function($value) use (&$finalValue) { + $finalValue = 'foo'; + return 'test'; + }, + function($value) use (&$finalValue) { + $finalValue = $value; + } + ); + + $promise1->reject(1); + Loop\run(); + $this->assertEquals(1, $finalValue); + $promise2->reject(2); + Loop\run(); + $this->assertEquals(1, $finalValue); + + } + + function testAllRejectThenResolve() { + + $promise1 = new Promise(); + $promise2 = new Promise(); + + $finalValue = 0; + Promise::all([$promise1, $promise2])->then( + function($value) use (&$finalValue) { + $finalValue = 'foo'; + return 'test'; + }, + function($value) use (&$finalValue) { + $finalValue = $value; + } + ); + + $promise1->reject(1); + Loop\run(); + $this->assertEquals(1, $finalValue); + $promise2->fulfill(2); + Loop\run(); + $this->assertEquals(1, $finalValue); + + } + + function testWaitResolve() { + + $promise = new Promise(); + Loop\nextTick(function() use ($promise) { + $promise->fulfill(1); + }); + $this->assertEquals( + 1, + $promise->wait() + ); + + } + + /** + * @expectedException \LogicException + */ + function testWaitWillNeverResolve() { + + $promise = new Promise(); + $promise->wait(); + + } + + function testWaitRejectedException() { + + $promise = new Promise(); + Loop\nextTick(function() use ($promise) { + $promise->reject(new \OutOfBoundsException('foo')); + }); + try { + $promise->wait(); + $this->fail('We did not get the expected exception'); + } catch (\Exception $e) { + $this->assertInstanceOf('OutOfBoundsException', $e); + $this->assertEquals('foo', $e->getMessage()); + } + + } + + function testWaitRejectedScalar() { + + $promise = new Promise(); + Loop\nextTick(function() use ($promise) { + $promise->reject('foo'); + }); + try { + $promise->wait(); + $this->fail('We did not get the expected exception'); + } catch (\Exception $e) { + $this->assertInstanceOf('Exception', $e); + $this->assertEquals('foo', $e->getMessage()); + } + + } + + function testWaitRejectedNonScalar() { + + $promise = new Promise(); + Loop\nextTick(function() use ($promise) { + $promise->reject([]); + }); + try { + $promise->wait(); + $this->fail('We did not get the expected exception'); + } catch (\Exception $e) { + $this->assertInstanceOf('Exception', $e); + $this->assertEquals('Promise was rejected with reason of type: array', $e->getMessage()); + } + + } +} diff --git a/htdocs/includes/sabre/sabre/event/tests/PromiseTest.php b/htdocs/includes/sabre/sabre/event/tests/PromiseTest.php new file mode 100644 index 00000000000..0029d898e53 --- /dev/null +++ b/htdocs/includes/sabre/sabre/event/tests/PromiseTest.php @@ -0,0 +1,386 @@ +<?php + +namespace Sabre\Event; + +class PromiseTest extends \PHPUnit_Framework_TestCase { + + function testSuccess() { + + $finalValue = 0; + $promise = new Promise(); + $promise->fulfill(1); + + $promise->then(function($value) use (&$finalValue) { + $finalValue = $value + 2; + }); + Loop\run(); + + $this->assertEquals(3, $finalValue); + + } + + function testFail() { + + $finalValue = 0; + $promise = new Promise(); + $promise->reject(1); + + $promise->then(null, function($value) use (&$finalValue) { + $finalValue = $value + 2; + }); + Loop\run(); + + $this->assertEquals(3, $finalValue); + + } + + function testChain() { + + $finalValue = 0; + $promise = new Promise(); + $promise->fulfill(1); + + $promise->then(function($value) use (&$finalValue) { + $finalValue = $value + 2; + return $finalValue; + })->then(function($value) use (&$finalValue) { + $finalValue = $value + 4; + return $finalValue; + }); + Loop\run(); + + $this->assertEquals(7, $finalValue); + + } + function testChainPromise() { + + $finalValue = 0; + $promise = new Promise(); + $promise->fulfill(1); + + $subPromise = new Promise(); + + $promise->then(function($value) use ($subPromise) { + return $subPromise; + })->then(function($value) use (&$finalValue) { + $finalValue = $value + 4; + return $finalValue; + }); + + $subPromise->fulfill(2); + Loop\run(); + + $this->assertEquals(6, $finalValue); + + } + + function testPendingResult() { + + $finalValue = 0; + $promise = new Promise(); + + $promise->then(function($value) use (&$finalValue) { + $finalValue = $value + 2; + }); + + $promise->fulfill(4); + Loop\run(); + + $this->assertEquals(6, $finalValue); + + } + + function testPendingFail() { + + $finalValue = 0; + $promise = new Promise(); + + $promise->then(null, function($value) use (&$finalValue) { + $finalValue = $value + 2; + }); + + $promise->reject(4); + Loop\run(); + + $this->assertEquals(6, $finalValue); + + } + + function testExecutorSuccess() { + + $promise = (new Promise(function($success, $fail) { + + $success('hi'); + + }))->then(function($result) use (&$realResult) { + + $realResult = $result; + + }); + Loop\run(); + + $this->assertEquals('hi', $realResult); + + } + + function testExecutorFail() { + + $promise = (new Promise(function($success, $fail) { + + $fail('hi'); + + }))->then(function($result) use (&$realResult) { + + $realResult = 'incorrect'; + + }, function($reason) use (&$realResult) { + + $realResult = $reason; + + }); + Loop\run(); + + $this->assertEquals('hi', $realResult); + + } + + /** + * @expectedException \Sabre\Event\PromiseAlreadyResolvedException + */ + function testFulfillTwice() { + + $promise = new Promise(); + $promise->fulfill(1); + $promise->fulfill(1); + + } + + /** + * @expectedException \Sabre\Event\PromiseAlreadyResolvedException + */ + function testRejectTwice() { + + $promise = new Promise(); + $promise->reject(1); + $promise->reject(1); + + } + + function testFromFailureHandler() { + + $ok = 0; + $promise = new Promise(); + $promise->otherwise(function($reason) { + + $this->assertEquals('foo', $reason); + throw new \Exception('hi'); + + })->then(function() use (&$ok) { + + $ok = -1; + + }, function() use (&$ok) { + + $ok = 1; + + }); + + $this->assertEquals(0, $ok); + $promise->reject('foo'); + Loop\run(); + + $this->assertEquals(1, $ok); + + } + + function testAll() { + + $promise1 = new Promise(); + $promise2 = new Promise(); + + $finalValue = 0; + Promise::all([$promise1, $promise2])->then(function($value) use (&$finalValue) { + + $finalValue = $value; + + }); + + $promise1->fulfill(1); + Loop\run(); + $this->assertEquals(0, $finalValue); + + $promise2->fulfill(2); + Loop\run(); + $this->assertEquals([1, 2], $finalValue); + + } + + function testAllReject() { + + $promise1 = new Promise(); + $promise2 = new Promise(); + + $finalValue = 0; + Promise::all([$promise1, $promise2])->then( + function($value) use (&$finalValue) { + $finalValue = 'foo'; + return 'test'; + }, + function($value) use (&$finalValue) { + $finalValue = $value; + } + ); + + $promise1->reject(1); + Loop\run(); + $this->assertEquals(1, $finalValue); + $promise2->reject(2); + Loop\run(); + $this->assertEquals(1, $finalValue); + + } + + function testAllRejectThenResolve() { + + $promise1 = new Promise(); + $promise2 = new Promise(); + + $finalValue = 0; + Promise::all([$promise1, $promise2])->then( + function($value) use (&$finalValue) { + $finalValue = 'foo'; + return 'test'; + }, + function($value) use (&$finalValue) { + $finalValue = $value; + } + ); + + $promise1->reject(1); + Loop\run(); + $this->assertEquals(1, $finalValue); + $promise2->fulfill(2); + Loop\run(); + $this->assertEquals(1, $finalValue); + + } + + function testRace() { + + $promise1 = new Promise(); + $promise2 = new Promise(); + + $finalValue = 0; + Promise\race([$promise1, $promise2])->then( + function($value) use (&$finalValue) { + $finalValue = $value; + }, + function($value) use (&$finalValue) { + $finalValue = $value; + } + ); + + $promise1->fulfill(1); + Loop\run(); + $this->assertEquals(1, $finalValue); + $promise2->fulfill(2); + Loop\run(); + $this->assertEquals(1, $finalValue); + + } + + function testRaceReject() { + + $promise1 = new Promise(); + $promise2 = new Promise(); + + $finalValue = 0; + Promise\race([$promise1, $promise2])->then( + function($value) use (&$finalValue) { + $finalValue = $value; + }, + function($value) use (&$finalValue) { + $finalValue = $value; + } + ); + + $promise1->reject(1); + Loop\run(); + $this->assertEquals(1, $finalValue); + $promise2->reject(2); + Loop\run(); + $this->assertEquals(1, $finalValue); + + } + + function testWaitResolve() { + + $promise = new Promise(); + Loop\nextTick(function() use ($promise) { + $promise->fulfill(1); + }); + $this->assertEquals( + 1, + $promise->wait() + ); + + } + + /** + * @expectedException \LogicException + */ + function testWaitWillNeverResolve() { + + $promise = new Promise(); + $promise->wait(); + + } + + function testWaitRejectedException() { + + $promise = new Promise(); + Loop\nextTick(function() use ($promise) { + $promise->reject(new \OutOfBoundsException('foo')); + }); + try { + $promise->wait(); + $this->fail('We did not get the expected exception'); + } catch (\Exception $e) { + $this->assertInstanceOf('OutOfBoundsException', $e); + $this->assertEquals('foo', $e->getMessage()); + } + + } + + function testWaitRejectedScalar() { + + $promise = new Promise(); + Loop\nextTick(function() use ($promise) { + $promise->reject('foo'); + }); + try { + $promise->wait(); + $this->fail('We did not get the expected exception'); + } catch (\Exception $e) { + $this->assertInstanceOf('Exception', $e); + $this->assertEquals('foo', $e->getMessage()); + } + + } + + function testWaitRejectedNonScalar() { + + $promise = new Promise(); + Loop\nextTick(function() use ($promise) { + $promise->reject([]); + }); + try { + $promise->wait(); + $this->fail('We did not get the expected exception'); + } catch (\Exception $e) { + $this->assertInstanceOf('Exception', $e); + $this->assertEquals('Promise was rejected with reason of type: array', $e->getMessage()); + } + + } +} diff --git a/htdocs/includes/sabre/sabre/event/tests/benchmark/bench.php b/htdocs/includes/sabre/sabre/event/tests/benchmark/bench.php new file mode 100644 index 00000000000..b1e6b1d4787 --- /dev/null +++ b/htdocs/includes/sabre/sabre/event/tests/benchmark/bench.php @@ -0,0 +1,116 @@ +<?php + +use Sabre\Event\EventEmitter; + +include __DIR__ . '/../../vendor/autoload.php'; + +abstract class BenchMark { + + protected $startTime; + protected $iterations = 10000; + protected $totalTime; + + function setUp() { + + } + + abstract function test(); + + function go() { + + $this->setUp(); + $this->startTime = microtime(true); + $this->test(); + $this->totalTime = microtime(true) - $this->startTime; + return $this->totalTime; + + } + +} + +class OneCallBack extends BenchMark { + + protected $emitter; + protected $iterations = 100000; + + function setUp() { + + $this->emitter = new EventEmitter(); + $this->emitter->on('foo', function() { + // NOOP + }); + + } + + function test() { + + for ($i = 0;$i < $this->iterations;$i++) { + $this->emitter->emit('foo', []); + } + + } + +} + +class ManyCallBacks extends BenchMark { + + protected $emitter; + + function setUp() { + + $this->emitter = new EventEmitter(); + for ($i = 0;$i < 100;$i++) { + $this->emitter->on('foo', function() { + // NOOP + }); + } + + } + + function test() { + + for ($i = 0;$i < $this->iterations;$i++) { + $this->emitter->emit('foo', []); + } + + } + +} + +class ManyPrioritizedCallBacks extends BenchMark { + + protected $emitter; + + function setUp() { + + $this->emitter = new EventEmitter(); + for ($i = 0;$i < 100;$i++) { + $this->emitter->on('foo', function() { + }, 1000 - $i); + } + + } + + function test() { + + for ($i = 0;$i < $this->iterations;$i++) { + $this->emitter->emit('foo', []); + } + + } + +} + +$tests = [ + 'OneCallBack', + 'ManyCallBacks', + 'ManyPrioritizedCallBacks', +]; + +foreach ($tests as $test) { + + $testObj = new $test(); + $result = $testObj->go(); + echo $test . " " . $result . "\n"; + +} diff --git a/htdocs/includes/sabre/sabre/http/.gitignore b/htdocs/includes/sabre/sabre/http/.gitignore new file mode 100644 index 00000000000..8c97686fb57 --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/.gitignore @@ -0,0 +1,15 @@ +# Composer +vendor/ +composer.lock + +# Tests +tests/cov/ + +# Composer binaries +bin/phpunit +bin/phpcs +bin/php-cs-fixer +bin/sabre-cs-fixer + +# Vim +.*.swp diff --git a/htdocs/includes/sabre/sabre/http/.travis.yml b/htdocs/includes/sabre/sabre/http/.travis.yml new file mode 100644 index 00000000000..8ae84d90f8e --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/.travis.yml @@ -0,0 +1,26 @@ +language: php +php: + - 5.4 + - 5.5 + - 5.6 + - 7 + - 7.1 + - hhvm + +matrix: + fast_finish: true + +env: + matrix: + - PREFER_LOWEST="" + - PREFER_LOWEST="--prefer-lowest" + + +before_script: + - rm -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini +# - composer self-update + - composer update --prefer-source $PREFER_LOWEST + +script: + - ./bin/phpunit --configuration tests/phpunit.xml + - ./bin/sabre-cs-fixer fix . --dry-run --diff diff --git a/htdocs/includes/sabre/sabre/http/CHANGELOG.md b/htdocs/includes/sabre/sabre/http/CHANGELOG.md new file mode 100644 index 00000000000..63d85afe35c --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/CHANGELOG.md @@ -0,0 +1,256 @@ +ChangeLog +========= + +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 it's 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 +[rfc-7240]: http://tools.ietf.org/html/rfc7240 diff --git a/htdocs/includes/sabre/sabre/http/LICENSE b/htdocs/includes/sabre/sabre/http/LICENSE new file mode 100644 index 00000000000..864041b22ca --- /dev/null +++ b/htdocs/includes/sabre/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/htdocs/includes/sabre/sabre/http/README.md b/htdocs/includes/sabre/sabre/http/README.md new file mode 100644 index 00000000000..ae03a796e13 --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/README.md @@ -0,0 +1,746 @@ +sabre/http +========== + +This library provides a toolkit to make working with the HTTP protocol 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/fruux/sabre-http.svg?branch=master)](https://travis-ci.org/fruux/sabre-http) | +| 3.0 | [![Build Status](https://travis-ci.org/fruux/sabre-http.svg?branch=3.0)](https://travis-ci.org/fruux/sabre-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" : "~3.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 it's 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 a instance of + `Request` so you would have to override the factory method as well, +3. By controlling the instantation 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 unittest. + +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 youre $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 incoded 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. + * + * @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 it's 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 it's 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-sensitity 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-insenstive. + * 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 or 1.1. + * + * @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 it's 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 it's 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-sensitity 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-insenstive. + * 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 or 1.1. + * + * @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/htdocs/includes/sabre/sabre/http/bin/.empty b/htdocs/includes/sabre/sabre/http/bin/.empty new file mode 100644 index 00000000000..e69de29bb2d diff --git a/htdocs/includes/sabre/sabre/http/composer.json b/htdocs/includes/sabre/sabre/http/composer.json new file mode 100644 index 00000000000..507d5d28df0 --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/composer.json @@ -0,0 +1,44 @@ +{ + "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" : ">=5.4", + "ext-mbstring" : "*", + "ext-ctype" : "*", + "sabre/event" : ">=1.0.0,<4.0.0", + "sabre/uri" : "~1.0" + }, + "require-dev" : { + "phpunit/phpunit" : "~4.3", + "sabre/cs" : "~0.0.1" + }, + "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/" + } + }, + "config" : { + "bin-dir" : "bin/" + } +} diff --git a/htdocs/includes/sabre/sabre/http/examples/asyncclient.php b/htdocs/includes/sabre/sabre/http/examples/asyncclient.php new file mode 100644 index 00000000000..b399e1e4f7b --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/examples/asyncclient.php @@ -0,0 +1,65 @@ +<?php + +/** + * This example demonstrates the ability for clients to work asynchronously. + * + * By default up to 10 requests will be executed in paralel. HTTP connections + * are re-used and DNS is cached, all thanks to the power of curl. + * + * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +use Sabre\HTTP\Client; +use Sabre\HTTP\Request; + +// 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; + } +} + +// This is the request we're repeating a 1000 times. +$request = new Request('GET', 'http://localhost/'); +$client = new Client(); + +for ($i = 0; $i < 1000; $i++) { + + echo "$i sending\n"; + $client->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 ($error['status'] === Client::STATUS_CURLERROR) { + // 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/htdocs/includes/sabre/sabre/http/examples/basicauth.php b/htdocs/includes/sabre/sabre/http/examples/basicauth.php new file mode 100644 index 00000000000..58bb7899268 --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/examples/basicauth.php @@ -0,0 +1,55 @@ +<?php + +/** + * This example shows how to do Basic authentication. + * * + * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +$userList = [ + "user1" => "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/htdocs/includes/sabre/sabre/http/examples/client.php b/htdocs/includes/sabre/sabre/http/examples/client.php new file mode 100644 index 00000000000..d16c1651b6c --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/examples/client.php @@ -0,0 +1,38 @@ +<?php + +/** + * This example shows how to make a HTTP request with the Request and Response + * objects. + * + * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +use Sabre\HTTP\Client; +use Sabre\HTTP\Request; + +// 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; + } +} + +// Constructing the request. +$request = new Request('GET', 'http://localhost/'); + +$client = new Client(); +//$client->addCurlSetting(CURLOPT_PROXY,'localhost:8888'); +$response = $client->send($request); + +echo "Response:\n"; + +echo (string)$response; diff --git a/htdocs/includes/sabre/sabre/http/examples/digestauth.php b/htdocs/includes/sabre/sabre/http/examples/digestauth.php new file mode 100644 index 00000000000..30a5501eb56 --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/examples/digestauth.php @@ -0,0 +1,56 @@ +<?php + +/** + * This example shows how to do Digest authentication. + * * + * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/). + * @author Markus Staab + * @license http://sabre.io/license/ Modified BSD License + */ +$userList = [ + "user1" => "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/htdocs/includes/sabre/sabre/http/examples/reverseproxy.php b/htdocs/includes/sabre/sabre/http/examples/reverseproxy.php new file mode 100644 index 00000000000..289e2b55198 --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/examples/reverseproxy.php @@ -0,0 +1,50 @@ +<?php + +// 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/'; + +use Sabre\HTTP\Client; +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(); +$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); diff --git a/htdocs/includes/sabre/sabre/http/examples/stringify.php b/htdocs/includes/sabre/sabre/http/examples/stringify.php new file mode 100644 index 00000000000..9f56201af9e --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/examples/stringify.php @@ -0,0 +1,51 @@ +<?php + +/** + * This simple example shows the capability of Request and Response objects to + * serialize themselves as strings. + * + * This is mainly useful for debugging purposes. + * + * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +use Sabre\HTTP\Request; +use Sabre\HTTP\Response; + +// 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 = new Request('POST', '/foo'); +$request->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/htdocs/includes/sabre/sabre/http/lib/Auth/AWS.php b/htdocs/includes/sabre/sabre/http/lib/Auth/AWS.php new file mode 100644 index 00000000000..5e176646aaf --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/lib/Auth/AWS.php @@ -0,0 +1,234 @@ +<?php + +namespace Sabre\HTTP\Auth; + +use Sabre\HTTP\Util; + +/** + * HTTP AWS Authentication handler + * + * Use this class to leverage amazon's AWS authentication header + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class AWS extends AbstractAuth { + + /** + * The signature supplied by the HTTP client + * + * @var string + */ + private $signature = null; + + /** + * The accesskey supplied by the HTTP client + * + * @var string + */ + private $accessKey = null; + + /** + * An error code, if any + * + * This value will be filled with one of the ERR_* constants + * + * @var int + */ + public $errorCode = 0; + + const ERR_NOAWSHEADER = 1; + const ERR_MD5CHECKSUMWRONG = 2; + const ERR_INVALIDDATEFORMAT = 3; + const ERR_REQUESTTIMESKEWED = 4; + const ERR_INVALIDSIGNATURE = 5; + + /** + * Gathers all information from the headers + * + * This method needs to be called prior to anything else. + * + * @return bool + */ + function init() { + + $authHeader = $this->request->getHeader('Authorization'); + $authHeader = explode(' ', $authHeader); + + if ($authHeader[0] != 'AWS' || !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 + * + * @return string + */ + function getAccessKey() { + + return $this->accessKey; + + } + + /** + * Validates the signature based on the secretKey + * + * @param string $secretKey + * @return bool + */ + function validate($secretKey) { + + $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($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($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 + * + * @return void + */ + 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. + * + * @param string $dateHeader + * @return bool + */ + protected function validateRFC2616Date($dateHeader) { + + $date = Util::parseHTTPDate($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 $date; + + } + + /** + * Returns a list of AMZ headers + * + * @return string + */ + protected function getAmzHeaders() { + + $amzHeaders = []; + $headers = $this->request->getHeaders(); + foreach ($headers as $headerName => $headerValue) { + if (strpos(strtolower($headerName), 'x-amz-') === 0) { + $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 + * + * @param string $key + * @param string $message + * @return string + */ + private function hmacsha1($key, $message) { + + 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/htdocs/includes/sabre/sabre/http/lib/Auth/AbstractAuth.php b/htdocs/includes/sabre/sabre/http/lib/Auth/AbstractAuth.php new file mode 100644 index 00000000000..ae45b3ee22a --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/lib/Auth/AbstractAuth.php @@ -0,0 +1,73 @@ +<?php + +namespace Sabre\HTTP\Auth; + +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +/** + * HTTP Authentication base class. + * + * This class provides some common functionality for the various base classes. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +abstract class AbstractAuth { + + /** + * Authentication realm + * + * @var string + */ + protected $realm; + + /** + * Request object + * + * @var RequestInterface + */ + protected $request; + + /** + * Response object + * + * @var ResponseInterface + */ + protected $response; + + /** + * Creates the object + * + * @param string $realm + * @return void + */ + function __construct($realm = 'SabreTooth', RequestInterface $request, ResponseInterface $response) { + + $this->realm = $realm; + $this->request = $request; + $this->response = $response; + + } + + /** + * This method sends the needed HTTP header and statuscode (401) to force + * the user to login. + * + * @return void + */ + abstract function requireLogin(); + + /** + * Returns the HTTP realm + * + * @return string + */ + function getRealm() { + + return $this->realm; + + } + +} diff --git a/htdocs/includes/sabre/sabre/http/lib/Auth/Basic.php b/htdocs/includes/sabre/sabre/http/lib/Auth/Basic.php new file mode 100644 index 00000000000..60633b95726 --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/lib/Auth/Basic.php @@ -0,0 +1,63 @@ +<?php + +namespace Sabre\HTTP\Auth; + +/** + * HTTP Basic authentication utility. + * + * This class helps you setup basic auth. The process is fairly simple: + * + * 1. Instantiate the class. + * 2. Call getCredentials (this will return null or a user/pass pair) + * 3. If you didn't get valid credentials, call 'requireLogin' + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Basic extends AbstractAuth { + + /** + * This method returns a numeric array with a username and password as the + * only elements. + * + * If no credentials were found, this method returns null. + * + * @return null|array + */ + function getCredentials() { + + $auth = $this->request->getHeader('Authorization'); + + if (!$auth) { + return null; + } + + if (strtolower(substr($auth, 0, 6)) !== 'basic ') { + 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 statuscode (401) to force + * the user to login. + * + * @return void + */ + function requireLogin() { + + $this->response->addHeader('WWW-Authenticate', 'Basic realm="' . $this->realm . '"'); + $this->response->setStatus(401); + + } + +} diff --git a/htdocs/includes/sabre/sabre/http/lib/Auth/Bearer.php b/htdocs/includes/sabre/sabre/http/lib/Auth/Bearer.php new file mode 100644 index 00000000000..eefdf11ee97 --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/lib/Auth/Bearer.php @@ -0,0 +1,56 @@ +<?php + +namespace Sabre\HTTP\Auth; + +/** + * HTTP Bearer authentication utility. + * + * This class helps you setup bearer auth. The process is fairly simple: + * + * 1. Instantiate the class. + * 2. Call getToken (this will return null or a token as string) + * 3. If you didn't get a valid token, call 'requireLogin' + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author François Kooman (fkooman@tuxed.net) + * @license http://sabre.io/license/ Modified BSD License + */ +class Bearer extends AbstractAuth { + + /** + * This method returns a string with an access token. + * + * If no token was found, this method returns null. + * + * @return null|string + */ + function getToken() { + + $auth = $this->request->getHeader('Authorization'); + + if (!$auth) { + return null; + } + + if (strtolower(substr($auth, 0, 7)) !== 'bearer ') { + return null; + } + + return substr($auth, 7); + + } + + /** + * This method sends the needed HTTP header and statuscode (401) to force + * authentication. + * + * @return void + */ + function requireLogin() { + + $this->response->addHeader('WWW-Authenticate', 'Bearer realm="' . $this->realm . '"'); + $this->response->setStatus(401); + + } + +} diff --git a/htdocs/includes/sabre/sabre/http/lib/Auth/Digest.php b/htdocs/includes/sabre/sabre/http/lib/Auth/Digest.php new file mode 100644 index 00000000000..4b3f0746fc6 --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/lib/Auth/Digest.php @@ -0,0 +1,231 @@ +<?php + +namespace Sabre\HTTP\Auth; + +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +/** + * HTTP Digest Authentication handler + * + * Use this class for easy http digest authentication. + * Instructions: + * + * 1. Create the object + * 2. Call the setRealm() method with the realm you plan to use + * 3. Call the init method function. + * 4. Call the getUserName() function. This function may return null if no + * authentication information was supplied. Based on the username you + * should check your internal database for either the associated password, + * or the so-called A1 hash of the digest. + * 5. Call either validatePassword() or validateA1(). This will return true + * or false. + * 6. To make sure an authentication prompt is displayed, call the + * requireLogin() method. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Digest extends AbstractAuth { + + /** + * These constants are used in setQOP(); + */ + const QOP_AUTH = 1; + const QOP_AUTHINT = 2; + + protected $nonce; + protected $opaque; + protected $digestParts; + protected $A1; + protected $qop = self::QOP_AUTH; + + /** + * Initializes the object + */ + function __construct($realm = 'SabreTooth', RequestInterface $request, ResponseInterface $response) { + + $this->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. + * + * @return void + */ + function init() { + + $digest = $this->getDigest(); + $this->digestParts = $this->parseDigest($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. + * + * @param int $qop + * @return void + */ + function setQOP($qop) { + + $this->qop = $qop; + + } + + /** + * Validates the user. + * + * The A1 parameter should be md5($username . ':' . $realm . ':' . $password); + * + * @param string $A1 + * @return bool + */ + function validateA1($A1) { + + $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. + * + * @param string $password + * @return bool + */ + function validatePassword($password) { + + $this->A1 = md5($this->digestParts['username'] . ':' . $this->realm . ':' . $password); + return $this->validate(); + + } + + /** + * Returns the username for the request + * + * @return string + */ + function getUsername() { + + return $this->digestParts['username']; + + } + + /** + * Validates the digest challenge + * + * @return bool + */ + protected function validate() { + + $A2 = $this->request->getMethod() . ':' . $this->digestParts['uri']; + + if ($this->digestParts['qop'] == 'auth-int') { + // 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($asString = true); + $this->request->setBody($body); + $A2 .= ':' . md5($body); + } else { + + // We need to make sure we support this qop value + if (!($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 + * + * @return void + */ + 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 compatibile with mod_php format and other webservers. + * + * If the header could not be found, null will be returned + * + * @return mixed + */ + 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 + * + * @param string $digest + * @return mixed + */ + protected function parseDigest($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[2] : $m[3]; + unset($needed_parts[$m[1]]); + } + + return $needed_parts ? false : $data; + + } + +} diff --git a/htdocs/includes/sabre/sabre/http/lib/Client.php b/htdocs/includes/sabre/sabre/http/lib/Client.php new file mode 100644 index 00000000000..0810c4a25b1 --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/lib/Client.php @@ -0,0 +1,601 @@ +<?php + +namespace Sabre\HTTP; + +use Sabre\Event\EventEmitter; +use Sabre\Uri; + +/** + * A rudimentary HTTP client. + * + * This object wraps PHP's curl extension and provides an easy way to send it a + * Request object, and return a Response object. + * + * This is by no means intended as the next best HTTP client, but it does the + * job and provides a simple integration with the rest of sabre/http. + * + * This client emits the following events: + * beforeRequest(RequestInterface $request) + * afterRequest(RequestInterface $request, ResponseInterface $response) + * error(RequestInterface $request, ResponseInterface $response, bool &$retry, int $retryCount) + * exception(RequestInterface $request, ClientException $e, bool &$retry, int $retryCount) + * + * The beforeRequest event allows you to do some last minute changes to the + * request before it's done, such as adding authentication headers. + * + * The afterRequest event will be emitted after the request is completed + * succesfully. + * + * If a HTTP error is returned (status code higher than 399) the error event is + * triggered. It's possible using this event to retry the request, by setting + * retry to true. + * + * The amount of times a request has retried is passed as $retryCount, which + * can be used to avoid retrying indefinitely. The first time the event is + * called, this will be 0. + * + * It's also possible to intercept specific http errors, by subscribing to for + * example 'error:401'. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Client extends EventEmitter { + + /** + * List of curl settings + * + * @var array + */ + protected $curlSettings = []; + + /** + * Wether or not exceptions should be thrown when a HTTP error is returned. + * + * @var bool + */ + protected $throwExceptions = false; + + /** + * The maximum number of times we'll follow a redirect. + * + * @var int + */ + protected $maxRedirects = 5; + + /** + * Initializes the client. + * + * @return void + */ + function __construct() { + + $this->curlSettings = [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADER => true, + CURLOPT_NOBODY => false, + CURLOPT_USERAGENT => 'sabre-http/' . Version::VERSION . ' (http://sabre.io/)', + ]; + + } + + /** + * Sends a request to a HTTP server, and returns a response. + * + * @param RequestInterface $request + * @return ResponseInterface + */ + function send(RequestInterface $request) { + + $this->emit('beforeRequest', [$request]); + + $retryCount = 0; + $redirects = 0; + + do { + + $doRedirect = false; + $retry = false; + + try { + + $response = $this->doRequest($request); + + $code = (int)$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 (in_array($code, [301, 302, 307, 308]) && $redirects < $this->maxRedirects) { + + $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(). + * + * @param RequestInterface $request + * @param callable $success + * @param callable $error + * @return void + */ + 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. + * + * @return bool + */ + function poll() { + + // nothing to do? + if (!$this->curlMultiMap) { + return false; + } + + do { + $r = curl_multi_exec( + $this->curlMultiHandle, + $stillRunning + ); + } while ($r === CURLM_CALL_MULTI_PERFORM); + + do { + + messageQueue: + + $status = curl_multi_info_read( + $this->curlMultiHandle, + $messagesInQueue + ); + + if ($status && $status['msg'] === CURLMSG_DONE) { + + $resourceId = intval($status['handle']); + list( + $request, + $successCallback, + $errorCallback, + $retryCount, + ) = $this->curlMultiMap[$resourceId]; + unset($this->curlMultiMap[$resourceId]); + $curlResult = $this->parseCurlResult(curl_multi_getcontent($status['handle']), $status['handle']); + $retry = false; + + if ($curlResult['status'] === self::STATUS_CURLERROR) { + + $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 ($curlResult['status'] === self::STATUS_HTTPERROR) { + + $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. + * + * @return void + */ + 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. + * + * @param bool $throwExceptions + * @return void + */ + function setThrowExceptions($throwExceptions) { + + $this->throwExceptions = $throwExceptions; + + } + + /** + * Adds a CURL setting. + * + * These settings will be included in every HTTP request. + * + * @param int $name + * @param mixed $value + * @return void + */ + function addCurlSetting($name, $value) { + + $this->curlSettings[$name] = $value; + + } + + /** + * This method is responsible for performing a single request. + * + * @param RequestInterface $request + * @return ResponseInterface + */ + protected function doRequest(RequestInterface $request) { + + $settings = $this->createCurlSettingsArray($request); + + if (!$this->curlHandle) { + $this->curlHandle = curl_init(); + } + + curl_setopt_array($this->curlHandle, $settings); + $response = $this->curlExec($this->curlHandle); + $response = $this->parseCurlResult($response, $this->curlHandle); + + if ($response['status'] === self::STATUS_CURLERROR) { + 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 + * + * @param RequestInterface $request + * @return array + */ + protected function createCurlSettingsArray(RequestInterface $request) { + + $settings = $this->curlSettings; + + switch ($request->getMethod()) { + case 'HEAD' : + $settings[CURLOPT_NOBODY] = true; + $settings[CURLOPT_CUSTOMREQUEST] = 'HEAD'; + $settings[CURLOPT_POSTFIELDS] = ''; + $settings[CURLOPT_PUT] = false; + break; + case 'GET' : + $settings[CURLOPT_CUSTOMREQUEST] = 'GET'; + $settings[CURLOPT_POSTFIELDS] = ''; + $settings[CURLOPT_PUT] = false; + break; + default : + $body = $request->getBody(); + if (is_resource($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] = $request->getBody(); + } 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; + } + + } + $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; + + } + + const STATUS_SUCCESS = 0; + const STATUS_CURLERROR = 1; + const STATUS_HTTPERROR = 2; + + /** + * 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 string $response + * @param resource $curlHandle + * @return Response + */ + protected function parseCurlResult($response, $curlHandle) { + + 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']) ?: null; + + 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); + + $response = new Response(); + $response->setStatus($curlInfo['http_code']); + + foreach ($headerBlob as $header) { + $parts = explode(':', $header, 2); + if (count($parts) == 2) { + $response->addHeader(trim($parts[0]), trim($parts[1])); + } + } + + $response->setBody($responseBody); + + $httpCode = intval($response->getStatus()); + + return [ + 'status' => $httpCode >= 400 ? self::STATUS_HTTPERROR : self::STATUS_SUCCESS, + 'response' => $response, + 'http_code' => $httpCode, + ]; + + } + + /** + * 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(). + * + * @param RequestInterface $request + * @param callable $success + * @param callable $error + * @param int $retryCount + */ + protected function sendAsyncInternal(RequestInterface $request, callable $success, callable $error, $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); + $this->curlMultiMap[intval($curl)] = [ + $request, + $success, + $error, + $retryCount + ]; + + } + + // @codeCoverageIgnoreStart + + /** + * Calls curl_exec + * + * This method exists so it can easily be overridden and mocked. + * + * @param resource $curlHandle + * @return string + */ + protected function curlExec($curlHandle) { + + return curl_exec($curlHandle); + + } + + /** + * Returns a bunch of information about a curl request. + * + * This method exists so it can easily be overridden and mocked. + * + * @param resource $curlHandle + * @return array + */ + protected function curlStuff($curlHandle) { + + return [ + curl_getinfo($curlHandle), + curl_errno($curlHandle), + curl_error($curlHandle), + ]; + + } + // @codeCoverageIgnoreEnd + +} diff --git a/htdocs/includes/sabre/sabre/http/lib/ClientException.php b/htdocs/includes/sabre/sabre/http/lib/ClientException.php new file mode 100644 index 00000000000..69631f44eef --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/lib/ClientException.php @@ -0,0 +1,15 @@ +<?php + +namespace Sabre\HTTP; + +/** + * This exception may be emitted by the HTTP\Client class, in case there was a + * problem emitting the request. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class ClientException extends \Exception { + +} diff --git a/htdocs/includes/sabre/sabre/http/lib/ClientHttpException.php b/htdocs/includes/sabre/sabre/http/lib/ClientHttpException.php new file mode 100644 index 00000000000..2923ef3b53a --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/lib/ClientHttpException.php @@ -0,0 +1,58 @@ +<?php + +namespace Sabre\HTTP; + +/** + * This exception represents a HTTP error coming from the Client. + * + * By default the Client will not emit these, this has to be explicitly enabled + * with the setThrowExceptions method. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class ClientHttpException extends \Exception implements HttpException { + + /** + * Response object + * + * @var ResponseInterface + */ + protected $response; + + /** + * Constructor + * + * @param ResponseInterface $response + */ + function __construct(ResponseInterface $response) { + + $this->response = $response; + parent::__construct($response->getStatusText(), $response->getStatus()); + + } + + /** + * The http status code for the error. + * + * @return int + */ + function getHttpStatus() { + + return $this->response->getStatus(); + + } + + /** + * Returns the full response object. + * + * @return ResponseInterface + */ + function getResponse() { + + return $this->response; + + } + +} diff --git a/htdocs/includes/sabre/sabre/http/lib/HttpException.php b/htdocs/includes/sabre/sabre/http/lib/HttpException.php new file mode 100644 index 00000000000..1303dec970e --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/lib/HttpException.php @@ -0,0 +1,30 @@ +<?php + +namespace Sabre\HTTP; + +/** + * An exception representing a HTTP error. + * + * This can be used as a generic exception in your application, if you'd like + * to map HTTP errors to exceptions. + * + * If you'd like to use this, create a new exception class, extending Exception + * and implementing this interface. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +interface HttpException { + + /** + * The http status code for the error. + * + * This may either be just the number, or a number and a human-readable + * message, separated by a space. + * + * @return string|null + */ + function getHttpStatus(); + +} diff --git a/htdocs/includes/sabre/sabre/http/lib/Message.php b/htdocs/includes/sabre/sabre/http/lib/Message.php new file mode 100644 index 00000000000..45bd183980d --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/lib/Message.php @@ -0,0 +1,314 @@ +<?php + +namespace Sabre\HTTP; + +/** + * This is the abstract base class for both the Request and Response objects. + * + * This object contains a few simple methods that are shared by both. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +abstract class Message implements MessageInterface { + + /** + * Request body + * + * This should be a stream resource + * + * @var resource + */ + protected $body; + + /** + * Contains the list of HTTP headers + * + * @var array + */ + protected $headers = []; + + /** + * HTTP message version (1.0 or 1.1) + * + * @var string + */ + protected $httpVersion = '1.1'; + + /** + * 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() { + + $body = $this->getBody(); + if (is_string($body) || is_null($body)) { + $stream = fopen('php://temp', 'r+'); + fwrite($stream, $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. + * + * @return string + */ + function getBodyAsString() { + + $body = $this->getBody(); + if (is_string($body)) { + return $body; + } + if (is_null($body)) { + return ''; + } + $contentLength = $this->getHeader('Content-Length'); + if (is_int($contentLength) || ctype_digit($contentLength)) { + return stream_get_contents($body, $contentLength); + } else { + return stream_get_contents($body); + } + } + + /** + * Returns the message body, as it's internal representation. + * + * This could be either a string or a stream. + * + * @return resource|string + */ + function getBody() { + + return $this->body; + + } + + /** + * Replaces the body resource with a new stream or string. + * + * @param resource|string $body + */ + 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. + * + * @return array + */ + function getHeaders() { + + $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. + * + * @param string $name + * @return bool + */ + function hasHeader($name) { + + return isset($this->headers[strtolower($name)]); + + } + + /** + * Returns a specific HTTP header, based on it's 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(). + * + * @param string $name + * @return string|null + */ + function getHeader($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 exists, this method will return an empty array. + * + * @param string $name + * @return string[] + */ + function getHeaderAsArray($name) { + + $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 $name + * @param string|string[] $value + * @return void + */ + function setHeader($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. + * + * @param array $headers + * @return void + */ + 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 $name + * @param string $value + * @return void + */ + function addHeader($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. + * + * @param array $headers + * @return void + */ + 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. + * + * @param string $name + * @return bool + */ + function removeHeader($name) { + + $name = strtolower($name); + if (!isset($this->headers[$name])) { + return false; + } + unset($this->headers[$name]); + return true; + + } + + /** + * Sets the HTTP version. + * + * Should be 1.0 or 1.1. + * + * @param string $version + * @return void + */ + function setHttpVersion($version) { + + $this->httpVersion = $version; + + } + + /** + * Returns the HTTP version. + * + * @return string + */ + function getHttpVersion() { + + return $this->httpVersion; + + } +} diff --git a/htdocs/includes/sabre/sabre/http/lib/MessageDecoratorTrait.php b/htdocs/includes/sabre/sabre/http/lib/MessageDecoratorTrait.php new file mode 100644 index 00000000000..1cb32da2259 --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/lib/MessageDecoratorTrait.php @@ -0,0 +1,251 @@ +<?php + +namespace Sabre\HTTP; + +/** + * This trait contains a bunch of methods, shared by both the RequestDecorator + * and the ResponseDecorator. + * + * Didn't seem needed to create a full class for this, so we're just + * implementing it as a trait. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +trait MessageDecoratorTrait { + + /** + * The inner request object. + * + * All method calls will be forwarded here. + * + * @var MessageInterface + */ + protected $inner; + + /** + * 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() { + + return $this->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. + * + * @return string + */ + function getBodyAsString() { + + return $this->inner->getBodyAsString(); + + } + + /** + * Returns the message body, as it's internal representation. + * + * This could be either a string or a stream. + * + * @return resource|string + */ + function getBody() { + + return $this->inner->getBody(); + + } + + /** + * Updates the body resource with a new stream. + * + * @param resource $body + * @return void + */ + 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. + * + * @return array + */ + function getHeaders() { + + return $this->inner->getHeaders(); + + } + + /** + * Will return true or false, depending on if a HTTP header exists. + * + * @param string $name + * @return bool + */ + function hasHeader($name) { + + return $this->inner->hasHeader($name); + + } + + /** + * Returns a specific HTTP header, based on it's 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(). + * + * @param string $name + * @return string|null + */ + function getHeader($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 exists, this method will return an empty array. + * + * @param string $name + * @return string[] + */ + function getHeaderAsArray($name) { + + 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 $name + * @param string|string[] $value + * @return void + */ + function setHeader($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. + * + * @param array $headers + * @return void + */ + 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 $name + * @param string $value + * @return void + */ + function addHeader($name, $value) { + + $this->inner->addHeader($name, $value); + + } + + /** + * Adds a new set of HTTP headers. + * + * Any existing headers will not be overwritten. + * + * @param array $headers + * @return void + */ + 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. + * + * @param string $name + * @return bool + */ + function removeHeader($name) { + + return $this->inner->removeHeader($name); + + } + + /** + * Sets the HTTP version. + * + * Should be 1.0 or 1.1. + * + * @param string $version + * @return void + */ + function setHttpVersion($version) { + + $this->inner->setHttpVersion($version); + + } + + /** + * Returns the HTTP version. + * + * @return string + */ + function getHttpVersion() { + + return $this->inner->getHttpVersion(); + + } + +} diff --git a/htdocs/includes/sabre/sabre/http/lib/MessageInterface.php b/htdocs/includes/sabre/sabre/http/lib/MessageInterface.php new file mode 100644 index 00000000000..df55beb2ffe --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/lib/MessageInterface.php @@ -0,0 +1,178 @@ +<?php + +namespace Sabre\HTTP; + +/** + * The MessageInterface is the base interface that's used by both + * the RequestInterface and ResponseInterface. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +interface MessageInterface { + + /** + * 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 it's 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|string $body + * @return void + */ + function setBody($body); + + /** + * Returns all the HTTP headers as an array. + * + * Every header is returned as an array, with one or more values. + * + * @return array + */ + function getHeaders(); + + /** + * Will return true or false, depending on if a HTTP header exists. + * + * @param string $name + * @return bool + */ + function hasHeader($name); + + /** + * Returns a specific HTTP header, based on it's 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(). + * + * @param string $name + * @return string|null + */ + function 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 exists, this method will return an empty array. + * + * @param string $name + * @return string[] + */ + function getHeaderAsArray($name); + + /** + * Updates a HTTP header. + * + * The case-sensitity of the name value must be retained as-is. + * + * If the header already existed, it will be overwritten. + * + * @param string $name + * @param string|string[] $value + * @return void + */ + function 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. + * + * @param array $headers + * @return void + */ + function setHeaders(array $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 $name + * @param string $value + * @return void + */ + function addHeader($name, $value); + + /** + * Adds a new set of HTTP headers. + * + * Any existing headers will not be overwritten. + * + * @param array $headers + * @return void + */ + function addHeaders(array $headers); + + /** + * Removes a HTTP header. + * + * The specified header name must be treated as case-insenstive. + * This method should return true if the header was successfully deleted, + * and false if the header did not exist. + * + * @param string $name + * @return bool + */ + function removeHeader($name); + + /** + * Sets the HTTP version. + * + * Should be 1.0 or 1.1. + * + * @param string $version + * @return void + */ + function setHttpVersion($version); + + /** + * Returns the HTTP version. + * + * @return string + */ + function getHttpVersion(); + +} diff --git a/htdocs/includes/sabre/sabre/http/lib/Request.php b/htdocs/includes/sabre/sabre/http/lib/Request.php new file mode 100644 index 00000000000..dfa3d5b4857 --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/lib/Request.php @@ -0,0 +1,316 @@ +<?php + +namespace Sabre\HTTP; + +use InvalidArgumentException; +use Sabre\Uri; + +/** + * The Request class represents a single HTTP request. + * + * You can either simply construct the object from scratch, or if you need + * access to the current HTTP request, use Sapi::getRequest. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Request extends Message implements RequestInterface { + + /** + * HTTP Method + * + * @var string + */ + protected $method; + + /** + * Request Url + * + * @var string + */ + protected $url; + + /** + * Creates the request object + * + * @param string $method + * @param string $url + * @param array $headers + * @param resource $body + */ + function __construct($method = null, $url = null, array $headers = null, $body = null) { + + if (is_array($method)) { + throw new InvalidArgumentException('The first argument for this constructor should be a string or null, not an array. Did you upgrade from sabre/http 1.0 to 2.0?'); + } + if (!is_null($method)) $this->setMethod($method); + if (!is_null($url)) $this->setUrl($url); + if (!is_null($headers)) $this->setHeaders($headers); + if (!is_null($body)) $this->setBody($body); + + } + + /** + * Returns the current HTTP method + * + * @return string + */ + function getMethod() { + + return $this->method; + + } + + /** + * Sets the HTTP method + * + * @param string $method + * @return void + */ + function setMethod($method) { + + $this->method = $method; + + } + + /** + * Returns the request url. + * + * @return string + */ + function getUrl() { + + return $this->url; + + } + + /** + * Sets the request url. + * + * @param string $url + * @return void + */ + function setUrl($url) { + + $this->url = $url; + + } + + /** + * Returns the list of query parameters. + * + * This is equivalent to PHP's $_GET superglobal. + * + * @return array + */ + function getQueryParameters() { + + $url = $this->getUrl(); + if (($index = strpos($url, '?')) === false) { + return []; + } else { + parse_str(substr($url, $index + 1), $queryParams); + return $queryParams; + } + + } + + /** + * Sets the absolute url. + * + * @param string $url + * @return void + */ + function setAbsoluteUrl($url) { + + $this->absoluteUrl = $url; + + } + + /** + * Returns the absolute url. + * + * @return string + */ + function getAbsoluteUrl() { + + return $this->absoluteUrl; + + } + + /** + * Base url + * + * @var string + */ + protected $baseUrl = '/'; + + /** + * Sets a base url. + * + * This url is used for relative path calculations. + * + * @param string $url + * @return void + */ + function setBaseUrl($url) { + + $this->baseUrl = $url; + + } + + /** + * Returns the current base url. + * + * @return string + */ + function getBaseUrl() { + + 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 incoded 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. + * + * @return string + */ + function getPath() { + + // Removing duplicated slashes. + $uri = str_replace('//', '/', $this->getUrl()); + + $uri = Uri\normalize($uri); + $baseUri = Uri\normalize($this->getBaseUrl()); + + if (strpos($uri, $baseUri) === 0) { + + // We're not interested in the query part (everything after the ?). + list($uri) = explode('?', $uri); + return trim(URLUtil::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 ''; + + } + + 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. + * + * @param array $postData + * @return void + */ + function setPostData(array $postData) { + + $this->postData = $postData; + + } + + /** + * Returns the POST data. + * + * This is equivalent to PHP's $_POST superglobal. + * + * @return array + */ + function getPostData() { + + 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. + * + * @param string $valueName + * @return string|null + */ + function getRawServerValue($valueName) { + + if (isset($this->rawServerData[$valueName])) { + return $this->rawServerData[$valueName]; + } + + } + + /** + * Sets the _SERVER array. + * + * @param array $data + * @return void + */ + function setRawServerData(array $data) { + + $this->rawServerData = $data; + + } + + /** + * Serializes the request object as a string. + * + * This is useful for debugging purposes. + * + * @return string + */ + function __toString() { + + $out = $this->getMethod() . ' ' . $this->getUrl() . ' HTTP/' . $this->getHTTPVersion() . "\r\n"; + + foreach ($this->getHeaders() as $key => $value) { + foreach ($value as $v) { + if ($key === 'Authorization') { + list($v) = explode(' ', $v, 2); + $v .= ' REDACTED'; + } + $out .= $key . ": " . $v . "\r\n"; + } + } + $out .= "\r\n"; + $out .= $this->getBodyAsString(); + + return $out; + + } + +} diff --git a/htdocs/includes/sabre/sabre/http/lib/RequestDecorator.php b/htdocs/includes/sabre/sabre/http/lib/RequestDecorator.php new file mode 100644 index 00000000000..7ee3f6fc85c --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/lib/RequestDecorator.php @@ -0,0 +1,231 @@ +<?php + +namespace Sabre\HTTP; + +/** + * Request Decorator + * + * This helper class allows you to easily create decorators for the Request + * object. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class RequestDecorator implements RequestInterface { + + use MessageDecoratorTrait; + + /** + * Constructor. + * + * @param RequestInterface $inner + */ + function __construct(RequestInterface $inner) { + + $this->inner = $inner; + + } + + /** + * Returns the current HTTP method + * + * @return string + */ + function getMethod() { + + return $this->inner->getMethod(); + + } + + /** + * Sets the HTTP method + * + * @param string $method + * @return void + */ + function setMethod($method) { + + $this->inner->setMethod($method); + + } + + /** + * Returns the request url. + * + * @return string + */ + function getUrl() { + + return $this->inner->getUrl(); + + } + + /** + * Sets the request url. + * + * @param string $url + * @return void + */ + function setUrl($url) { + + $this->inner->setUrl($url); + + } + + /** + * Returns the absolute url. + * + * @return string + */ + function getAbsoluteUrl() { + + return $this->inner->getAbsoluteUrl(); + + } + + /** + * Sets the absolute url. + * + * @param string $url + * @return void + */ + function setAbsoluteUrl($url) { + + $this->inner->setAbsoluteUrl($url); + + } + + /** + * Returns the current base url. + * + * @return string + */ + function getBaseUrl() { + + return $this->inner->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) { + + $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 incoded 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. + * + * @return string + */ + function getPath() { + + return $this->inner->getPath(); + + } + + /** + * Returns the list of query parameters. + * + * This is equivalent to PHP's $_GET superglobal. + * + * @return array + */ + function getQueryParameters() { + + return $this->inner->getQueryParameters(); + + } + + /** + * Returns the POST data. + * + * This is equivalent to PHP's $_POST superglobal. + * + * @return array + */ + function getPostData() { + + 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. + * + * @param array $postData + * @return void + */ + 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. + * + * @param string $valueName + * @return string|null + */ + function getRawServerValue($valueName) { + + return $this->inner->getRawServerValue($valueName); + + } + + /** + * Sets the _SERVER array. + * + * @param array $data + * @return void + */ + function setRawServerData(array $data) { + + $this->inner->setRawServerData($data); + + } + + /** + * Serializes the request object as a string. + * + * This is useful for debugging purposes. + * + * @return string + */ + function __toString() { + + return $this->inner->__toString(); + + } +} diff --git a/htdocs/includes/sabre/sabre/http/lib/RequestInterface.php b/htdocs/includes/sabre/sabre/http/lib/RequestInterface.php new file mode 100644 index 00000000000..63d9cbb51a0 --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/lib/RequestInterface.php @@ -0,0 +1,147 @@ +<?php + +namespace Sabre\HTTP; + +/** + * The RequestInterface represents a HTTP request. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +interface RequestInterface extends MessageInterface { + + /** + * 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 incoded 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. + * + * @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); + + +} diff --git a/htdocs/includes/sabre/sabre/http/lib/Response.php b/htdocs/includes/sabre/sabre/http/lib/Response.php new file mode 100644 index 00000000000..01920d8d9fb --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/lib/Response.php @@ -0,0 +1,193 @@ +<?php + +namespace Sabre\HTTP; + +/** + * This class represents a single HTTP response. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Response extends Message implements ResponseInterface { + + /** + * This is the list of currently registered HTTP status codes. + * + * @var array + */ + static $statusCodes = [ + 100 => 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authorative 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 array $headers + * @param resource $body + */ + function __construct($status = null, array $headers = null, $body = null) { + + if (!is_null($status)) $this->setStatus($status); + if (!is_null($headers)) $this->setHeaders($headers); + if (!is_null($body)) $this->setBody($body); + + } + + + /** + * Returns the current HTTP status code. + * + * @return int + */ + function getStatus() { + + return $this->status; + + } + + /** + * Returns the human-readable status string. + * + * In the case of a 200, this may for example be 'OK'. + * + * @return string + */ + function getStatusText() { + + 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 + * @return void + */ + function setStatus($status) { + + if (ctype_digit($status) || is_int($status)) { + + $statusCode = $status; + $statusText = isset(self::$statusCodes[$status]) ? self::$statusCodes[$status] : 'Unknown'; + + } else { + list( + $statusCode, + $statusText + ) = explode(' ', $status, 2); + } + 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. + * + * @return string + */ + function __toString() { + + $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/htdocs/includes/sabre/sabre/http/lib/ResponseDecorator.php b/htdocs/includes/sabre/sabre/http/lib/ResponseDecorator.php new file mode 100644 index 00000000000..db3a6750718 --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/lib/ResponseDecorator.php @@ -0,0 +1,84 @@ +<?php + +namespace Sabre\HTTP; + +/** + * Response Decorator + * + * This helper class allows you to easily create decorators for the Response + * object. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class ResponseDecorator implements ResponseInterface { + + use MessageDecoratorTrait; + + /** + * Constructor. + * + * @param ResponseInterface $inner + */ + function __construct(ResponseInterface $inner) { + + $this->inner = $inner; + + } + + /** + * Returns the current HTTP status code. + * + * @return int + */ + function getStatus() { + + return $this->inner->getStatus(); + + } + + + /** + * Returns the human-readable status string. + * + * In the case of a 200, this may for example be 'OK'. + * + * @return string + */ + function getStatusText() { + + 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 + * @return void + */ + function setStatus($status) { + + $this->inner->setStatus($status); + + } + + /** + * Serializes the request object as a string. + * + * This is useful for debugging purposes. + * + * @return string + */ + function __toString() { + + return $this->inner->__toString(); + + } +} diff --git a/htdocs/includes/sabre/sabre/http/lib/ResponseInterface.php b/htdocs/includes/sabre/sabre/http/lib/ResponseInterface.php new file mode 100644 index 00000000000..411cdc06cbb --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/lib/ResponseInterface.php @@ -0,0 +1,45 @@ +<?php + +namespace Sabre\HTTP; + +/** + * This interface represents a HTTP response. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +interface ResponseInterface extends MessageInterface { + + /** + * Returns the current HTTP status code. + * + * @return int + */ + function getStatus(); + + /** + * Returns the human-readable status string. + * + * In the case of a 200, this may for example be 'OK'. + * + * @return string + */ + function 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 + * @throws \InvalidArgumentException + * @return void + */ + function setStatus($status); + +} diff --git a/htdocs/includes/sabre/sabre/http/lib/Sapi.php b/htdocs/includes/sabre/sabre/http/lib/Sapi.php new file mode 100644 index 00000000000..6c83c8719d5 --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/lib/Sapi.php @@ -0,0 +1,194 @@ +<?php + +namespace Sabre\HTTP; + +/** + * PHP SAPI + * + * This object is responsible for: + * 1. Constructing a Request object based on the current HTTP request sent to + * the PHP process. + * 2. Sending the Response object back to the client. + * + * It could be said that this class provides a mapping between the Request and + * Response objects, and php's: + * + * * $_SERVER + * * $_POST + * * $_FILES + * * php://input + * * echo() + * * header() + * * php://output + * + * You can choose to either call all these methods statically, but you can also + * instantiate this as an object to allow for polymorhpism. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Sapi { + + /** + * This static method will create a new Request object, based on the + * current PHP request. + * + * @return Request + */ + static function getRequest() { + + $r = self::createFromServerArray($_SERVER); + $r->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. + * + * @param ResponseInterface $response + * @return void + */ + 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 ($k === 0) { + header($key . ': ' . $v); + } else { + header($key . ': ' . $v, false); + } + } + + } + + $body = $response->getBody(); + if (is_null($body)) return; + + $contentLength = $response->getHeader('Content-Length'); + if ($contentLength !== null) { + $output = fopen('php://output', 'wb'); + if (is_resource($body) && get_resource_type($body) == 'stream') { + stream_copy_to_stream($body, $output, $contentLength); + } else { + fwrite($output, $body, $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. + * + * @param array $serverArray + * @return Request + */ + static function createFromServerArray(array $serverArray) { + + $headers = []; + $method = null; + $url = null; + $httpVersion = '1.1'; + + $protocol = 'http'; + $hostName = 'localhost'; + + foreach ($serverArray as $key => $value) { + + switch ($key) { + + case 'SERVER_PROTOCOL' : + if ($value === 'HTTP/1.0') { + $httpVersion = '1.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) && $value !== 'off') { + $protocol = 'https'; + } + break; + + default : + if (substr($key, 0, 5) === 'HTTP_') { + // It's a HTTP header + + // Normalizing it to be prettier + $header = strtolower(substr($key, 5)); + + // Transforming dashes into spaces, and uppercasing + // every first letter. + $header = ucwords(str_replace('_', ' ', $header)); + + // Turning spaces into dashes. + $header = str_replace(' ', '-', $header); + $headers[$header] = $value; + + } + break; + + + } + + } + + $r = new Request($method, $url, $headers); + $r->setHttpVersion($httpVersion); + $r->setRawServerData($serverArray); + $r->setAbsoluteUrl($protocol . '://' . $hostName . $url); + return $r; + + } + +} diff --git a/htdocs/includes/sabre/sabre/http/lib/URLUtil.php b/htdocs/includes/sabre/sabre/http/lib/URLUtil.php new file mode 100644 index 00000000000..85c0e11504a --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/lib/URLUtil.php @@ -0,0 +1,103 @@ +<?php + +namespace Sabre\HTTP; + +use Sabre\URI; + +/** + * URL utility class + * + * Note: this class is deprecated. All its functionality moved to functions.php + * or sabre\uri. + * + * @deprecated + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class URLUtil { + + /** + * Encodes the path of a url. + * + * slashes (/) are treated as path-separators. + * + * @deprecated use \Sabre\HTTP\encodePath() + * @param string $path + * @return string + */ + static function encodePath($path) { + + return encodePath($path); + + } + + /** + * Encodes a 1 segment of a path + * + * Slashes are considered part of the name, and are encoded as %2f + * + * @deprecated use \Sabre\HTTP\encodePathSegment() + * @param string $pathSegment + * @return string + */ + static function encodePathSegment($pathSegment) { + + return encodePathSegment($pathSegment); + + } + + /** + * Decodes a url-encoded path + * + * @deprecated use \Sabre\HTTP\decodePath + * @param string $path + * @return string + */ + static function decodePath($path) { + + return decodePath($path); + + } + + /** + * Decodes a url-encoded path segment + * + * @deprecated use \Sabre\HTTP\decodePathSegment() + * @param string $path + * @return string + */ + static function decodePathSegment($path) { + + return decodePathSegment($path); + + } + + /** + * Returns the 'dirname' and 'basename' for a path. + * + * @deprecated Use Sabre\Uri\split(). + * @param string $path + * @return array + */ + static function splitPath($path) { + + return Uri\split($path); + + } + + /** + * Resolves relative urls, like a browser would. + * + * @deprecated Use Sabre\Uri\resolve(). + * @param string $basePath + * @param string $newPath + * @return string + */ + static function resolve($basePath, $newPath) { + + return Uri\resolve($basePath, $newPath); + + } + +} diff --git a/htdocs/includes/sabre/sabre/http/lib/Util.php b/htdocs/includes/sabre/sabre/http/lib/Util.php new file mode 100644 index 00000000000..e3f13a645b9 --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/lib/Util.php @@ -0,0 +1,74 @@ +<?php + +namespace Sabre\HTTP; + +/** + * HTTP utility methods + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @author Paul Voegler + * @deprecated All these functions moved to functions.php + * @license http://sabre.io/license/ Modified BSD License + */ +class Util { + + /** + * Content negotiation + * + * @deprecated Use \Sabre\HTTP\negotiateContentType + * @param string|null $acceptHeaderValue + * @param array $availableOptions + * @return string|null + */ + static function negotiateContentType($acceptHeaderValue, array $availableOptions) { + + return negotiateContentType($acceptHeaderValue, $availableOptions); + + } + + /** + * Deprecated! Use negotiateContentType. + * + * @deprecated Use \Sabre\HTTP\NegotiateContentType + * @param string|null $acceptHeaderValue + * @param array $availableOptions + * @return string|null + */ + static function negotiate($acceptHeaderValue, array $availableOptions) { + + return negotiateContentType($acceptHeaderValue, $availableOptions); + + } + + /** + * Parses a RFC2616-compatible date string + * + * This method returns false if the date is invalid + * + * @deprecated Use parseDate + * @param string $dateHeader + * @return bool|DateTime + */ + static function parseHTTPDate($dateHeader) { + + return parseDate($dateHeader); + + } + + /** + * Transforms a DateTime object to HTTP's most common date format. + * + * We're serializing it as the RFC 1123 date, which, for HTTP must be + * specified as GMT. + * + * @deprecated Use toDate + * @param \DateTime $dateTime + * @return string + */ + static function toHTTPDate(\DateTime $dateTime) { + + return toDate($dateTime); + + } +} diff --git a/htdocs/includes/sabre/sabre/http/lib/Version.php b/htdocs/includes/sabre/sabre/http/lib/Version.php new file mode 100644 index 00000000000..a5a42740512 --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/lib/Version.php @@ -0,0 +1,19 @@ +<?php + +namespace Sabre\HTTP; + +/** + * This class contains the version number for the HTTP package + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Version { + + /** + * Full version number + */ + const VERSION = '4.2.2'; + +} diff --git a/htdocs/includes/sabre/sabre/http/lib/functions.php b/htdocs/includes/sabre/sabre/http/lib/functions.php new file mode 100644 index 00000000000..d9411962399 --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/lib/functions.php @@ -0,0 +1,445 @@ +<?php + +namespace Sabre\HTTP; + +use DateTime; + +/** + * A collection of useful helpers for parsing or generating various HTTP + * headers. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ + +/** + * Parses a HTTP date-string. + * + * This method returns false if the date is invalid. + * + * The following formats are supported: + * Sun, 06 Nov 1994 08:49:37 GMT ; IMF-fixdate + * Sunday, 06-Nov-94 08:49:37 GMT ; obsolete RFC 850 format + * Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format + * + * See: + * http://tools.ietf.org/html/rfc7231#section-7.1.1.1 + * + * @param string $dateString + * @return bool|DateTime + */ +function parseDate($dateString) { + + // Only the format is checked, valid ranges are checked by strtotime below + $month = '(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)'; + $weekday = '(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)'; + $wkday = '(Mon|Tue|Wed|Thu|Fri|Sat|Sun)'; + $time = '([0-1]\d|2[0-3])(\:[0-5]\d){2}'; + $date3 = $month . ' ([12]\d|3[01]| [1-9])'; + $date2 = '(0[1-9]|[12]\d|3[01])\-' . $month . '\-\d{2}'; + // 4-digit year cannot begin with 0 - unix timestamp begins in 1970 + $date1 = '(0[1-9]|[12]\d|3[01]) ' . $month . ' [1-9]\d{3}'; + + // ANSI C's asctime() format + // 4-digit year cannot begin with 0 - unix timestamp begins in 1970 + $asctime_date = $wkday . ' ' . $date3 . ' ' . $time . ' [1-9]\d{3}'; + // RFC 850, obsoleted by RFC 1036 + $rfc850_date = $weekday . ', ' . $date2 . ' ' . $time . ' GMT'; + // RFC 822, updated by RFC 1123 + $rfc1123_date = $wkday . ', ' . $date1 . ' ' . $time . ' GMT'; + // allowed date formats by RFC 2616 + $HTTP_date = "($rfc1123_date|$rfc850_date|$asctime_date)"; + + // allow for space around the string and strip it + $dateString = trim($dateString, ' '); + if (!preg_match('/^' . $HTTP_date . '$/', $dateString)) + return false; + + // append implicit GMT timezone to ANSI C time format + if (strpos($dateString, ' GMT') === false) + $dateString .= ' GMT'; + + try { + return new DateTime($dateString, new \DateTimeZone('UTC')); + } catch (\Exception $e) { + return false; + } + +} + +/** + * Transforms a DateTime object to a valid HTTP/1.1 Date header value + * + * @param DateTime $dateTime + * @return string + */ +function toDate(DateTime $dateTime) { + + // We need to clone it, as we don't want to affect the existing + // DateTime. + $dateTime = clone $dateTime; + $dateTime->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 + * @param array $availableOptions + * @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 (is_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 + * @return array + */ +function parsePrefer($input) { + + $token = '[!#$%&\'*+\-.^_`~A-Za-z0-9]+'; + + // Work in progress + $word = '(?: [a-zA-Z0-9]+ | "[a-zA-Z0-9]*" )'; + + $regex = <<<REGEX +/ +^ +(?<name> $token) # Prefer property name +\s* # Optional space +(?: = \s* # Prefer property value + (?<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 + * @return string[] + */ +function getHeaderValues($values, $values2 = null) { + + $values = (array)$values; + if ($values2) { + $values = array_merge($values, (array)$values2); + } + 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 + * + * @param string $str + * @return array + */ +function parseMimeType($str) { + + $parameters = []; + // If no q= parameter appears, then quality = 1. + $quality = 1; + + $parts = explode(';', $str); + + // The first part is the mime-type. + $mimeType = array_shift($parts); + + $mimeType = explode('/', trim($mimeType)); + if (count($mimeType) !== 2) { + // Illegal value + return null; + } + 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-negotation. + if ($partName !== 'q') { + $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. + * + * @param string $path + * @return string + */ +function encodePath($path) { + + 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 + * + * @param string $pathSegment + * @return string + */ +function encodePathSegment($pathSegment) { + + return preg_replace_callback('/([^A-Za-z0-9_\-\.~\(\):@])/', function($match) { + + return '%' . sprintf('%02x', ord($match[0])); + + }, $pathSegment); +} + +/** + * Decodes a url-encoded path + * + * @param string $path + * @return string + */ +function decodePath($path) { + + return decodePathSegment($path); + +} + +/** + * Decodes a url-encoded path segment + * + * @param string $path + * @return string + */ +function decodePathSegment($path) { + + $path = rawurldecode($path); + $encoding = mb_detect_encoding($path, ['UTF-8', 'ISO-8859-1']); + + switch ($encoding) { + + case 'ISO-8859-1' : + $path = utf8_encode($path); + + } + + return $path; + +} diff --git a/htdocs/includes/sabre/sabre/http/tests/HTTP/Auth/AWSTest.php b/htdocs/includes/sabre/sabre/http/tests/HTTP/Auth/AWSTest.php new file mode 100644 index 00000000000..650761acae3 --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/tests/HTTP/Auth/AWSTest.php @@ -0,0 +1,235 @@ +<?php + +namespace Sabre\HTTP\Auth; + +use Sabre\HTTP\Request; +use Sabre\HTTP\Response; + +class AWSTest extends \PHPUnit_Framework_TestCase { + + /** + * @var Sabre\HTTP\Response + */ + private $response; + + /** + * @var Sabre\HTTP\Request + */ + private $request; + + /** + * @var Sabre\HTTP\Auth\AWS + */ + private $auth; + + const REALM = 'SabreDAV unittest'; + + function setUp() { + + $this->response = new Response(); + $this->request = new Request(); + $this->auth = new AWS(self::REALM, $this->request, $this->response); + + } + + 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); + + } + + 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); + + } + + 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); + + } + + 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); + + } + + 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); + + } + + 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); + + } + + 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()); + + } + + function test401() { + + $this->auth->requireLogin(); + $test = preg_match('/^AWS$/', $this->response->getHeader('WWW-Authenticate'), $matches); + $this->assertTrue($test == true, '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/htdocs/includes/sabre/sabre/http/tests/HTTP/Auth/BasicTest.php b/htdocs/includes/sabre/sabre/http/tests/HTTP/Auth/BasicTest.php new file mode 100644 index 00000000000..7c25e59de1b --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/tests/HTTP/Auth/BasicTest.php @@ -0,0 +1,69 @@ +<?php + +namespace Sabre\HTTP\Auth; + +use Sabre\HTTP\Request; +use Sabre\HTTP\Response; + +class BasicTest extends \PHPUnit_Framework_TestCase { + + function testGetCredentials() { + + $request = new Request('GET', '/', [ + 'Authorization' => 'Basic ' . base64_encode('user:pass:bla') + ]); + + $basic = new Basic('Dagger', $request, new Response()); + + $this->assertEquals([ + 'user', + 'pass:bla', + ], $basic->getCredentials()); + + } + + function testGetInvalidCredentialsColonMissing() { + + $request = new Request('GET', '/', [ + 'Authorization' => 'Basic ' . base64_encode('userpass') + ]); + + $basic = new Basic('Dagger', $request, new Response()); + + $this->assertNull($basic->getCredentials()); + + } + + function testGetCredentialsNoheader() { + + $request = new Request('GET', '/', []); + $basic = new Basic('Dagger', $request, new Response()); + + $this->assertNull($basic->getCredentials()); + + } + + function testGetCredentialsNotBasic() { + + $request = new Request('GET', '/', [ + 'Authorization' => 'QBasic ' . base64_encode('user:pass:bla') + ]); + $basic = new Basic('Dagger', $request, new Response()); + + $this->assertNull($basic->getCredentials()); + + } + + function testRequireLogin() { + + $response = new Response(); + $basic = new Basic('Dagger', new Request(), $response); + + $basic->requireLogin(); + + $this->assertEquals('Basic realm="Dagger"', $response->getHeader('WWW-Authenticate')); + $this->assertEquals(401, $response->getStatus()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/http/tests/HTTP/Auth/BearerTest.php b/htdocs/includes/sabre/sabre/http/tests/HTTP/Auth/BearerTest.php new file mode 100644 index 00000000000..ee2e9e0bd72 --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/tests/HTTP/Auth/BearerTest.php @@ -0,0 +1,57 @@ +<?php + +namespace Sabre\HTTP\Auth; + +use Sabre\HTTP\Request; +use Sabre\HTTP\Response; + +class BearerTest extends \PHPUnit_Framework_TestCase { + + function testGetToken() { + + $request = new Request('GET', '/', [ + 'Authorization' => 'Bearer 12345' + ]); + + $bearer = new Bearer('Dagger', $request, new Response()); + + $this->assertEquals( + '12345', + $bearer->getToken() + ); + + } + + function testGetCredentialsNoheader() { + + $request = new Request('GET', '/', []); + $bearer = new Bearer('Dagger', $request, new Response()); + + $this->assertNull($bearer->getToken()); + + } + + function testGetCredentialsNotBearer() { + + $request = new Request('GET', '/', [ + 'Authorization' => 'QBearer 12345' + ]); + $bearer = new Bearer('Dagger', $request, new Response()); + + $this->assertNull($bearer->getToken()); + + } + + function testRequireLogin() { + + $response = new Response(); + $bearer = new Bearer('Dagger', new Request(), $response); + + $bearer->requireLogin(); + + $this->assertEquals('Bearer realm="Dagger"', $response->getHeader('WWW-Authenticate')); + $this->assertEquals(401, $response->getStatus()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/http/tests/HTTP/Auth/DigestTest.php b/htdocs/includes/sabre/sabre/http/tests/HTTP/Auth/DigestTest.php new file mode 100644 index 00000000000..ffb69c76d6d --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/tests/HTTP/Auth/DigestTest.php @@ -0,0 +1,191 @@ +<?php + +namespace Sabre\HTTP\Auth; + +use Sabre\HTTP\Request; +use Sabre\HTTP\Response; + +class DigestTest extends \PHPUnit_Framework_TestCase { + + /** + * @var Sabre\HTTP\Response + */ + private $response; + + /** + * request + * + * @var Sabre\HTTP\Request + */ + private $request; + + /** + * @var Sabre\HTTP\Auth\Digest + */ + private $auth; + + const REALM = 'SabreDAV unittest'; + + function setUp() { + + $this->response = new Response(); + $this->request = new Request(); + $this->auth = new Digest(self::REALM, $this->request, $this->response); + + + } + + 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'); + + } + + 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'); + + } + + function testInvalidDigest2() { + + $this->request->setMethod('GET'); + $this->request->setHeader('Authorization', 'basic blablabla'); + + $this->auth->init(); + $this->assertFalse($this->auth->validateA1(md5('user:realm:password'))); + + } + + + 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'); + + } + + 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($test == true, '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/htdocs/includes/sabre/sabre/http/tests/HTTP/ClientTest.php b/htdocs/includes/sabre/sabre/http/tests/HTTP/ClientTest.php new file mode 100644 index 00000000000..ea25907df2d --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/tests/HTTP/ClientTest.php @@ -0,0 +1,474 @@ +<?php + +namespace Sabre\HTTP; + +class ClientTest extends \PHPUnit_Framework_TestCase { + + function testCreateCurlSettingsArrayGET() { + + $client = new ClientMock(); + $client->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_POSTFIELDS => '', + CURLOPT_PUT => false, + 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 (defined('HHVM_VERSION') === false) { + $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + } + + + $this->assertEquals($settings, $client->createCurlSettingsArray($request)); + + } + + 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_POSTFIELDS => '', + CURLOPT_PUT => false, + 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 (defined('HHVM_VERSION') === false) { + $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + } + + $this->assertEquals($settings, $client->createCurlSettingsArray($request)); + + } + + 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_POSTFIELDS => '', + CURLOPT_PUT => false, + 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 (defined('HHVM_VERSION') === false) { + $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + } + + $this->assertEquals($settings, $client->createCurlSettingsArray($request)); + + } + + function testCreateCurlSettingsArrayPUTStream() { + + $client = new ClientMock(); + + $h = fopen('php://memory', 'r+'); + fwrite($h, 'booh'); + $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_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 (defined('HHVM_VERSION') === false) { + $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + } + + $this->assertEquals($settings, $client->createCurlSettingsArray($request)); + + } + + 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 (defined('HHVM_VERSION') === false) { + $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + } + + $this->assertEquals($settings, $client->createCurlSettingsArray($request)); + + } + + 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()); + + } + + 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); + + } + + 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); + + } + + 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()); + + } + + 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()); + } + + } + + 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()); + + } + + 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']); + + } + + 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()); + + } + + 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. + * + * We are also going to persist all settings this method generates. While + * the underlying object doesn't behave exactly the same, it helps us + * simulate what curl does internally, and helps us identify problems with + * settings that are set by _some_ methods and not correctly reset by other + * methods after subsequent use. + * forces + */ + function createCurlSettingsArray(RequestInterface $request) { + + $settings = parent::createCurlSettingsArray($request); + $settings = $settings + $this->persistedSettings; + $this->persistedSettings = $settings; + return $settings; + + } + /** + * Making this method public. + */ + function parseCurlResult($response, $curlHandle) { + + return parent::parseCurlResult($response, $curlHandle); + + } + + /** + * This method is responsible for performing a single request. + * + * @param RequestInterface $request + * @return ResponseInterface + */ + function doRequest(RequestInterface $request) { + + $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 + * @return array + */ + protected function curlStuff($curlHandle) { + + $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 + * @return string + */ + protected function curlExec($curlHandle) { + + $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/htdocs/includes/sabre/sabre/http/tests/HTTP/FunctionsTest.php b/htdocs/includes/sabre/sabre/http/tests/HTTP/FunctionsTest.php new file mode 100644 index 00000000000..a107d1f007a --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/tests/HTTP/FunctionsTest.php @@ -0,0 +1,121 @@ +<?php + +namespace Sabre\HTTP; + +class FunctionsTest extends \PHPUnit_Framework_TestCase { + + /** + * @dataProvider getHeaderValuesData + */ + function testGetHeaderValues($input, $output) { + + $this->assertEquals( + $output, + getHeaderValues($input) + ); + + } + + 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 + */ + function testPrefer($input, $output) { + + $this->assertEquals( + $output, + parsePrefer($input) + ); + + } + + 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%'], + [], + ] + ]; + + } + +} diff --git a/htdocs/includes/sabre/sabre/http/tests/HTTP/MessageDecoratorTest.php b/htdocs/includes/sabre/sabre/http/tests/HTTP/MessageDecoratorTest.php new file mode 100644 index 00000000000..a4052c60c0c --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/tests/HTTP/MessageDecoratorTest.php @@ -0,0 +1,93 @@ +<?php + +namespace Sabre\HTTP; + +class MessageDecoratorTest extends \PHPUnit_Framework_TestCase { + + protected $inner; + protected $outer; + + function setUp() { + + $this->inner = new Request(); + $this->outer = new RequestDecorator($this->inner); + + } + + 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()); + + } + + 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()); + } + + 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')); + } + + function testHttpVersion() { + + $this->outer->setHttpVersion('1.0'); + + $this->assertEquals('1.0', $this->inner->getHttpVersion()); + $this->assertEquals('1.0', $this->outer->getHttpVersion()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/http/tests/HTTP/MessageTest.php b/htdocs/includes/sabre/sabre/http/tests/HTTP/MessageTest.php new file mode 100644 index 00000000000..cb5aadc416c --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/tests/HTTP/MessageTest.php @@ -0,0 +1,246 @@ +<?php + +namespace Sabre\HTTP; + +class MessageTest extends \PHPUnit_Framework_TestCase { + + function testConstruct() { + + $message = new MessageMock(); + $this->assertInstanceOf('Sabre\HTTP\Message', $message); + + } + + 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())); + + } + + 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()); + + } + + /** + * 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 respoding 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. + */ + 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. + */ + 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() + ); + + } + + + function testGetEmptyBodyStream() { + + $message = new MessageMock(); + $body = $message->getBodyAsStream(); + + $this->assertEquals('', stream_get_contents($body)); + + } + + function testGetEmptyBodyString() { + + $message = new MessageMock(); + $body = $message->getBodyAsString(); + + $this->assertEquals('', $body); + + } + + 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') + ); + + } + + 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()); + + } + + 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()); + + } + + 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)); + + } + + 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') + ); + + } + + function testHasHeaders() { + + $message = new MessageMock(); + + $this->assertFalse($message->hasHeader('X-Foo')); + $message->setHeader('X-Foo', 'Bar'); + $this->assertTrue($message->hasHeader('X-Foo')); + + } + +} + +class MessageMock extends Message { } diff --git a/htdocs/includes/sabre/sabre/http/tests/HTTP/RequestDecoratorTest.php b/htdocs/includes/sabre/sabre/http/tests/HTTP/RequestDecoratorTest.php new file mode 100644 index 00000000000..08af48749bd --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/tests/HTTP/RequestDecoratorTest.php @@ -0,0 +1,112 @@ +<?php + +namespace Sabre\HTTP; + +class RequestDecoratorTest extends \PHPUnit_Framework_TestCase { + + protected $inner; + protected $outer; + + function setUp() { + + $this->inner = new Request(); + $this->outer = new RequestDecorator($this->inner); + + } + + function testMethod() { + + $this->outer->setMethod('FOO'); + $this->assertEquals('FOO', $this->inner->getMethod()); + $this->assertEquals('FOO', $this->outer->getMethod()); + + } + + function testUrl() { + + $this->outer->setUrl('/foo'); + $this->assertEquals('/foo', $this->inner->getUrl()); + $this->assertEquals('/foo', $this->outer->getUrl()); + + } + + 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()); + + } + + function testBaseUrl() { + + $this->outer->setBaseUrl('/foo'); + $this->assertEquals('/foo', $this->inner->getBaseUrl()); + $this->assertEquals('/foo', $this->outer->getBaseUrl()); + + } + + function testPath() { + + $this->outer->setBaseUrl('/foo'); + $this->outer->setUrl('/foo/bar'); + $this->assertEquals('bar', $this->inner->getPath()); + $this->assertEquals('bar', $this->outer->getPath()); + + } + + 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()); + + } + + 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()); + + } + + + 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')); + } + + 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/htdocs/includes/sabre/sabre/http/tests/HTTP/RequestTest.php b/htdocs/includes/sabre/sabre/http/tests/HTTP/RequestTest.php new file mode 100644 index 00000000000..e3daab4d354 --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/tests/HTTP/RequestTest.php @@ -0,0 +1,167 @@ +<?php + +namespace Sabre\HTTP; + +class RequestTest extends \PHPUnit_Framework_TestCase { + + function testConstruct() { + + $request = new Request('GET', '/foo', [ + 'User-Agent' => 'Evert', + ]); + $this->assertEquals('GET', $request->getMethod()); + $this->assertEquals('/foo', $request->getUrl()); + $this->assertEquals([ + 'User-Agent' => ['Evert'], + ], $request->getHeaders()); + + } + + function testGetQueryParameters() { + + $request = new Request('GET', '/foo?a=b&c&d=e'); + $this->assertEquals([ + 'a' => 'b', + 'c' => null, + 'd' => 'e', + ], $request->getQueryParameters()); + + } + + function testGetQueryParametersNoData() { + + $request = new Request('GET', '/foo'); + $this->assertEquals([], $request->getQueryParameters()); + + } + + /** + * @backupGlobals + */ + function testCreateFromPHPRequest() { + + $_SERVER['REQUEST_METHOD'] = 'PUT'; + + $request = Sapi::getRequest(); + $this->assertEquals('PUT', $request->getMethod()); + + } + + function testGetAbsoluteUrl() { + + $s = [ + 'HTTP_HOST' => 'sabredav.org', + 'REQUEST_URI' => '/foo' + ]; + + $r = Sapi::createFromServerArray($s); + + $this->assertEquals('http://sabredav.org/foo', $r->getAbsoluteUrl()); + + $s = [ + 'HTTP_HOST' => 'sabredav.org', + 'REQUEST_URI' => '/foo', + 'HTTPS' => 'on', + ]; + + $r = Sapi::createFromServerArray($s); + + $this->assertEquals('https://sabredav.org/foo', $r->getAbsoluteUrl()); + + } + + function testGetPostData() { + + $post = [ + 'bla' => 'foo', + ]; + $r = new Request(); + $r->setPostData($post); + $this->assertEquals($post, $r->getPostData()); + + } + + function testGetPath() { + + $request = new Request(); + $request->setBaseUrl('/foo'); + $request->setUrl('/foo/bar/'); + + $this->assertEquals('bar', $request->getPath()); + + } + + function testGetPathStrippedQuery() { + + $request = new Request(); + $request->setBaseUrl('/foo'); + $request->setUrl('/foo/bar/?a=b'); + + $this->assertEquals('bar', $request->getPath()); + + } + + function testGetPathMissingSlash() { + + $request = new Request(); + $request->setBaseUrl('/foo/'); + $request->setUrl('/foo'); + + $this->assertEquals('', $request->getPath()); + + } + + /** + * @expectedException \LogicException + */ + function testGetPathOutsideBaseUrl() { + + $request = new Request(); + $request->setBaseUrl('/foo/'); + $request->setUrl('/bar/'); + + $request->getPath(); + + } + + function testToString() { + + $request = new Request('PUT', '/foo/bar', ['Content-Type' => 'text/xml']); + $request->setBody('foo'); + + $expected = <<<HI +PUT /foo/bar HTTP/1.1\r +Content-Type: text/xml\r +\r +foo +HI; + $this->assertEquals($expected, (string)$request); + + } + + function testToStringAuthorization() { + + $request = new Request('PUT', '/foo/bar', ['Content-Type' => 'text/xml', 'Authorization' => 'Basic foobar']); + $request->setBody('foo'); + + $expected = <<<HI +PUT /foo/bar HTTP/1.1\r +Content-Type: text/xml\r +Authorization: Basic REDACTED\r +\r +foo +HI; + $this->assertEquals($expected, (string)$request); + + } + + /** + * @expectedException \InvalidArgumentException + */ + function testConstructorWithArray() { + + $request = new Request([]); + + } + +} diff --git a/htdocs/includes/sabre/sabre/http/tests/HTTP/ResponseDecoratorTest.php b/htdocs/includes/sabre/sabre/http/tests/HTTP/ResponseDecoratorTest.php new file mode 100644 index 00000000000..838953b3144 --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/tests/HTTP/ResponseDecoratorTest.php @@ -0,0 +1,37 @@ +<?php + +namespace Sabre\HTTP; + +class ResponseDecoratorTest extends \PHPUnit_Framework_TestCase { + + protected $inner; + protected $outer; + + function setUp() { + + $this->inner = new Response(); + $this->outer = new ResponseDecorator($this->inner); + + } + + 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()); + + } + + 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/htdocs/includes/sabre/sabre/http/tests/HTTP/ResponseTest.php b/htdocs/includes/sabre/sabre/http/tests/HTTP/ResponseTest.php new file mode 100644 index 00000000000..117551bb969 --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/tests/HTTP/ResponseTest.php @@ -0,0 +1,48 @@ +<?php + +namespace Sabre\HTTP; + +class ResponseTest extends \PHPUnit_Framework_TestCase { + + function testConstruct() { + + $response = new Response(200, ['Content-Type' => 'text/xml']); + $this->assertEquals(200, $response->getStatus()); + $this->assertEquals('OK', $response->getStatusText()); + + } + + 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()); + + } + + /** + * @expectedException InvalidArgumentException + */ + function testInvalidStatus() { + + $response = new Response(1000); + + } + + function testToString() { + + $response = new Response(200, ['Content-Type' => 'text/xml']); + $response->setBody('foo'); + + $expected = <<<HI +HTTP/1.1 200 OK\r +Content-Type: text/xml\r +\r +foo +HI; + $this->assertEquals($expected, (string)$response); + + } + +} diff --git a/htdocs/includes/sabre/sabre/http/tests/HTTP/SapiTest.php b/htdocs/includes/sabre/sabre/http/tests/HTTP/SapiTest.php new file mode 100644 index 00000000000..158ce2171b9 --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/tests/HTTP/SapiTest.php @@ -0,0 +1,167 @@ +<?php + +namespace Sabre\HTTP; + +class SapiTest extends \PHPUnit_Framework_TestCase { + + function testConstructFromServerArray() { + + $request = Sapi::createFromServerArray([ + 'REQUEST_URI' => '/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')); + + } + + 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()); + + } + + 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()); + + } + + 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. + */ + 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 + */ + 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); + + } + + /** + * @runInSeparateProcess + * @depends testSend + */ + 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); + + } + +} diff --git a/htdocs/includes/sabre/sabre/http/tests/HTTP/URLUtilTest.php b/htdocs/includes/sabre/sabre/http/tests/HTTP/URLUtilTest.php new file mode 100644 index 00000000000..a2d65a5e36d --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/tests/HTTP/URLUtilTest.php @@ -0,0 +1,187 @@ +<?php + +namespace Sabre\HTTP; + +class URLUtilTest extends \PHPUnit_Framework_TestCase{ + + function testEncodePath() { + + $str = ''; + for ($i = 0;$i < 128;$i++) $str .= chr($i); + + $newStr = URLUtil::encodePath($str); + + $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-./' . + '0123456789:%3b%3c%3d%3e%3f' . + '@ABCDEFGHIJKLMNO' . + 'PQRSTUVWXYZ%5b%5c%5d%5e_' . + '%60abcdefghijklmno' . + 'pqrstuvwxyz%7b%7c%7d~%7f', + $newStr); + + $this->assertEquals($str, URLUtil::decodePath($newStr)); + + } + + function testEncodePathSegment() { + + $str = ''; + for ($i = 0;$i < 128;$i++) $str .= chr($i); + + $newStr = URLUtil::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, URLUtil::decodePathSegment($newStr)); + + } + + function testDecode() { + + $str = 'Hello%20Test+Test2.txt'; + $newStr = URLUtil::decodePath($str); + $this->assertEquals('Hello Test+Test2.txt', $newStr); + + } + + /** + * @depends testDecode + */ + function testDecodeUmlaut() { + + $str = 'Hello%C3%BC.txt'; + $newStr = URLUtil::decodePath($str); + $this->assertEquals("Hello\xC3\xBC.txt", $newStr); + + } + + /** + * @depends testDecodeUmlaut + */ + function testDecodeUmlautLatin1() { + + $str = 'Hello%FC.txt'; + $newStr = URLUtil::decodePath($str); + $this->assertEquals("Hello\xC3\xBC.txt", $newStr); + + } + + /** + * This testcase was sent by a bug reporter + * + * @depends testDecode + */ + function testDecodeAccentsWindows7() { + + $str = '/webdav/%C3%A0fo%C3%B3'; + $newStr = URLUtil::decodePath($str); + $this->assertEquals(strtolower($str), URLUtil::encodePath($newStr)); + + } + + function testSplitPath() { + + $strings = [ + + // input // expected result + '/foo/bar' => ['/foo','bar'], + '/foo/bar/' => ['/foo','bar'], + 'foo/bar/' => ['foo','bar'], + 'foo/bar' => ['foo','bar'], + 'foo/bar/baz' => ['foo/bar','baz'], + 'foo/bar/baz/' => ['foo/bar','baz'], + 'foo' => ['','foo'], + 'foo/' => ['','foo'], + '/foo/' => ['','foo'], + '/foo' => ['','foo'], + '' => [null,null], + + // UTF-8 + "/\xC3\xA0fo\xC3\xB3/bar" => ["/\xC3\xA0fo\xC3\xB3",'bar'], + "/\xC3\xA0foo/b\xC3\xBCr/" => ["/\xC3\xA0foo","b\xC3\xBCr"], + "foo/\xC3\xA0\xC3\xBCr" => ["foo","\xC3\xA0\xC3\xBCr"], + + ]; + + foreach ($strings as $input => $expected) { + + $output = URLUtil::splitPath($input); + $this->assertEquals($expected, $output, 'The expected output for \'' . $input . '\' was incorrect'); + + + } + + } + + /** + * @dataProvider resolveData + */ + function testResolve($base, $update, $expected) { + + $this->assertEquals( + $expected, + URLUtil::resolve($base, $update) + ); + + } + + function resolveData() { + + return [ + [ + 'http://example.org/foo/baz', + '/bar', + 'http://example.org/bar', + ], + [ + 'https://example.org/foo', + '//example.net/', + 'https://example.net/', + ], + [ + 'https://example.org/foo', + '?a=b', + 'https://example.org/foo?a=b', + ], + [ + '//example.org/foo', + '?a=b', + '//example.org/foo?a=b', + ], + // Ports and fragments + [ + 'https://example.org:81/foo#hey', + '?a=b#c=d', + 'https://example.org:81/foo?a=b#c=d', + ], + // Relative.. in-directory paths + [ + 'http://example.org/foo/bar', + 'bar2', + 'http://example.org/foo/bar2', + ], + // Now the base path ended with a slash + [ + 'http://example.org/foo/bar/', + 'bar2/bar3', + 'http://example.org/foo/bar/bar2/bar3', + ], + ]; + + } + +} diff --git a/htdocs/includes/sabre/sabre/http/tests/HTTP/UtilTest.php b/htdocs/includes/sabre/sabre/http/tests/HTTP/UtilTest.php new file mode 100644 index 00000000000..5659bdd2e82 --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/tests/HTTP/UtilTest.php @@ -0,0 +1,206 @@ +<?php + +namespace Sabre\HTTP; + +class UtilTest extends \PHPUnit_Framework_TestCase { + + 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 = Util::parseHTTPDate($time); + $this->assertEquals($expected, $result->format('U')); + } + + $result = Util::parseHTTPDate('Wed Oct 6 10:26:00 2010'); + $this->assertEquals(1286360760, $result->format('U')); + + } + + 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(Util::parseHTTPDate($time), 'We used the string: ' . $time); + } + + } + + function testTimezones() { + + $default = date_default_timezone_get(); + date_default_timezone_set('Europe/Amsterdam'); + + $this->testParseHTTPDate(); + + date_default_timezone_set($default); + + } + + function testToHTTPDate() { + + $dt = new \DateTime('2011-12-10 12:00:00 +0200'); + + $this->assertEquals( + 'Sat, 10 Dec 2011 10:00:00 GMT', + Util::toHTTPDate($dt) + ); + + } + + /** + * @dataProvider negotiateData + */ + function testNegotiate($acceptHeader, $available, $expected) { + + $this->assertEquals( + $expected, + Util::negotiate($acceptHeader, $available) + ); + + } + + 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 whitepace 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/htdocs/includes/sabre/sabre/http/tests/bootstrap.php b/htdocs/includes/sabre/sabre/http/tests/bootstrap.php new file mode 100644 index 00000000000..74931b6f118 --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/tests/bootstrap.php @@ -0,0 +1,8 @@ +<?php + +date_default_timezone_set('UTC'); + +ini_set('error_reporting', E_ALL | E_STRICT | E_DEPRECATED); + +// Composer autoloader +include __DIR__ . '/../vendor/autoload.php'; diff --git a/htdocs/includes/sabre/sabre/http/tests/phpcs/ruleset.xml b/htdocs/includes/sabre/sabre/http/tests/phpcs/ruleset.xml new file mode 100644 index 00000000000..ec2c4c84b1d --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/tests/phpcs/ruleset.xml @@ -0,0 +1,57 @@ +<?xml version="1.0"?> +<ruleset name="sabre.php"> + <description>sabre.io codesniffer ruleset</description> + + <!-- Include the whole PSR-1 standard --> + <rule ref="PSR1" /> + + <!-- All PHP files MUST use the Unix LF (linefeed) line ending. --> + <rule ref="Generic.Files.LineEndings"> + <properties> + <property name="eolChar" value="\n"/> + </properties> + </rule> + + <!-- The closing ?> tag MUST be omitted from files containing only PHP. --> + <rule ref="Zend.Files.ClosingTag"/> + + <!-- There MUST NOT be trailing whitespace at the end of non-blank lines. --> + <rule ref="Squiz.WhiteSpace.SuperfluousWhitespace"> + <properties> + <property name="ignoreBlankLines" value="true"/> + </properties> + </rule> + + <!-- There MUST NOT be more than one statement per line. --> + <rule ref="Generic.Formatting.DisallowMultipleStatements"/> + + <rule ref="Generic.WhiteSpace.ScopeIndent"> + <properties> + <property name="ignoreIndentationTokens" type="array" value="T_COMMENT,T_DOC_COMMENT"/> + </properties> + </rule> + <rule ref="Generic.WhiteSpace.DisallowTabIndent"/> + + <!-- PHP keywords MUST be in lower case. --> + <rule ref="Generic.PHP.LowerCaseKeyword"/> + + <!-- The PHP constants true, false, and null MUST be in lower case. --> + <rule ref="Generic.PHP.LowerCaseConstant"/> + + <!-- <rule ref="Squiz.Scope.MethodScope"/> --> + <rule ref="Squiz.WhiteSpace.ScopeKeywordSpacing"/> + + <!-- In the argument list, there MUST NOT be a space before each comma, and there MUST be one space after each comma. --> + <!-- + <rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing"> + <properties> + <property name="equalsSpacing" value="1"/> + </properties> + </rule> + <rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing.SpacingAfterHint"> + <severity>0</severity> + </rule> + --> + <rule ref="PEAR.WhiteSpace.ScopeClosingBrace"/> + +</ruleset> diff --git a/htdocs/includes/sabre/sabre/http/tests/phpunit.xml b/htdocs/includes/sabre/sabre/http/tests/phpunit.xml new file mode 100644 index 00000000000..32d701a37f7 --- /dev/null +++ b/htdocs/includes/sabre/sabre/http/tests/phpunit.xml @@ -0,0 +1,18 @@ +<phpunit + colors="true" + bootstrap="bootstrap.php" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" + strict="true" + > + <testsuite name="Sabre_HTTP"> + <directory>HTTP/</directory> + </testsuite> + + <filter> + <whitelist addUncoveredFilesFromWhitelist="true"> + <directory suffix=".php">../lib/</directory> + </whitelist> + </filter> +</phpunit> diff --git a/htdocs/includes/sabre/sabre/uri/.gitignore b/htdocs/includes/sabre/sabre/uri/.gitignore new file mode 100644 index 00000000000..19d1affd4a7 --- /dev/null +++ b/htdocs/includes/sabre/sabre/uri/.gitignore @@ -0,0 +1,13 @@ +# Composer +vendor/ +composer.lock + +# Tests +tests/cov/ + +# Composer binaries +bin/phpunit +bin/phpcs + +# Vim +.*.swp diff --git a/htdocs/includes/sabre/sabre/uri/.travis.yml b/htdocs/includes/sabre/sabre/uri/.travis.yml new file mode 100644 index 00000000000..75c8270df21 --- /dev/null +++ b/htdocs/includes/sabre/sabre/uri/.travis.yml @@ -0,0 +1,14 @@ +language: php +php: + - 5.4 + - 5.5 + - 5.6 + - 7 + - 7.1 + +script: + - ./bin/phpunit --configuration tests/phpunit.xml.dist + - ./bin/sabre-cs-fixer fix lib/ --dry-run --diff + +before_script: composer install --dev + diff --git a/htdocs/includes/sabre/sabre/uri/CHANGELOG.md b/htdocs/includes/sabre/sabre/uri/CHANGELOG.md new file mode 100644 index 00000000000..a30e45139ae --- /dev/null +++ b/htdocs/includes/sabre/sabre/uri/CHANGELOG.md @@ -0,0 +1,51 @@ +ChangeLog +========= + +1.2.0 (2016-12-06) +------------------ + +* Now throwing `InvalidUriException` if a uri passed to the `parse` function + is invalid or could not be parsed. +* #11: Fix support for URIs that start with a triple slash. PHP's `parse_uri()` + doesn't support them, so we now have a pure-php fallback in case it fails. +* #9: Fix support for relative URI's that have a non-uri encoded colon `:` in + them. + + +1.1.1 (2016-10-27) +------------------ + +* #10: Correctly support file:// URIs in the build() method. (@yuloh) + + +1.1.0 (2016-03-07) +------------------ + +* #6: PHP's `parse_url()` corrupts strings if they contain certain + non ascii-characters such as Chinese or Hebrew. sabre/uri's `parse()` + function now percent-encodes these characters beforehand. + + +1.0.1 (2015-04-28) +------------------ + +* #4: Using php-cs-fixer to automatically enforce conding standards. +* #5: Resolving to and building `mailto:` urls were not correctly handled. + + +1.0.0 (2015-01-27) +------------------ + +* Added a `normalize` function. +* Added a `buildUri` function. +* Fixed a bug in the `resolve` when only a new fragment is specified. + +San José, CalConnect XXXII release! + +0.0.1 (2014-11-17) +------------------ + +* First version! +* Source was lifted from sabre/http package. +* Provides a `resolve` and a `split` function. +* Requires PHP 5.4.8 and up. diff --git a/htdocs/includes/sabre/sabre/uri/LICENSE b/htdocs/includes/sabre/sabre/uri/LICENSE new file mode 100644 index 00000000000..9a3a9194600 --- /dev/null +++ b/htdocs/includes/sabre/sabre/uri/LICENSE @@ -0,0 +1,27 @@ +Copyright (C) 2014-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/htdocs/includes/sabre/sabre/uri/README.md b/htdocs/includes/sabre/sabre/uri/README.md new file mode 100644 index 00000000000..76f55d8e454 --- /dev/null +++ b/htdocs/includes/sabre/sabre/uri/README.md @@ -0,0 +1,55 @@ +sabre/uri +========= + +sabre/uri is a lightweight library that provides several functions for working +with URIs, staying true to the rules of [RFC3986][2]. + +Partially inspired by [Node.js URL library][3], and created to solve real +problems in PHP applications. 100% unitested and many tests are based on +examples from RFC3986. + +The library provides the following functions: + +1. `resolve` to resolve relative urls. +2. `normalize` to aid in comparing urls. +3. `parse`, which works like PHP's [parse_url][6]. +4. `build` to do the exact opposite of `parse`. +5. `split` to easily get the 'dirname' and 'basename' of a URL without all the + problems those two functions have. + + +Further reading +--------------- + +* [Installation][7] +* [Usage][8] + + +Build status +------------ + +| branch | status | +| ------ | ------ | +| master | [![Build Status](https://travis-ci.org/fruux/sabre-uri.svg?branch=master)](https://travis-ci.org/fruux/sabre-uri) | + + +Questions? +---------- + +Head over to the [sabre/dav mailinglist][4], or you can also just open a ticket +on [GitHub][5]. + + +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://sabre.io/uri/ +[2]: https://tools.ietf.org/html/rfc3986/ +[3]: http://nodejs.org/api/url.html +[4]: http://groups.google.com/group/sabredav-discuss +[5]: https://github.com/fruux/sabre-uri/issues/ +[6]: http://php.net/manual/en/function.parse-url.php +[7]: http://sabre.io/uri/install/ +[8]: http://sabre.io/uri/usage/ diff --git a/htdocs/includes/sabre/sabre/uri/composer.json b/htdocs/includes/sabre/sabre/uri/composer.json new file mode 100644 index 00000000000..efa5a693004 --- /dev/null +++ b/htdocs/includes/sabre/sabre/uri/composer.json @@ -0,0 +1,41 @@ +{ + "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": ">=5.4.7" + }, + "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/" + } + }, + "require-dev": { + "sabre/cs": "~1.0.0", + "phpunit/phpunit" : "*" + }, + "config" : { + "bin-dir" : "bin/" + } +} diff --git a/htdocs/includes/sabre/sabre/uri/lib/InvalidUriException.php b/htdocs/includes/sabre/sabre/uri/lib/InvalidUriException.php new file mode 100644 index 00000000000..0385fd4628d --- /dev/null +++ b/htdocs/includes/sabre/sabre/uri/lib/InvalidUriException.php @@ -0,0 +1,17 @@ +<?php + +namespace Sabre\Uri; + +/** + * Invalid Uri + * + * This is thrown when an attempt was made to use Sabre\Uri parse a uri that + * it could not. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (https://evertpot.com/) + * @license http://sabre.io/license/ + */ +class InvalidUriException extends \Exception { + +} diff --git a/htdocs/includes/sabre/sabre/uri/lib/Version.php b/htdocs/includes/sabre/sabre/uri/lib/Version.php new file mode 100644 index 00000000000..4633a209861 --- /dev/null +++ b/htdocs/includes/sabre/sabre/uri/lib/Version.php @@ -0,0 +1,19 @@ +<?php + +namespace Sabre\Uri; + +/** + * This class contains the version number for this package. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ + */ +class Version { + + /** + * Full version number + */ + const VERSION = '1.2.0'; + +} diff --git a/htdocs/includes/sabre/sabre/uri/lib/functions.php b/htdocs/includes/sabre/sabre/uri/lib/functions.php new file mode 100644 index 00000000000..df74b98b324 --- /dev/null +++ b/htdocs/includes/sabre/sabre/uri/lib/functions.php @@ -0,0 +1,373 @@ +<?php + +namespace Sabre\Uri; + +/** + * This file contains all the uri handling functions. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ + */ + +/** + * Resolves relative urls, like a browser would. + * + * This function takes a basePath, which itself _may_ also be relative, and + * then applies the relative path on top of it. + * + * @param string $basePath + * @param string $newPath + * @return string + */ +function resolve($basePath, $newPath) { + + $base = parse($basePath); + $delta = parse($newPath); + + $pick = function($part) use ($base, $delta) { + + if ($delta[$part]) { + return $delta[$part]; + } elseif ($base[$part]) { + return $base[$part]; + } + return null; + + }; + + // If the new path defines a scheme, it's absolute and we can just return + // that. + if ($delta['scheme']) { + return build($delta); + } + + $newParts = []; + + $newParts['scheme'] = $pick('scheme'); + $newParts['host'] = $pick('host'); + $newParts['port'] = $pick('port'); + + $path = ''; + if ($delta['path']) { + // If the path starts with a slash + if ($delta['path'][0] === '/') { + $path = $delta['path']; + } else { + // Removing last component from base path. + $path = $base['path']; + if (strpos($path, '/') !== false) { + $path = substr($path, 0, strrpos($path, '/')); + } + $path .= '/' . $delta['path']; + } + } else { + $path = $base['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'] = $path; + if ($delta['query']) { + $newParts['query'] = $delta['query']; + } elseif (!empty($base['query']) && empty($delta['host']) && empty($delta['path'])) { + // Keep the old query if host and path didn't change + $newParts['query'] = $base['query']; + } + if ($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. + * + * @param string $uri + * @return string + */ +function normalize($uri) { + + $parts = parse($uri); + + if (!empty($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 ($parts['scheme']) { + $parts['scheme'] = strtolower($parts['scheme']); + $defaultPorts = [ + 'http' => '80', + 'https' => '443', + ]; + + if (!empty($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 (empty($parts['path'])) { + // An empty path is equivalent to / in http. + $parts['path'] = '/'; + } + break; + } + } + + if ($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. + * + * @param string $uri + * @return array + */ +function parse($uri) { + + // 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 + ); + + $result = parse_url($uri); + if (!$result) { + $result = _parse_fallback($uri); + } + + 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 + * @return string + */ +function build(array $parts) { + + $uri = ''; + + $authority = ''; + if (!empty($parts['host'])) { + $authority = $parts['host']; + if (!empty($parts['user'])) { + $authority = $parts['user'] . '@' . $authority; + } + if (!empty($parts['port'])) { + $authority = $authority . ':' . $parts['port']; + } + } + + if (!empty($parts['scheme'])) { + // If there's a scheme, there's also a host. + $uri = $parts['scheme'] . ':'; + + } + if ($authority || (!empty($parts['scheme']) && $parts['scheme'] === 'file')) { + // No scheme, but there is a host. + $uri .= '//' . $authority; + + } + + if (!empty($parts['path'])) { + $uri .= $parts['path']; + } + if (!empty($parts['query'])) { + $uri .= '?' . $parts['query']; + } + if (!empty($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. + * + * @param string $path + * @return array + */ +function split($path) { + + $matches = []; + if (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. + * + * @param string $uri + * @return array + */ +function _parse_fallback($uri) { + + // 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 + ); + + $result = [ + 'scheme' => null, + 'host' => null, + 'port' => null, + 'user' => null, + 'path' => null, + 'fragment' => null, + 'query' => null, + ]; + + if (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); + + } + + // Taking off a fragment part + if (strpos($uri, '#')) { + list($uri, $result['fragment']) = explode('#', $uri, 2); + } + // Taking off the query part + if (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. + $result['path'] = substr($uri, 2); + $result['host'] = ''; + } elseif (substr($uri, 0, 2) === '//') { + // Uris that have an authority part. + $regex = ' + %^ + // + (?: (?<user> [^:@]+) (: (?<pass> [^@]+)) @)? + (?<host> ( [^:/]* | \[ [^\]]+ \] )) + (?: : (?<port> [0-9]+))? + (?<path> / .*)? + $%x + '; + if (!preg_match($regex, $uri, $matches)) { + throw new InvalidUriException('Invalid, or could not parse URI'); + } + if ($matches['host']) $result['host'] = $matches['host']; + if ($matches['port']) $result['port'] = (int)$matches['port']; + if (isset($matches['path'])) $result['path'] = $matches['path']; + if ($matches['user']) $result['user'] = $matches['user']; + if ($matches['pass']) $result['pass'] = $matches['pass']; + } else { + $result['path'] = $uri; + } + + return $result; +} diff --git a/htdocs/includes/sabre/sabre/uri/tests/BuildTest.php b/htdocs/includes/sabre/sabre/uri/tests/BuildTest.php new file mode 100644 index 00000000000..ae4b4ba2719 --- /dev/null +++ b/htdocs/includes/sabre/sabre/uri/tests/BuildTest.php @@ -0,0 +1,41 @@ +<?php + +namespace Sabre\Uri; + +class BuildTest extends \PHPUnit_Framework_TestCase{ + + /** + * @dataProvider buildUriData + */ + function testBuild($value) { + + $this->assertEquals( + $value, + build(parse_url($value)) + ); + + } + + function buildUriData() { + + return [ + ['http://example.org/'], + ['http://example.org/foo/bar'], + ['//example.org/foo/bar'], + ['/foo/bar'], + ['http://example.org:81/'], + ['http://user@example.org:81/'], + ['http://example.org:81/hi?a=b'], + ['http://example.org:81/hi?a=b#c=d'], + // [ '//example.org:81/hi?a=b#c=d'], // Currently fails due to a + // PHP bug. + ['/hi?a=b#c=d'], + ['?a=b#c=d'], + ['#c=d'], + ['file:///etc/hosts'], + ['file://localhost/etc/hosts'], + ]; + + } + +} diff --git a/htdocs/includes/sabre/sabre/uri/tests/NormalizeTest.php b/htdocs/includes/sabre/sabre/uri/tests/NormalizeTest.php new file mode 100644 index 00000000000..4dbe943205e --- /dev/null +++ b/htdocs/includes/sabre/sabre/uri/tests/NormalizeTest.php @@ -0,0 +1,42 @@ +<?php + +namespace Sabre\Uri; + +class NormalizeTest extends \PHPUnit_Framework_TestCase{ + + /** + * @dataProvider normalizeData + */ + function testNormalize($in, $out) { + + $this->assertEquals( + $out, + normalize($in) + ); + + } + + function normalizeData() { + + return [ + ['http://example.org/', 'http://example.org/'], + ['HTTP://www.EXAMPLE.com/', 'http://www.example.com/'], + ['http://example.org/%7Eevert', 'http://example.org/~evert'], + ['http://example.org/./evert', 'http://example.org/evert'], + ['http://example.org/../evert', 'http://example.org/evert'], + ['http://example.org/foo/../evert', 'http://example.org/evert'], + ['/%41', '/A'], + ['/%3F', '/%3F'], + ['/%3f', '/%3F'], + ['http://example.org', 'http://example.org/'], + ['http://example.org:/', 'http://example.org/'], + ['http://example.org:80/', 'http://example.org/'], + // See issue #6. parse_url corrupts strings like this, but only on + // macs. + //[ 'http://example.org/有词法别名.zh','http://example.org/%E6%9C%89%E8%AF%8D%E6%B3%95%E5%88%AB%E5%90%8D.zh'], + + ]; + + } + +} diff --git a/htdocs/includes/sabre/sabre/uri/tests/ParseTest.php b/htdocs/includes/sabre/sabre/uri/tests/ParseTest.php new file mode 100644 index 00000000000..ab0ead2cc96 --- /dev/null +++ b/htdocs/includes/sabre/sabre/uri/tests/ParseTest.php @@ -0,0 +1,179 @@ +<?php + +namespace Sabre\Uri; + +class ParseTest extends \PHPUnit_Framework_TestCase{ + + /** + * @dataProvider parseData + */ + function testParse($in, $out) { + + $this->assertEquals( + $out, + parse($in) + ); + + } + + /** + * @dataProvider parseData + */ + function testParseFallback($in, $out) { + + $result = _parse_fallback($in); + $result = $result + [ + 'scheme' => null, + 'host' => null, + 'path' => null, + 'port' => null, + 'user' => null, + 'query' => null, + 'fragment' => null, + ]; + + $this->assertEquals( + $out, + $result + ); + + } + + function parseData() { + + return [ + [ + 'http://example.org/hello?foo=bar#test', + [ + 'scheme' => 'http', + 'host' => 'example.org', + 'path' => '/hello', + 'port' => null, + 'user' => null, + 'query' => 'foo=bar', + 'fragment' => 'test' + ] + ], + // See issue #6. parse_url corrupts strings like this, but only on + // macs. + [ + 'http://example.org/有词法别名.zh', + [ + 'scheme' => 'http', + 'host' => 'example.org', + 'path' => '/%E6%9C%89%E8%AF%8D%E6%B3%95%E5%88%AB%E5%90%8D.zh', + 'port' => null, + 'user' => null, + 'query' => null, + 'fragment' => null + ] + ], + [ + 'ftp://user:password@ftp.example.org/', + [ + 'scheme' => 'ftp', + 'host' => 'ftp.example.org', + 'path' => '/', + 'port' => null, + 'user' => 'user', + 'pass' => 'password', + 'query' => null, + 'fragment' => null, + ] + ], + // See issue #9, parse_url doesn't like colons followed by numbers even + // though they are allowed since RFC 3986 + [ + 'http://example.org/hello:12?foo=bar#test', + [ + 'scheme' => 'http', + 'host' => 'example.org', + 'path' => '/hello:12', + 'port' => null, + 'user' => null, + 'query' => 'foo=bar', + 'fragment' => 'test' + ] + ], + [ + '/path/to/colon:34', + [ + 'scheme' => null, + 'host' => null, + 'path' => '/path/to/colon:34', + 'port' => null, + 'user' => null, + 'query' => null, + 'fragment' => null, + ] + ], + // File scheme + [ + 'file:///foo/bar', + [ + 'scheme' => 'file', + 'host' => '', + 'path' => '/foo/bar', + 'port' => null, + 'user' => null, + 'query' => null, + 'fragment' => null, + ] + ], + // Weird scheme with triple-slash. See Issue #11. + [ + 'vfs:///somefile', + [ + 'scheme' => 'vfs', + 'host' => '', + 'path' => '/somefile', + 'port' => null, + 'user' => null, + 'query' => null, + 'fragment' => null, + ] + ], + // Examples from RFC3986 + [ + 'ldap://[2001:db8::7]/c=GB?objectClass?one', + [ + 'scheme' => 'ldap', + 'host' => '[2001:db8::7]', + 'path' => '/c=GB', + 'port' => null, + 'user' => null, + 'query' => 'objectClass?one', + 'fragment' => null, + ] + ], + [ + 'news:comp.infosystems.www.servers.unix', + [ + 'scheme' => 'news', + 'host' => null, + 'path' => 'comp.infosystems.www.servers.unix', + 'port' => null, + 'user' => null, + 'query' => null, + 'fragment' => null, + ] + ], + // Port + [ + 'http://example.org:8080/', + [ + 'scheme' => 'http', + 'host' => 'example.org', + 'path' => '/', + 'port' => 8080, + 'user' => null, + 'query' => null, + 'fragment' => null, + ] + ], + + ]; + + } + +} diff --git a/htdocs/includes/sabre/sabre/uri/tests/ResolveTest.php b/htdocs/includes/sabre/sabre/uri/tests/ResolveTest.php new file mode 100644 index 00000000000..73e4dea69d8 --- /dev/null +++ b/htdocs/includes/sabre/sabre/uri/tests/ResolveTest.php @@ -0,0 +1,83 @@ +<?php + +namespace Sabre\Uri; + +class ResolveTest extends \PHPUnit_Framework_TestCase{ + + /** + * @dataProvider resolveData + */ + function testResolve($base, $update, $expected) { + + $this->assertEquals( + $expected, + resolve($base, $update) + ); + + } + + function resolveData() { + + return [ + [ + 'http://example.org/foo/baz', + '/bar', + 'http://example.org/bar', + ], + [ + 'https://example.org/foo', + '//example.net/', + 'https://example.net/', + ], + [ + 'https://example.org/foo', + '?a=b', + 'https://example.org/foo?a=b', + ], + [ + '//example.org/foo', + '?a=b', + '//example.org/foo?a=b', + ], + // Ports and fragments + [ + 'https://example.org:81/foo#hey', + '?a=b#c=d', + 'https://example.org:81/foo?a=b#c=d', + ], + // Relative.. in-directory paths + [ + 'http://example.org/foo/bar', + 'bar2', + 'http://example.org/foo/bar2', + ], + // Now the base path ended with a slash + [ + 'http://example.org/foo/bar/', + 'bar2/bar3', + 'http://example.org/foo/bar/bar2/bar3', + ], + // .. and . + [ + 'http://example.org/foo/bar/', + '../bar2/.././/bar3/', + 'http://example.org/foo//bar3/', + ], + // Only updating the fragment + [ + 'https://example.org/foo?a=b', + '#comments', + 'https://example.org/foo?a=b#comments', + ], + // Switching to mailto! + [ + 'https://example.org/foo?a=b', + 'mailto:foo@example.org', + 'mailto:foo@example.org', + ], + + ]; + + } + +} diff --git a/htdocs/includes/sabre/sabre/uri/tests/SplitTest.php b/htdocs/includes/sabre/sabre/uri/tests/SplitTest.php new file mode 100644 index 00000000000..2d73c9b255d --- /dev/null +++ b/htdocs/includes/sabre/sabre/uri/tests/SplitTest.php @@ -0,0 +1,41 @@ +<?php + +namespace Sabre\Uri; + +class SplitTest extends \PHPUnit_Framework_TestCase{ + + function testSplit() { + + $strings = [ + + // input // expected result + '/foo/bar' => ['/foo','bar'], + '/foo/bar/' => ['/foo','bar'], + 'foo/bar/' => ['foo','bar'], + 'foo/bar' => ['foo','bar'], + 'foo/bar/baz' => ['foo/bar','baz'], + 'foo/bar/baz/' => ['foo/bar','baz'], + 'foo' => ['','foo'], + 'foo/' => ['','foo'], + '/foo/' => ['','foo'], + '/foo' => ['','foo'], + '' => [null,null], + + // UTF-8 + "/\xC3\xA0fo\xC3\xB3/bar" => ["/\xC3\xA0fo\xC3\xB3",'bar'], + "/\xC3\xA0foo/b\xC3\xBCr/" => ["/\xC3\xA0foo","b\xC3\xBCr"], + "foo/\xC3\xA0\xC3\xBCr" => ["foo","\xC3\xA0\xC3\xBCr"], + + ]; + + foreach ($strings as $input => $expected) { + + $output = split($input); + $this->assertEquals($expected, $output, 'The expected output for \'' . $input . '\' was incorrect'); + + + } + + } + +} diff --git a/htdocs/includes/sabre/sabre/uri/tests/phpcs/ruleset.xml b/htdocs/includes/sabre/sabre/uri/tests/phpcs/ruleset.xml new file mode 100644 index 00000000000..ec2c4c84b1d --- /dev/null +++ b/htdocs/includes/sabre/sabre/uri/tests/phpcs/ruleset.xml @@ -0,0 +1,57 @@ +<?xml version="1.0"?> +<ruleset name="sabre.php"> + <description>sabre.io codesniffer ruleset</description> + + <!-- Include the whole PSR-1 standard --> + <rule ref="PSR1" /> + + <!-- All PHP files MUST use the Unix LF (linefeed) line ending. --> + <rule ref="Generic.Files.LineEndings"> + <properties> + <property name="eolChar" value="\n"/> + </properties> + </rule> + + <!-- The closing ?> tag MUST be omitted from files containing only PHP. --> + <rule ref="Zend.Files.ClosingTag"/> + + <!-- There MUST NOT be trailing whitespace at the end of non-blank lines. --> + <rule ref="Squiz.WhiteSpace.SuperfluousWhitespace"> + <properties> + <property name="ignoreBlankLines" value="true"/> + </properties> + </rule> + + <!-- There MUST NOT be more than one statement per line. --> + <rule ref="Generic.Formatting.DisallowMultipleStatements"/> + + <rule ref="Generic.WhiteSpace.ScopeIndent"> + <properties> + <property name="ignoreIndentationTokens" type="array" value="T_COMMENT,T_DOC_COMMENT"/> + </properties> + </rule> + <rule ref="Generic.WhiteSpace.DisallowTabIndent"/> + + <!-- PHP keywords MUST be in lower case. --> + <rule ref="Generic.PHP.LowerCaseKeyword"/> + + <!-- The PHP constants true, false, and null MUST be in lower case. --> + <rule ref="Generic.PHP.LowerCaseConstant"/> + + <!-- <rule ref="Squiz.Scope.MethodScope"/> --> + <rule ref="Squiz.WhiteSpace.ScopeKeywordSpacing"/> + + <!-- In the argument list, there MUST NOT be a space before each comma, and there MUST be one space after each comma. --> + <!-- + <rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing"> + <properties> + <property name="equalsSpacing" value="1"/> + </properties> + </rule> + <rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing.SpacingAfterHint"> + <severity>0</severity> + </rule> + --> + <rule ref="PEAR.WhiteSpace.ScopeClosingBrace"/> + +</ruleset> diff --git a/htdocs/includes/sabre/sabre/uri/tests/phpunit.xml.dist b/htdocs/includes/sabre/sabre/uri/tests/phpunit.xml.dist new file mode 100644 index 00000000000..338d24d3c06 --- /dev/null +++ b/htdocs/includes/sabre/sabre/uri/tests/phpunit.xml.dist @@ -0,0 +1,18 @@ +<phpunit + colors="true" + bootstrap="../vendor/autoload.php" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" + strict="true" + > + <testsuite name="sabre-uri"> + <directory>.</directory> + </testsuite> + + <filter> + <whitelist addUncoveredFilesFromWhitelist="true"> + <directory suffix=".php">../lib/</directory> + </whitelist> + </filter> +</phpunit> diff --git a/htdocs/includes/sabre/sabre/vobject/.gitignore b/htdocs/includes/sabre/sabre/vobject/.gitignore new file mode 100644 index 00000000000..95935f7988e --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/.gitignore @@ -0,0 +1,21 @@ +# Composer stuff +vendor/ +composer.lock +tests/cov/ +tests/temp + +#vim +.*.swp + +#binaries +bin/phpunit +bin/phpcs +bin/php-cs-fixer +bin/sabre-cs-fixer +bin/hoa + +# Development stuff +testdata/ + +# OS X +.DS_Store diff --git a/htdocs/includes/sabre/sabre/vobject/.travis.yml b/htdocs/includes/sabre/sabre/vobject/.travis.yml new file mode 100644 index 00000000000..3c5b321571a --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/.travis.yml @@ -0,0 +1,20 @@ +language: php +php: + - 5.5 + - 5.6 + - 7.0 + - 7.1 + +sudo: false + +script: + - phpunit --configuration tests/phpunit.xml + - ./bin/sabre-cs-fixer fix . --dry-run --diff + +before_script: + - phpenv config-rm xdebug.ini; true + - composer install + +cache: + directories: + - $HOME/.composer/cache diff --git a/htdocs/includes/sabre/sabre/vobject/CHANGELOG.md b/htdocs/includes/sabre/sabre/vobject/CHANGELOG.md new file mode 100644 index 00000000000..c8f4cb4beb9 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/CHANGELOG.md @@ -0,0 +1,782 @@ +ChangeLog +========= + +4.1.2 (2016-12-15) +------------------ + +* #340: Support for `BYYEARDAY` recurrence when `FREQ=YEARLY`. (@PHPGangsta) +* #341: Support for `BYWEEKNO` recurrence when `FREQ=YEARLY`. (@PHPGangsta) +* Updated to the latest windows timezone data mappings. +* #344: Auto-detecting more Outlook 365-generated timezone identifiers. + (@jpirkey) +* #348: `FreeBusyGenerator` can now accept streams. +* Support sabre/xml 1.5 and 2.0. +* #355: Support `DateTimeInterface` in more places where only `DateTime` was + supported. (@gharlan). +* #351: Fixing an inclusive/exclusive problem with `isInTimeRange` and + `fastForward` with all-day events. (@strokyl, thanks you are brilliant). + + +4.1.1 (2016-07-15) +------------------ + +* #327: Throwing `InvalidDataException` in more cases where invalid iCalendar + dates and times were provided. (@rsto) +* #331: Fix dealing with multiple overridden instances falling on the same + date/time (@afedyk-sugarcrm). +* #333: Fix endless loop on invalid `BYMONTH` values in recurrence. + (@PHPGangsta) +* #339: Fixed a few `validate()` results when repair is off. (@PHPGangsta) +* #338: Stripping invalid `BYMONTH=` rules during `validate()` (@PHPGangsta) +* #336: Fix incorrect `BYSECOND=` validation. (@PHPGangsta) + + +4.1.0 (2016-04-06) +------------------ + +* #309: When expanding recurring events, the first event should also have a + `RECURRENCE-ID` property. +* #306: iTip REPLYs to the first instance of a recurring event was not handled + correctly. +* Slightly better error message during validation of `N` and `ADR` properties. +* #312: Correctly extracing timezone in the iTip broker, even when we don't + have a master event. (@vkomrakov-sugar). +* When validating a component's property that must appear once and which could + automatically be repaired, make sure we report the change as 'repaired'. +* Added a PHPUnitAssertions trait. This trait makes it easy to compare two + vcards or iCalendar objects semantically. +* Better error message when parsing objects with an invalid `VALUE` parameter. + + +4.0.3 (2016-03-12) +------------------ + +* #300: Added `VCard::getByType()` to quickly get a property with a specific + `TYPE` parameter. (@kbond) +* #302: `UNTIL` was not encoded correctly when converting to jCal. + (@GrahamLinagora) +* #303: `COUNT` is now encoded as an int in jCal instead of a string. (@strokyl) +* #295: `RRULE` now has more validation and repair rules. + + +4.0.2 (2016-01-11) +------------------ + +* #288: Only decode `CHARSET` if we're reading vCard 2.1. If it appears + in any other document, we must ignore it. + + +4.0.1 (2016-01-04) +------------------ + +* #284: When generating `CANCEL` iTip messages, we now include `DTEND`. + (@kewisch) + + +4.0.0 (2015-12-11) +------------------ + +* #274: When creating new vCards, the default vCard version is now 4.0. +* #275: `VEVENT`, `VTODO` and `VCARD` now automatically get a `UID` and + `DTSTAMP` property if this was not already specified. +* `ParseException` now extends `\Exception`. +* `Sabre\VObject\Reader::read` now has a `$charset` argument. +* #272: `Sabre\VObject\Recur\EventIterator::$maxInstances` is now + `Sabre\VObject\Settings::$maxRecurrences` and is also honored by the + FreeBusyGenerator. +* #278: `expand()` did not work correctly on events with sub-components. + + +4.0.0-beta1 (2015-12-02) +------------------------ + +* #258: Support for expanding events that use `RDATE`. (@jabdoa2) +* #258: Correctly support TZID for events that use `RDATE`. (@jabdoa2) +* #240: `Component\VCalendar::expand()` now returns a new expanded `VCalendar` + object, instead of editing the existing `VCalendar` in-place. This is a BC + break. +* #265: Using the new `InvalidDataException` in place of + `InvalidArgumentException` and `LogicException` in all places where we fail + because there was something wrong with input data. +* #227: Always add `VALUE=URI` to `PHOTO` properties. +* #235: Always add `VALUE=URI` to `URL` properties. +* It's now possible to override which class is used instead of + `Component\VCalendar` or `Component\VCard` during parsing. +* #263: Lots of small cleanups. (@jakobsack) +* #220: Automatically stop recurring after 3500 recurrences. +* #41: Allow user to set different encoding than UTF-8 when decoding vCards. +* #41: Support the `ENCODING` parameter from vCard 2.1. + Both ISO-8859-1 and Windows-1252 are currently supported. +* #185: Fix encoding/decoding of `TIME` values in jCal/jCard. + + +4.0.0-alpha2 (2015-09-04) +------------------------- + +* Updated windows timezone file to support new mexican timezone. +* #239: Added a `BirthdayCalendarGenerator`. (@DominikTo) +* #250: `isInTimeRange()` now considers the timezone for floating dates and + times. (@armin-hackmann) +* Added a duplicate vcard merging tool for the command line. +* #253: `isInTimeRange()` now correctly handles events that throw the + `NoInstancesException` exception. (@migrax, @DominikTo) +* #254: The parser threw an `E_NOTICE` for certain invalid objects. It now + correctly throws a `ParseException`. + + +4.0.0-alpha1 (2015-07-17) +------------------------- + +* sabre/vobject now requires PHP 5.5. +* #244: PHP7 support. +* Lots of speedups and reduced memory usage! +* #160: Support for xCal a.k.a. RFC6321! (@Hywan) +* #192: Support for xCard a.k.a. RFC6351! (@Hywan) +* #139: We now accept `DateTimeInterface` wherever it accepted `DateTime` + before in arguments. This means that either `DateTime` or + `DateTimeImmutable` may be used everywhere. +* #242: Full support for the `VAVAILABILITY` component, and calculating + `VFREEBUSY` based on `VAVAILABILITY` data. +* #186: Fixing conversion of `UTC-OFFSET` properties when going back and + forward between jCal and iCalendar. +* Properties, Components and Parameters now implement PHP's `JsonSerializable` + interface. +* #139: We now _always_ return `DateTimeImmutable` from any method. This could + potentially have big implications if you manipulate Date objects anywhere. +* #161: Simplified `ElementList` by extending `ArrayIterator`. +* Removed `RecurrenceIterator` (use Recur\EventIterator instead). +* Now using php-cs-fixer to automatically enforce and correct CS. +* #233: The `+00:00` timezone is now recognized as UTC. (@c960657) +* #237: Added a `destroy()` method to all documents. This method breaks any + circular references, allowing PHP to free up memory. +* #197: Made accessing properties and objects by their name a lot faster. This + especially helps objects that have a lot of sub-components or properties, + such as large iCalendar objects. +* #197: The `$children` property on components has been changed from `public` + to `protected`. Use the `children()` method instead to get a flat list of + objects. +* #244: The `Float` and `Integer` classes have been renamed to `FloatValue` + and `IntegerValue` to allow PHP 7 compatibility. + + +3.5.3 (2016-10-06) +------------------ + +* #331: Fix dealing with multiple overridden instances falling on the same + date/time (@afedyk-sugarcrm). + + +3.5.2 (2016-04-24) +----------------- + +* #312: Backported a fix related to iTip processing of events with timezones, + without a master event. + + +3.5.1 (2016-04-06) +------------------ + +* #309: When expanding recurring events, the first event should also have a + `RECURRENCE-ID` property. +* #306: iTip REPLYs to the first instance of a recurring event was not handled + correctly. + + +3.5.0 (2016-01-11) +------------------ + +* This release supports PHP 7, contrary to 3.4.x versions. +* BC Break: `Sabre\VObject\Property\Float` has been renamed to + `Sabre\VObject\Property\FloatValue`. +* BC Break: `Sabre\VObject\Property\Integer` has been renamed to + `Sabre\VObject\Property\IntegerValue`. + + +3.4.9 (2016-01-11) +------------------ + +* This package now specifies in composer.json that it does not support PHP 7. + For PHP 7, use version 3.5.x or 4.x. + + +3.4.8 (2016-01-04) +------------------ + +* #284: When generating `CANCEL` iTip messages, we now include `DTEND`. + (@kewisch). + + +3.4.7 (2015-09-05) +------------------ + +* #253: Handle `isInTimeRange` for recurring events that have 0 valid + instances. (@DominikTo, @migrax). + + +3.4.6 (2015-08-06) +------------------ + +* #250: Recurring all-day events are incorrectly included in time range + requests when not using UTC in the time range. (@armin-hackmann) + + +3.4.5 (2015-06-02) +------------------ + +* #229: Converting vcards from 3.0 to 4.0 that contained a `LANG` property + would throw an error. + + +3.4.4 (2015-05-27) +------------------ + +* #228: Fixed a 'party crasher' bug in the iTip broker. This would break + scheduling in some cases. + + +3.4.3 (2015-05-19) +------------------ + +* #219: Corrected validation of `EXDATE` properties with more than one value. +* #212: `BYSETPOS` with values below `-1` was broken and could cause infinite + loops. +* #211: Fix `BYDAY=-5TH` in recurrence iterator. (@lindquist) +* #216: `ENCODING` parameter is now validated for all document types. +* #217: Initializing vCard `DATE` objects with a PHP DateTime object will now + work correctly. (@thomascube) + + +3.4.2 (2015-02-25) +------------------ + +* #210: iTip: Replying to an event without a master event was broken. + + +3.4.1 (2015-02-24) +------------------ + +* A minor change to ensure that unittests work correctly in the sabre/dav + test-suite. + + +3.4.0 (2015-02-23) +------------------ + +* #196: Made parsing recurrence rules a lot faster on big calendars. +* Updated windows timezone mappings to latest unicode version. +* #202: Support for parsing and validating `VAVAILABILITY` components. (@Hywan) +* #195: PHP 5.3 compatibility in 'generatevcards' script. (@rickdenhaan) +* #205: Improving handling of multiple `EXDATE` when processing iTip changes. + (@armin-hackmann) +* #187: Fixed validator rules for `LAST-MODIFIED` properties. +* #188: Retain floating times when generating instances using + `Recur\EventIterator`. +* #203: Skip tests for timezones that are not supported on older PHP versions, + instead of a hard fail. +* #204: Dealing a bit better with vCard date-time values that contained + milliseconds. (which is normally invalid). (@armin-hackmann) + + +3.3.5 (2015-01-09) +------------------ + +* #168: Expanding calendars now removes objects with recurrence rules that + don't have a valid recurrence instance. +* #177: SCHEDULE-STATUS should not contain a reason phrase, only a status + code. +* #175: Parser can now read and skip the UTF-8 BOM. +* #179: Added `isFloating` to `DATE-TIME` properties. +* #179: Fixed jCal serialization of floating `DATE-TIME` properties. +* #173: vCard converter failed for `X-ABDATE` properties that had no + `X-ABLABEL`. +* #180: Added `PROFILE_CALDAV` and `PROFILE_CARDDAV` to enable validation rules + specific for CalDAV/CardDAV servers. +* #176: A missing `UID` is no longer an error, but a warning for the vCard + validator, unless `PROFILE_CARDDAV` is specified. + + +3.3.4 (2014-11-19) +------------------ + +* #154: Converting `ANNIVERSARY` to `X-ANNIVERSARY` and `X-ABDATE` and + vice-versa when converting to/from vCard 4. +* #154: It's now possible to easily select all vCard properties belonging to + a single group with `$vcard->{'ITEM1.'}` syntax. (@armin-hackmann) +* #156: Simpler way to check if a string is UTF-8. (@Hywan) +* Unittest improvements. +* #159: The recurrence iterator, freebusy generator and iCalendar DATE and + DATE-TIME properties can now all accept a reference timezone when working + floating times or all-day events. +* #159: Master events will no longer get a `RECURRENCE-ID` when expanding. +* #159: `RECURRENCE-ID` for all-day events will now be correct when expanding. +* #163: Added a `getTimeZone()` method to `VTIMEZONE` components. + + +3.3.3 (2014-10-09) +------------------ + +* #142: `CANCEL` and `REPLY` messages now include the `DTSTART` from the + original event. +* #143: `SCHEDULE-AGENT` on the `ORGANIZER` property is respected. +* #144: `PARTSTAT=NEEDS-ACTION` is now set for new invites, if no `PARTSTAT` is + set to support the inbox feature of iOS. +* #147: Bugs related to scheduling all-day events. +* #148: Ignore events that have attendees but no organizer. +* #149: Avoiding logging errors during timezone detection. This is a workaround + for a PHP bug. +* Support for "Line Islands Standard Time" windows timezone. +* #154: Correctly work around vCard parameters that have a value but no name. + + +3.3.2 (2014-09-19) +------------------ + +* Changed: iTip broker now sets RSVP status to false when replies are received. +* #118: iTip Message now has a `getScheduleStatus()` method. +* #119: Support for detecting 'significant changes'. +* #120: Support for `SCHEDULE-FORCE-SEND`. +* #121: iCal demands parameters containing the + sign to be quoted. +* #122: Don't generate REPLY messages for events that have been cancelled. +* #123: Added `SUMMARY` to iTip messages. +* #130: Incorrect validation rules for `RELATED` (should be `RELATED-TO`). +* #128: `ATTACH` in iCalendar is `URI` by default, not `BINARY`. +* #131: RRULE that doesn't provide a single valid instance now throws an + exception. +* #136: Validator rejects *all* control characters. We were missing a few. +* #133: Splitter objects will throw exceptions when receiving incompatible + objects. +* #127: Attendees who delete recurring event instances events they had already + declined earlier will no longer generate another reply. +* #125: Send CANCEL messages when ORGANIZER property gets deleted. + + +3.3.1 (2014-08-18) +------------------ + +* Changed: It's now possible to pass DateTime objects when using the magic + setters on properties. (`$event->DTSTART = new DateTime('now')`). +* #111: iTip Broker does not process attendee adding events to EXDATE. +* #112: EventIterator now sets TZID on RECURRENCE-ID. +* #113: Timezone support during creation of iTip REPLY messages. +* #114: VTIMEZONE is retained when generating new REQUEST objects. +* #114: Support for 'MAILTO:' style email addresses (in uppercase) in the iTip + broker. This improves evolution support. +* #115: Using REQUEST-STATUS from REPLY messages and now propegating that into + SCHEDULE-STATUS. + + +3.3.0 (2014-08-07) +------------------ + +* We now use PSR-4 for the directory structure. This means that everything + that was used to be in the `lib/Sabre/VObject` directory is now moved to + `lib/`. If you use composer to load this library, you shouldn't have to do + anything about that though. +* VEVENT now get populated with a DTSTAMP and UID property by default. +* BC Break: Removed the 'includes.php' file. Use composer instead. +* #103: Added support for processing [iTip][iTip] messages. This allows a user + to parse incoming iTip messages and apply the result on existing calendars, + or automatically generate invites/replies/cancellations based on changes that + a user made on objects. +* #75, #58, #18: Fixes related to overriding the first event in recurrences. +* Added: VCalendar::getBaseComponent to find the 'master' component in a + calendar. +* #51: Support for iterating RDATE properties. +* Fixed: Issue #101: RecurrenceIterator::nextMonthly() shows events that are + excluded events with wrong time + + +3.2.4 (2014-07-14) +------------------ + +* Added: Issue #98. The VCardConverter now takes `X-APPLE-OMIT-YEAR` into + consideration when converting between vCard 3 and 4. +* Fixed: Issue #96. Some support for Yahoo's broken vcards. +* Fixed: PHP 5.3 support was broken in the cli tool. + + +3.2.3 (2014-06-12) +------------------ + +* Validator now checks if DUE and DTSTART are of the same type in VTODO, and + ensures that DUE is always after DTSTART. +* Removed documentation from source repository, to http://sabre.io/vobject/ +* Expanded the vobject cli tool validation output to make it easier to find + issues. +* Fixed: vobject repair. It was not working for iCalendar objects. + + +3.2.2 (2014-05-07) +------------------ + +* Minor tweak in unittests to make it run on PHP 5.5.12. Json-prettifying + slightly changed which caused the test to fail. + + +3.2.1 (2014-05-03) +------------------ + +* Minor tweak to make the unittests run with the latest hhvm on travis. +* Updated timezone definitions. +* Updated copyright links to point to http://sabre.io/ + + +3.2.0 (2014-04-02) +------------------ + +* Now hhvm compatible! +* The validator can now detect a _lot_ more problems. Many rules for both + iCalendar and vCard were added. +* Added: bin/generate_vcards, a utility to generate random vcards for testing + purposes. Patches are welcome to add more data. +* Updated: Windows timezone mapping to latest version from unicode.org +* Changed: The timezone maps are now loaded in from external files, in + lib/Sabre/VObject/timezonedata. +* Added: Fixing badly encoded URL's from google contacts vcards. +* Fixed: Issue #68. Couldn't decode properties ending in a colon. +* Fixed: Issue #72. RecurrenceIterator should respect timezone in the UNTIL + clause. +* Fixed: Issue #67. BYMONTH limit on DAILY recurrences. +* Fixed: Issue #26. Return a more descriptive error when coming across broken + BYDAY rules. +* Fixed: Issue #28. Incorrect timezone detection for some timezones. +* Fixed: Issue #70. Casting a parameter with a null value to string would fail. +* Added: Support for rfc6715 and rfc6474. +* Added: Support for DateTime objects in the VCard DATE-AND-OR-TIME property. +* Added: UUIDUtil, for easily creating unique identifiers. +* Fixed: Issue #83. Creating new VALUE=DATE objects using php's DateTime. +* Fixed: Issue #86. Don't go into an infinite loop when php errors are + disabled and an invalid file is read. + + +3.1.4 (2014-03-30) +------------------ + +* Fixed: Issue #87: Several compatibility fixes related to timezone handling + changes in PHP 5.5.10. + + +3.1.3 (2013-10-02) +------------------ + +* Fixed: Support from properties from draft-daboo-valarm-extensions-04. Issue + #56. +* Fixed: Issue #54. Parsing a stream of multiple vcards separated by more than + one newline. Thanks @Vedmak for the patch. +* Fixed: Serializing vcard 2.1 parameters with no name caused a literal '1' to + be inserted. +* Added: VCardConverter removed properties that are no longer supported in vCard + 4.0. +* Added: vCards with a minimum number of values (such as N), but don't have that + many, are now automatically padded with empty components. +* Added: The vCard validator now also checks for a minimum number of components, + and has the ability to repair these. +* Added: Some support for vCard 2.1 in the VCard converter, to upgrade to vCard + 3.0 or 4.0. +* Fixed: Issue 60 Use Document::$componentMap when instantiating the top-level + VCalendar and VCard components. +* Fixed: Issue 62: Parsing iCalendar parameters with no value. +* Added: --forgiving option to vobject utility. +* Fixed: Compound properties such as ADR were not correctly split up in vCard + 2.1 quoted printable-encoded properties. +* Fixed: Issue 64: Encoding of binary properties of converted vCards. Thanks + @DominikTo for the patch. + + +3.1.2 (2013-08-13) +------------------ + +* Fixed: Setting correct property group on VCard conversion + + +3.1.1 (2013-08-02) +------------------ + +* Fixed: Issue #53. A regression in RecurrenceIterator. + + +3.1.0 (2013-07-27) +------------------ + +* Added: bad-ass new cli debugging utility (in bin/vobject). +* Added: jCal and jCard parser. +* Fixed: URI properties should not escape ; and ,. +* Fixed: VCard 4 documents now correctly use URI as a default value-type for + PHOTO and others. BINARY no longer exists in vCard 4. +* Added: Utility to convert between 2.1, 3.0 and 4.0 vCards. +* Added: You can now add() multiple parameters to a property in one call. +* Added: Parameter::has() for easily checking if a parameter value exists. +* Added: VCard::preferred() to find a preferred email, phone number, etc for a + contact. +* Changed: All $duration properties are now public. +* Added: A few validators for iCalendar documents. +* Fixed: Issue #50. RecurrenceIterator gives incorrect result when exception + events are out of order in the iCalendar file. +* Fixed: Issue #48. Overridden events in the recurrence iterator that were past + the UNTIL date were ignored. +* Added: getDuration for DURATION values such as TRIGGER. Thanks to + @SimonSimCity. +* Fixed: Issue #52. vCard 2.1 parameters with no name may lose values if there's + more than 1. Thanks to @Vedmak. + + +3.0.0 (2013-06-21) +------------------ + +* Fixed: includes.php file was still broken. Our tool to generate it had some + bugs. + + +3.0.0-beta4 (2013-06-21) +------------------------ + +* Fixed: includes.php was no longer up to date. + + +3.0.0-beta3 (2013-06-17) +------------------------ + +* Added: OPTION_FORGIVING now also allows slashes in property names. +* Fixed: DateTimeParser no longer fails on dates with years < 1000 & > 4999 +* Fixed: Issue 36: Workaround for the recurrenceiterator and caldav events with + a missing base event. +* Fixed: jCard encoding of TIME properties. +* Fixed: jCal encoding of REQUEST-STATUS, GEO and PERIOD values. + + +3.0.0-beta2 (2013-06-10) +------------------------ + +* Fixed: Corrected includes.php file. +* Fixed: vCard date-time parser supported extended-format dates as well. +* Changed: Properties have been moved to an ICalendar or VCard directory. +* Fixed: Couldn't parse vCard 3 extended format dates and times. +* Fixed: Couldn't export jCard DATE values correctly. +* Fixed: Recursive loop in ICalendar\DateTime property. + + +3.0.0-beta1 (2013-06-07) +------------------------ + +* Added: jsonSerialize() for creating jCal and jCard documents. +* Added: helper method to parse vCard dates and times. +* Added: Specialized classes for FLOAT, LANGUAGE-TAG, TIME, TIMESTAMP, + DATE-AND-OR-TIME, CAL-ADDRESS, UNKNOWN and UTC-OFFSET properties. +* Removed: CommaSeparatedText property. Now included into Text. +* Fixed: Multiple parameters with the same name are now correctly encoded. +* Fixed: Parameter values containing a comma are now enclosed in double-quotes. +* Fixed: Iterating parameter values should now fully work as expected. +* Fixed: Support for vCard 2.1 nameless parameters. +* Changed: $valueMap, $componentMap and $propertyMap now all use fully-qualified + class names, so they are actually overridable. +* Fixed: Updating DATE-TIME to DATE values now behaves like expected. + + +3.0.0-alpha4 (2013-05-31) +------------------------- + +* Added: It's now possible to send parser options to the splitter classes. +* Added: A few tweaks to improve component and property creation. + + +3.0.0-alpha3 (2013-05-13) +------------------------- + +* Changed: propertyMap, valueMap and componentMap are now static properties. +* Changed: Component::remove() will throw an exception when trying to a node + that's not a child of said component. +* Added: Splitter objects are now faster, line numbers are accurately reported + and use less memory. +* Added: MimeDir parser can now continue parsing with the same stream buffer. +* Fixed: vobjectvalidate.php is operational again. +* Fixed: \r is properly stripped in text values. +* Fixed: QUOTED-PRINTABLE is now correctly encoded as well as encoded, for + vCards 2.1. +* Fixed: Parser assumes vCard 2.1, if no version was supplied. + + +3.0.0-alpha2 (2013-05-22) +------------------------- + +* Fixed: vCard URL properties were referencing a non-existant class. + + +3.0.0-alpha1 (2013-05-21) +------------------------- + +* Fixed: Now correctly dealing with escaping of properties. This solves the + problem with double-backslashes where they don't belong. +* Added: Easy support for properties with more than one value, using setParts + and getParts. +* Added: Support for broken 2.1 vCards produced by microsoft. +* Added: Automatically decoding quoted-printable values. +* Added: Automatically decoding base64 values. +* Added: Decoding RFC6868 parameter values (uses ^ as an escape character). +* Added: Fancy new MimeDir parser that can also parse streams. +* Added: Automatically mapping many, many properties to a property-class with + specialized API's. +* Added: remove() method for easily removing properties and sub-components + components. +* Changed: Components, Properties and Parameters can no longer be created with + Component::create, Property::create and Parameter::create. They must instead + be created through the root component. (A VCalendar or VCard object). +* Changed: API for DateTime properties has slightly changed. +* Changed: the ->value property is now protected everywhere. Use getParts() and + getValue() instead. +* BC Break: No support for mac newlines (\r). Never came across these anyway. +* Added: add() method to the Property class. +* Added: It's now possible to easy set multi-value properties as arrays. +* Added: When setting date-time properties you can just pass PHP's DateTime + object. +* Added: New components automatically get a bunch of default properties, such as + VERSION and CALSCALE. +* Added: You can add new sub-components much quicker with the magic setters, and + add() method. + + +2.1.7 (2015-01-21) +------------------ + +* Fixed: Issue #94, a workaround for bad escaping of ; and , in compound + properties. It's not a full solution, but it's an improvement for those + stuck in the 2.1 versions. + + +2.1.6 (2014-12-10) +------------------ + +* Fixed: Minor change to make sure that unittests succeed on every PHP version. + + +2.1.5 (2014-06-03) +------------------ + +* Fixed: #94: Better parameter escaping. +* Changed: Documentation cleanups. + + +2.1.4 (2014-03-30) +------------------ + +* Fixed: Issue #87: Several compatibility fixes related to timezone handling + changes in PHP 5.5.10. + + +2.1.3 (2013-10-02) +------------------ + +* Fixed: Issue #55. \r must be stripped from property values. +* Fixed: Issue #65. Putting quotes around parameter values that contain a colon. + + +2.1.2 (2013-08-02) +------------------ + +* Fixed: Issue #53. A regression in RecurrenceIterator. + + +2.1.1 (2013-07-27) +------------------ + +* Fixed: Issue #50. RecurrenceIterator gives incorrect result when exception + events are out of order in the iCalendar file. +* Fixed: Issue #48. Overridden events in the recurrence iterator that were past + the UNTIL date were ignored. + + +2.1.0 (2013-06-17) +------------------ + +* This version is fully backwards compatible with 2.0.\*. However, it contains a + few new API's that mimic the VObject 3 API. This allows it to be used a + 'bridge' version. Specifically, this new version exists so SabreDAV 1.7 and + 1.8 can run with both the 2 and 3 versions of this library. +* Added: Property\DateTime::hasTime(). +* Added: Property\MultiDateTime::hasTime(). +* Added: Property::getValue(). +* Added: Document class. +* Added: Document::createComponent and Document::createProperty. +* Added: Parameter::getValue(). + + +2.0.7 (2013-03-05) +------------------ + +* Fixed: Microsoft re-uses their magic numbers for different timezones, + specifically id 2 for both Sarajevo and Lisbon). A workaround was added to + deal with this. + + +2.0.6 (2013-02-17) +------------------ + +* Fixed: The reader now properly parses parameters without a value. + + +2.0.5 (2012-11-05) +------------------ + +* Fixed: The FreeBusyGenerator is now properly using the factory methods for + creation of components and properties. + + +2.0.4 (2012-11-02) +------------------ + +* Added: Known Lotus Notes / Domino timezone id's. + + +2.0.3 (2012-10-29) +------------------ + +* Added: Support for 'GMT+????' format in TZID's. +* Added: Support for formats like SystemV/EST5EDT in TZID's. +* Fixed: RecurrenceIterator now repairs recurrence rules where UNTIL < DTSTART. +* Added: Support for BYHOUR in FREQ=DAILY (@hollodk). +* Added: Support for BYHOUR and BYDAY in FREQ=WEEKLY. + + +2.0.2 (2012-10-06) +------------------ + +* Added: includes.php file, to load the entire library in one go. +* Fixed: A problem with determining alarm triggers for TODO's. + + +2.0.1 (2012-09-22) +------------------ + +* Removed: Element class. It wasn't used. +* Added: Basic validation and repair methods for broken input data. +* Fixed: RecurrenceIterator could infinitely loop when an INTERVAL of 0 was + specified. +* Added: A cli script that can validate and automatically repair vcards and + iCalendar objects. +* Added: A new 'Compound' property, that can automatically split up parts for + properties such as N, ADR, ORG and CATEGORIES. +* Added: Splitter classes, that can split up large objects (such as exports) + into individual objects (thanks @DominikTo and @armin-hackmann). +* Added: VFREEBUSY component, which allows easily checking wether timeslots are + available. +* Added: The Reader class now has a 'FORGIVING' option, which allows it to parse + properties with incorrect characters in the name (at this time, it just allows + underscores). +* Added: Also added the 'IGNORE_INVALID_LINES' option, to completely disregard + any invalid lines. +* Fixed: A bug in Windows timezone-id mappings for times created in Greenlands + timezone (sorry Greenlanders! I do care!). +* Fixed: DTEND was not generated correctly for VFREEBUSY reports. +* Fixed: Parser is at least 25% faster with real-world data. + + +2.0.0 (2012-08-08) +------------------ + +* VObject is now a separate project from SabreDAV. See the SabreDAV changelog + for version information before 2.0. +* New: VObject library now uses PHP 5.3 namespaces. +* New: It's possible to specify lists of parameters when constructing + properties. +* New: made it easier to construct the FreeBusyGenerator. + +[iTip]: http://tools.ietf.org/html/rfc5546 diff --git a/htdocs/includes/sabre/sabre/vobject/LICENSE b/htdocs/includes/sabre/sabre/vobject/LICENSE new file mode 100644 index 00000000000..a99c8da1988 --- /dev/null +++ b/htdocs/includes/sabre/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/htdocs/includes/sabre/sabre/vobject/README.md b/htdocs/includes/sabre/sabre/vobject/README.md new file mode 100644 index 00000000000..0e37f13880d --- /dev/null +++ b/htdocs/includes/sabre/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/fruux/sabre-vobject.svg?branch=master)](https://travis-ci.org/fruux/sabre-vobject) | +| 3.5 | [![Build Status](https://travis-ci.org/fruux/sabre-vobject.svg?branch=3.5)](https://travis-ci.org/fruux/sabre-vobject) | +| 3.4 | [![Build Status](https://travis-ci.org/fruux/sabre-vobject.svg?branch=3.4)](https://travis-ci.org/fruux/sabre-vobject) | +| 3.1 | [![Build Status](https://travis-ci.org/fruux/sabre-vobject.svg?branch=3.1)](https://travis-ci.org/fruux/sabre-vobject) | +| 2.1 | [![Build Status](https://travis-ci.org/fruux/sabre-vobject.svg?branch=2.1)](https://travis-ci.org/fruux/sabre-vobject) | +| 2.0 | [![Build Status](https://travis-ci.org/fruux/sabre-vobject.svg?branch=2.0)](https://travis-ci.org/fruux/sabre-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/htdocs/includes/sabre/sabre/vobject/bin/bench.php b/htdocs/includes/sabre/sabre/vobject/bin/bench.php new file mode 100644 index 00000000000..807b40777c6 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/bin/bench.php @@ -0,0 +1,12 @@ +#!/usr/bin/env php +<?php + +include __DIR__ . '/../vendor/autoload.php'; + +$data = stream_get_contents(STDIN); + +$start = microtime(true); + +$lol = Sabre\VObject\Reader::read($data); + +echo "time: " . (microtime(true) - $start) . "\n"; diff --git a/htdocs/includes/sabre/sabre/vobject/bin/bench_freebusygenerator.php b/htdocs/includes/sabre/sabre/vobject/bin/bench_freebusygenerator.php new file mode 100644 index 00000000000..2c51b2a32b0 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/bin/bench_freebusygenerator.php @@ -0,0 +1,62 @@ +<?php + +include __DIR__ . '/../vendor/autoload.php'; + +if ($argc < 2) { + echo "sabre/vobject ", Sabre\VObject\Version::VERSION, " freebusy benchmark\n"; + echo "\n"; + echo "This script can be used to measure the speed of generating a\n"; + echo "free-busy report based on a calendar.\n"; + echo "\n"; + echo "The process will be repeated 100 times to get accurate stats\n"; + echo "\n"; + echo "Usage: " . $argv[0] . " inputfile.ics\n"; + die(); +} + +list(, $inputFile) = $argv; + +$bench = new Hoa\Bench\Bench(); +$bench->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/htdocs/includes/sabre/sabre/vobject/bin/bench_manipulatevcard.php b/htdocs/includes/sabre/sabre/vobject/bin/bench_manipulatevcard.php new file mode 100644 index 00000000000..adc198e9bc6 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/bin/bench_manipulatevcard.php @@ -0,0 +1,69 @@ +<?php + +include __DIR__ . '/../vendor/autoload.php'; + +if ($argc < 2) { + echo "sabre/vobject ", Sabre\VObject\Version::VERSION, " manipulation benchmark\n"; + echo "\n"; + echo "This script can be used to measure the speed of opening a large amount of\n"; + echo "vcards, making a few alterations and serializing them again.\n"; + echo "system."; + echo "\n"; + echo "Usage: " . $argv[0] . " inputfile.vcf\n"; + die(); +} + +list(, $inputFile) = $argv; + +$input = file_get_contents($inputFile); + +$splitter = new Sabre\VObject\Splitter\VCard($input); + +$bench = new Hoa\Bench\Bench(); + +while (true) { + + $bench->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/htdocs/includes/sabre/sabre/vobject/bin/fetch_windows_zones.php b/htdocs/includes/sabre/sabre/vobject/bin/fetch_windows_zones.php new file mode 100644 index 00000000000..3f2a00f7ae0 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/bin/fetch_windows_zones.php @@ -0,0 +1,51 @@ +#!/usr/bin/env php +<?php + +$windowsZonesUrl = 'http://unicode.org/repos/cldr/trunk/common/supplemental/windowsZones.xml'; +$outputFile = __DIR__ . '/../lib/timezonedata/windowszones.php'; + +echo "Fetching timezone map from: " . $windowsZonesUrl, "\n"; + +$data = file_get_contents($windowsZonesUrl); + +$xml = simplexml_load_string($data); + +$map = []; + +foreach ($xml->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, "<?php\n\n"); +fwrite($f, "/**\n"); +fwrite($f, " * Automatically generated timezone file\n"); +fwrite($f, " *\n"); +fwrite($f, " * Last update: " . date(DATE_W3C) . "\n"); +fwrite($f, " * Source: " . $windowsZonesUrl . "\n"); +fwrite($f, " *\n"); +fwrite($f, " * @copyright Copyright (C) fruux GmbH (https://fruux.com/).\n"); +fwrite($f, " * @license http://sabre.io/license/ Modified BSD License\n"); +fwrite($f, " */\n"); +fwrite($f, "\n"); +fwrite($f, "return "); +fwrite($f, var_export($map, true) . ';'); +fclose($f); + +echo "Formatting\n"; + +exec(__DIR__ . '/sabre-cs-fixer fix ' . escapeshellarg($outputFile)); + +echo "Done\n"; diff --git a/htdocs/includes/sabre/sabre/vobject/bin/generate_vcards b/htdocs/includes/sabre/sabre/vobject/bin/generate_vcards new file mode 100644 index 00000000000..4663c3c16d5 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/bin/generate_vcards @@ -0,0 +1,241 @@ +#!/usr/bin/env php +<?php + +namespace Sabre\VObject; + +// This sucks.. we have to try to find the composer autoloader. But chances +// are, we can't find it this way. So we'll do our bestest +$paths = [ + __DIR__ . '/../vendor/autoload.php', // In case vobject is cloned directly + __DIR__ . '/../../../autoload.php', // In case vobject is a composer dependency. +]; + +foreach($paths as $path) { + if (file_exists($path)) { + include $path; + break; + } +} + +if (!class_exists('Sabre\\VObject\\Version')) { + fwrite(STDERR, "Composer autoloader could not be properly loaded.\n"); + die(1); +} + +if ($argc < 2) { + + $version = Version::VERSION; + + $help = <<<HI +sabre/vobject $version +Usage: + generate_vcards [count] + +Options: + count The number of random vcards to generate + +Examples: + generate_vcards 1000 > 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/htdocs/includes/sabre/sabre/vobject/bin/generateicalendardata.php b/htdocs/includes/sabre/sabre/vobject/bin/generateicalendardata.php new file mode 100644 index 00000000000..a2df3c63a0c --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/bin/generateicalendardata.php @@ -0,0 +1,88 @@ +#!/usr/bin/env php +<?php + +use Sabre\VObject; + +if ($argc < 2) { + $cmd = $argv[0]; + fwrite(STDERR, <<<HI +Fruux test data generator + +This script generates a lot of test data. This is used for profiling and stuff. +Currently it just generates events in a single calendar. + +The iCalendar output goes to stdout. Other messages to stderr. + +{$cmd} [events] + + +HI + ); + die(); +} + +$events = 100; + +if (isset($argv[1])) $events = (int)$argv[1]; + +include __DIR__ . '/../vendor/autoload.php'; + +fwrite(STDERR, "Generating " . $events . " events\n"); + +$currentDate = new DateTime('-' . round($events / 2) . ' days'); + +$calendar = new VObject\Component\VCalendar(); + +$ii = 0; + +while ($ii < $events) { + + $ii++; + + $event = $calendar->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)); + die(-1); +} + +fwrite(STDERR, "Serializing this beast\n"); + +echo $calendar->serialize(); + +fwrite(STDERR, "done.\n"); diff --git a/htdocs/includes/sabre/sabre/vobject/bin/mergeduplicates.php b/htdocs/includes/sabre/sabre/vobject/bin/mergeduplicates.php new file mode 100644 index 00000000000..076524d36a6 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/bin/mergeduplicates.php @@ -0,0 +1,184 @@ +#!/usr/bin/env php +<?php + +namespace Sabre\VObject; + +// This sucks.. we have to try to find the composer autoloader. But chances +// are, we can't find it this way. So we'll do our bestest +$paths = [ + __DIR__ . '/../vendor/autoload.php', // In case vobject is cloned directly + __DIR__ . '/../../../autoload.php', // In case vobject is a composer dependency. +]; + +foreach ($paths as $path) { + if (file_exists($path)) { + include $path; + break; + } +} + +if (!class_exists('Sabre\\VObject\\Version')) { + fwrite(STDERR, "Composer autoloader could not be loaded.\n"); + die(1); +} + +echo "sabre/vobject ", Version::VERSION, " duplicate contact merge tool\n"; + +if ($argc < 3) { + + echo "\n"; + echo "Usage: ", $argv[0], " input.vcf output.vcf [debug.log]\n"; + die(1); + +} + +$input = fopen($argv[1], 'r'); +$output = fopen($argv[2], 'w'); +$debug = isset($argv[3]) ? fopen($argv[3], 'w') : null; + +$splitter = new Splitter\VCard($input); + +// The following properties are ignored. If they appear in some vcards +// but not in others, we don't consider them for the sake of finding +// differences. +$ignoredProperties = [ + "PRODID", + "VERSION", + "REV", + "UID", + "X-ABLABEL", +]; + + +$collectedNames = []; + +$stats = [ + "Total vcards" => 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 ($newProp->name === 'EMAIL' || $newProp->name === 'TEL') { + + // 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/htdocs/includes/sabre/sabre/vobject/bin/rrulebench.php b/htdocs/includes/sabre/sabre/vobject/bin/rrulebench.php new file mode 100644 index 00000000000..af26b4765ca --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/bin/rrulebench.php @@ -0,0 +1,32 @@ +<?php + +include __DIR__ . '/../vendor/autoload.php'; + +if ($argc < 4) { + echo "sabre/vobject ", Sabre\VObject\Version::VERSION, " RRULE benchmark\n"; + echo "\n"; + echo "This script can be used to measure the speed of the 'recurrence expansion'\n"; + echo "system."; + echo "\n"; + echo "Usage: " . $argv[0] . " inputfile.ics startdate enddate\n"; + die(); +} + +list(, $inputFile, $startDate, $endDate) = $argv; + +$bench = new Hoa\Bench\Bench(); +$bench->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/htdocs/includes/sabre/sabre/vobject/bin/vobject b/htdocs/includes/sabre/sabre/vobject/bin/vobject new file mode 100644 index 00000000000..2aca7e72965 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/bin/vobject @@ -0,0 +1,27 @@ +#!/usr/bin/env php +<?php + +namespace Sabre\VObject; + +// This sucks.. we have to try to find the composer autoloader. But chances +// are, we can't find it this way. So we'll do our bestest +$paths = [ + __DIR__ . '/../vendor/autoload.php', // In case vobject is cloned directly + __DIR__ . '/../../../autoload.php', // In case vobject is a composer dependency. +]; + +foreach($paths as $path) { + if (file_exists($path)) { + include $path; + break; + } +} + +if (!class_exists('Sabre\\VObject\\Version')) { + fwrite(STDERR, "Composer autoloader could not be loaded.\n"); + die(1); +} + +$cli = new Cli(); +exit($cli->main($argv)); + diff --git a/htdocs/includes/sabre/sabre/vobject/composer.json b/htdocs/includes/sabre/sabre/vobject/composer.json new file mode 100644 index 00000000000..cfa4a712d37 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/composer.json @@ -0,0 +1,88 @@ +{ + "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" : ">=5.5", + "ext-mbstring" : "*", + "sabre/xml" : ">=1.5 <3.0" + }, + "require-dev" : { + "phpunit/phpunit" : "*", + "sabre/cs" : "^1.0.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/" + } + }, + "bin" : [ + "bin/vobject", + "bin/generate_vcards" + ], + "extra" : { + "branch-alias" : { + "dev-master" : "4.0.x-dev" + } + }, + "config" : { + "bin-dir" : "bin" + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/BirthdayCalendarGenerator.php b/htdocs/includes/sabre/sabre/vobject/lib/BirthdayCalendarGenerator.php new file mode 100644 index 00000000000..55391224997 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/BirthdayCalendarGenerator.php @@ -0,0 +1,191 @@ +<?php + +namespace Sabre\VObject; + +use Sabre\VObject\Component\VCalendar; + +/** + * This class generates birthday calendars. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Dominik Tobschall (http://tobschall.de/) + * @license http://sabre.io/license/ Modified BSD License + */ +class BirthdayCalendarGenerator { + + /** + * Input objects. + * + * @var array + */ + protected $objects = []; + + /** + * Default year. + * Used for dates without a year. + */ + const DEFAULT_YEAR = 2000; + + /** + * Output format for the SUMMARY. + * + * @var string + */ + protected $format = '%1$s\'s Birthday'; + + /** + * Creates the generator. + * + * Check the setTimeRange and setObjects methods for details about the + * arguments. + * + * @param mixed $objects + */ + function __construct($objects = null) { + + if ($objects) { + $this->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 + * + * @return void + */ + 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 + * + * @return void + */ + function setFormat($format) { + + $this->format = $format; + + } + + /** + * Parses the input data and returns a VCALENDAR. + * + * @return Component/VCalendar + */ + 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/htdocs/includes/sabre/sabre/vobject/lib/Cli.php b/htdocs/includes/sabre/sabre/vobject/lib/Cli.php new file mode 100644 index 00000000000..df7ac22f3cc --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Cli.php @@ -0,0 +1,771 @@ +<?php + +namespace Sabre\VObject; + +use + InvalidArgumentException; + +/** + * This is the CLI interface for sabre-vobject. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Cli { + + /** + * No output. + * + * @var bool + */ + protected $quiet = false; + + /** + * Help display. + * + * @var bool + */ + protected $showHelp = false; + + /** + * Wether to spit out 'mimedir' or 'json' format. + * + * @var string + */ + protected $format; + + /** + * JSON pretty print. + * + * @var bool + */ + protected $pretty; + + /** + * Source file. + * + * @var string + */ + protected $inputPath; + + /** + * Destination file. + * + * @var string + */ + protected $outputPath; + + /** + * output stream. + * + * @var resource + */ + protected $stdout; + + /** + * stdin. + * + * @var resource + */ + protected $stdin; + + /** + * stderr. + * + * @var resource + */ + protected $stderr; + + /** + * Input format (one of json or mimedir). + * + * @var string + */ + protected $inputFormat; + + /** + * Makes the parser less strict. + * + * @var bool + */ + protected $forgiving = false; + + /** + * Main function. + * + * @return int + */ + function main(array $argv) { + + // @codeCoverageIgnoreStart + // We cannot easily test this, so we'll skip it. Pretty basic anyway. + + if (!$this->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 (count($positional) === 0) { + $this->showHelp(); + return 1; + } + + if (count($positional) === 1) { + 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('Uknown 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 (substr($this->inputPath, -5) === '.json') { + $this->inputFormat = 'json'; + } else { + $this->inputFormat = 'mimedir'; + } + } + if (!$this->format) { + if (substr($this->outputPath, -5) === '.json') { + $this->format = 'json'; + } else { + $this->format = 'mimedir'; + } + } + + + $realCode = 0; + + try { + + while ($input = $this->readInput()) { + + $returnCode = $this->$command($input); + if ($returnCode !== 0) $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. + * + * @return void + */ + 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 debbugging.'); + $this->log( + <<<HELP + +If source_file is set as '-', STDIN will be used. +If output_file is omitted, STDOUT will be used. +All other output is sent to STDERR. + +HELP + ); + + $this->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. + * + * @param Component $vObj + * + * @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. + * + * @param Component $vObj + * + * @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 ($vObj->name === 'VCARD') { + $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 + * + * @return int + */ + protected function color($vObj) { + + fwrite($this->stdout, $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 + * + * @return void + */ + 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 accomodate 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 ($array[$key]->name === 'VTIMEZONE') { + $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 ($array[$key]->name === 'VERSION') { + $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. + * + * @param Property $property + * + * @return void + */ + 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. + * + * @param array $argv + * + * @return void + */ + protected function parseArguments(array $argv) { + + $positional = []; + $options = []; + + for ($ii = 0; $ii < count($argv); $ii++) { + + // Skipping the first argument. + if ($ii === 0) 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 ($this->inputFormat === 'mimedir') { + $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 + * + * @return void + */ + protected function log($msg, $color = 'default') { + + if (!$this->quiet) { + if ($color !== 'default') { + $msg = $this->colorize($color, $msg); + } + fwrite($this->stderr, $msg . "\n"); + } + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Component.php b/htdocs/includes/sabre/sabre/vobject/lib/Component.php new file mode 100644 index 00000000000..9a10ed3f8c7 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Component.php @@ -0,0 +1,700 @@ +<?php + +namespace Sabre\VObject; + +use Sabre\Xml; + +/** + * Component. + * + * A component represents a group of properties, such as VCALENDAR, VEVENT, or + * VCARD. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Component extends Node { + + /** + * Component name. + * + * This will contain a string such as VEVENT, VTODO, VCALENDAR, VCARD. + * + * @var string + */ + public $name; + + /** + * A list of properties and/or sub-components. + * + * @var array + */ + 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 Document $root + * @param string $name such as VCALENDAR, VEVENT. + * @param array $children + * @param bool $defaults + * + * @return void + */ + function __construct(Document $root, $name, array $children = [], $defaults = true) { + + $this->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 + */ + 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 + * @return void + */ + 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 (strpos($item, '.') === false) { + 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 + */ + 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 + */ + 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 + */ + function select($name) { + + $group = null; + $name = strtoupper($name); + if (strpos($name, '.') !== false) { + 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 && 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 && strtoupper($child->group) === $group) { + $result[] = $child; + } + + } + + } + return $result; + + } + + /** + * Turns the object back into a serialized blob. + * + * @return string + */ + 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 accomodate 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 ($array[$key]->name === 'VTIMEZONE') { + $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 ($array[$key]->name === 'VERSION') { + $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 + */ + 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. + * + * @return void + */ + function xmlSerialize(Xml\Writer $writer) { + + $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 + */ + function __get($name) { + + if ($name === 'children') { + + 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 (count($matches) === 0) { + 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 + */ + 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 + * + * @return void + */ + 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 + * + * @return void + */ + 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. + * + * @return void + */ + 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 + */ + 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 + */ + 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]) || $propertyCounters[$propName] !== 1) { + $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) { + $messages[] = [ + 'level' => 3, + '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. + * + * @return void + */ + function destroy() { + + parent::destroy(); + foreach ($this->children as $childGroup) { + foreach ($childGroup as $child) { + $child->destroy(); + } + } + $this->children = []; + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Component/Available.php b/htdocs/includes/sabre/sabre/vobject/lib/Component/Available.php new file mode 100644 index 00000000000..b3aaf08afad --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Component/Available.php @@ -0,0 +1,126 @@ +<?php + +namespace Sabre\VObject\Component; + +use Sabre\VObject; + +/** + * The Available sub-component. + * + * This component adds functionality to a component, specific for AVAILABLE + * components. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Ivan Enderlin + * @license http://sabre.io/license/ Modified BSD License + */ +class Available extends VObject\Component { + + /** + * 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 + */ + function getEffectiveStartEnd() { + + $effectiveStart = $this->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 + */ + 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 + */ + 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/htdocs/includes/sabre/sabre/vobject/lib/Component/VAlarm.php b/htdocs/includes/sabre/sabre/vobject/lib/Component/VAlarm.php new file mode 100644 index 00000000000..faa8a5e74be --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Component/VAlarm.php @@ -0,0 +1,142 @@ +<?php + +namespace Sabre\VObject\Component; + +use DateTimeImmutable; +use DateTimeInterface; +use Sabre\VObject; +use Sabre\VObject\InvalidDataException; + +/** + * VAlarm component. + * + * This component contains some additional functionality specific for VALARMs. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class VAlarm extends VObject\Component { + + /** + * Returns a DateTime object when this alarm is going to trigger. + * + * This ignores repeated alarm, only the first trigger is returned. + * + * @return DateTimeImmutable + */ + function getEffectiveTriggerTime() { + + $trigger = $this->TRIGGER; + if (!isset($trigger['VALUE']) || strtoupper($trigger['VALUE']) === 'DURATION') { + $triggerDuration = VObject\DateTimeParser::parseDuration($this->TRIGGER); + $related = (isset($trigger['RELATED']) && strtoupper($trigger['RELATED']) == 'END') ? 'END' : 'START'; + + $parentComponent = $this->parent; + if ($related === 'START') { + + if ($parentComponent->name === 'VTODO') { + $propName = 'DUE'; + } else { + $propName = 'DTSTART'; + } + + $effectiveTrigger = $parentComponent->$propName->getDateTime(); + $effectiveTrigger = $effectiveTrigger->add($triggerDuration); + } else { + if ($parentComponent->name === 'VTODO') { + $endProp = 'DUE'; + } elseif ($parentComponent->name === 'VEVENT') { + $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 + */ + 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 + */ + function getValidationRules() { + + return [ + 'ACTION' => 1, + 'TRIGGER' => 1, + + 'DURATION' => '?', + 'REPEAT' => '?', + + 'ATTACH' => '?', + ]; + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Component/VAvailability.php b/htdocs/includes/sabre/sabre/vobject/lib/Component/VAvailability.php new file mode 100644 index 00000000000..66b3310c5e8 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Component/VAvailability.php @@ -0,0 +1,156 @@ +<?php + +namespace Sabre\VObject\Component; + +use DateTimeInterface; +use Sabre\VObject; + +/** + * The VAvailability component. + * + * This component adds functionality to a component, specific for VAVAILABILITY + * components. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Ivan Enderlin + * @license http://sabre.io/license/ Modified BSD License + */ +class VAvailability extends VObject\Component { + + /** + * 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: + * + * https://tools.ietf.org/html/draft-daboo-calendar-availability-05#section-3.1 + * + * @param DateTimeInterface $start + * @param DateTimeInterface $end + * + * @return bool + */ + function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end) { + + list($effectiveStart, $effectiveEnd) = $this->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 + */ + 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 + */ + 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 + */ + 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/htdocs/includes/sabre/sabre/vobject/lib/Component/VCalendar.php b/htdocs/includes/sabre/sabre/vobject/lib/Component/VCalendar.php new file mode 100644 index 00000000000..1b3137d38eb --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Component/VCalendar.php @@ -0,0 +1,561 @@ +<?php + +namespace Sabre\VObject\Component; + +use DateTimeInterface; +use DateTimeZone; +use Sabre\VObject; +use Sabre\VObject\Component; +use Sabre\VObject\InvalidDataException; +use Sabre\VObject\Property; +use Sabre\VObject\Recur\EventIterator; +use Sabre\VObject\Recur\NoInstancesException; + +/** + * The VCalendar component. + * + * This component adds functionality to a component, specific for a VCALENDAR. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class VCalendar extends VObject\Document { + + /** + * The default name for this component. + * + * This should be 'VCALENDAR' or 'VCARD'. + * + * @var string + */ + static $defaultName = 'VCALENDAR'; + + /** + * This is a list of components, and which classes they should map to. + * + * @var array + */ + static $componentMap = [ + 'VCALENDAR' => 'Sabre\\VObject\\Component\\VCalendar', + 'VALARM' => 'Sabre\\VObject\\Component\\VAlarm', + 'VEVENT' => 'Sabre\\VObject\\Component\\VEvent', + 'VFREEBUSY' => 'Sabre\\VObject\\Component\\VFreeBusy', + 'VAVAILABILITY' => 'Sabre\\VObject\\Component\\VAvailability', + 'AVAILABLE' => 'Sabre\\VObject\\Component\\Available', + 'VJOURNAL' => 'Sabre\\VObject\\Component\\VJournal', + 'VTIMEZONE' => 'Sabre\\VObject\\Component\\VTimeZone', + 'VTODO' => 'Sabre\\VObject\\Component\\VTodo', + ]; + + /** + * List of value-types, and which classes they map to. + * + * @var array + */ + static $valueMap = [ + 'BINARY' => 'Sabre\\VObject\\Property\\Binary', + 'BOOLEAN' => 'Sabre\\VObject\\Property\\Boolean', + 'CAL-ADDRESS' => 'Sabre\\VObject\\Property\\ICalendar\\CalAddress', + 'DATE' => 'Sabre\\VObject\\Property\\ICalendar\\Date', + 'DATE-TIME' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', + 'DURATION' => 'Sabre\\VObject\\Property\\ICalendar\\Duration', + 'FLOAT' => 'Sabre\\VObject\\Property\\FloatValue', + 'INTEGER' => 'Sabre\\VObject\\Property\\IntegerValue', + 'PERIOD' => 'Sabre\\VObject\\Property\\ICalendar\\Period', + 'RECUR' => 'Sabre\\VObject\\Property\\ICalendar\\Recur', + 'TEXT' => 'Sabre\\VObject\\Property\\Text', + 'TIME' => 'Sabre\\VObject\\Property\\Time', + 'UNKNOWN' => 'Sabre\\VObject\\Property\\Unknown', // jCard / jCal-only. + 'URI' => 'Sabre\\VObject\\Property\\Uri', + 'UTC-OFFSET' => 'Sabre\\VObject\\Property\\UtcOffset', + ]; + + /** + * List of properties, and which classes they map to. + * + * @var array + */ + static $propertyMap = [ + // Calendar properties + 'CALSCALE' => 'Sabre\\VObject\\Property\\FlatText', + 'METHOD' => 'Sabre\\VObject\\Property\\FlatText', + 'PRODID' => 'Sabre\\VObject\\Property\\FlatText', + 'VERSION' => 'Sabre\\VObject\\Property\\FlatText', + + // Component properties + 'ATTACH' => 'Sabre\\VObject\\Property\\Uri', + 'CATEGORIES' => 'Sabre\\VObject\\Property\\Text', + 'CLASS' => 'Sabre\\VObject\\Property\\FlatText', + 'COMMENT' => 'Sabre\\VObject\\Property\\FlatText', + 'DESCRIPTION' => 'Sabre\\VObject\\Property\\FlatText', + 'GEO' => 'Sabre\\VObject\\Property\\FloatValue', + 'LOCATION' => 'Sabre\\VObject\\Property\\FlatText', + 'PERCENT-COMPLETE' => 'Sabre\\VObject\\Property\\IntegerValue', + 'PRIORITY' => 'Sabre\\VObject\\Property\\IntegerValue', + 'RESOURCES' => 'Sabre\\VObject\\Property\\Text', + 'STATUS' => 'Sabre\\VObject\\Property\\FlatText', + 'SUMMARY' => 'Sabre\\VObject\\Property\\FlatText', + + // Date and Time Component Properties + 'COMPLETED' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', + 'DTEND' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', + 'DUE' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', + 'DTSTART' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', + 'DURATION' => 'Sabre\\VObject\\Property\\ICalendar\\Duration', + 'FREEBUSY' => 'Sabre\\VObject\\Property\\ICalendar\\Period', + 'TRANSP' => 'Sabre\\VObject\\Property\\FlatText', + + // Time Zone Component Properties + 'TZID' => 'Sabre\\VObject\\Property\\FlatText', + 'TZNAME' => 'Sabre\\VObject\\Property\\FlatText', + 'TZOFFSETFROM' => 'Sabre\\VObject\\Property\\UtcOffset', + 'TZOFFSETTO' => 'Sabre\\VObject\\Property\\UtcOffset', + 'TZURL' => 'Sabre\\VObject\\Property\\Uri', + + // Relationship Component Properties + 'ATTENDEE' => 'Sabre\\VObject\\Property\\ICalendar\\CalAddress', + 'CONTACT' => 'Sabre\\VObject\\Property\\FlatText', + 'ORGANIZER' => 'Sabre\\VObject\\Property\\ICalendar\\CalAddress', + 'RECURRENCE-ID' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', + 'RELATED-TO' => 'Sabre\\VObject\\Property\\FlatText', + 'URL' => 'Sabre\\VObject\\Property\\Uri', + 'UID' => 'Sabre\\VObject\\Property\\FlatText', + + // Recurrence Component Properties + 'EXDATE' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', + 'RDATE' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', + 'RRULE' => 'Sabre\\VObject\\Property\\ICalendar\\Recur', + 'EXRULE' => 'Sabre\\VObject\\Property\\ICalendar\\Recur', // Deprecated since rfc5545 + + // Alarm Component Properties + 'ACTION' => 'Sabre\\VObject\\Property\\FlatText', + 'REPEAT' => 'Sabre\\VObject\\Property\\IntegerValue', + 'TRIGGER' => 'Sabre\\VObject\\Property\\ICalendar\\Duration', + + // Change Management Component Properties + 'CREATED' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', + 'DTSTAMP' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', + 'LAST-MODIFIED' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', + 'SEQUENCE' => 'Sabre\\VObject\\Property\\IntegerValue', + + // Request Status + 'REQUEST-STATUS' => 'Sabre\\VObject\\Property\\Text', + + // Additions from draft-daboo-valarm-extensions-04 + 'ALARM-AGENT' => 'Sabre\\VObject\\Property\\Text', + 'ACKNOWLEDGED' => 'Sabre\\VObject\\Property\\ICalendar\\DateTime', + 'PROXIMITY' => 'Sabre\\VObject\\Property\\Text', + 'DEFAULT-ALARM' => 'Sabre\\VObject\\Property\\Boolean', + + // Additions from draft-daboo-calendar-availability-05 + 'BUSYTYPE' => 'Sabre\\VObject\\Property\\Text', + + ]; + + /** + * Returns the current document type. + * + * @return int + */ + 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[] + */ + function getBaseComponents($componentName = null) { + + $isBaseComponent = function($component) { + + if (!$component instanceof VObject\Component) { + return false; + } + if ($component->name === 'VTIMEZONE') { + 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 + */ + function getBaseComponent($componentName = null) { + + $isBaseComponent = function($component) { + + if (!$component instanceof VObject\Component) { + return false; + } + if ($component->name === 'VTIMEZONE') { + 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 it's 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 DateTimeInterface $start + * @param DateTimeInterface $end + * @param DateTimeZone $timeZone reference timezone for floating dates and + * times. + * @return VCalendar + */ + 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 && $child->name !== 'PRODID') { + // We explictly want to ignore PRODID, because we want to + // overwrite it with our own. + $newChildren[] = clone $child; + } elseif ($child instanceof Component && $child->name !== 'VTIMEZONE') { + + // We're also stripping all VTIMEZONE objects because we're + // converting everything to UTC. + if ($child->name === 'VEVENT' && (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 ($child->name === 'VEVENT' && $child->isInTimeRange($start, $end)) { + $newChildren[] = $stripTimezones(clone $child); + } + + } + + } + + foreach ($recurringEvents as $events) { + + try { + $it = new EventIterator($events, $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 + */ + 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 + */ + function validate($options = 0) { + + $warnings = parent::validate($options); + + if ($ver = $this->VERSION) { + if ((string)$ver !== '2.0') { + $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 ($componentsFound === 0) { + $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 (count($componentTypes) === 0) { + $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 + */ + 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/htdocs/includes/sabre/sabre/vobject/lib/Component/VCard.php b/htdocs/includes/sabre/sabre/vobject/lib/Component/VCard.php new file mode 100644 index 00000000000..4f620de1070 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Component/VCard.php @@ -0,0 +1,553 @@ +<?php + +namespace Sabre\VObject\Component; + +use Sabre\VObject; +use Sabre\Xml; + +/** + * The VCard component. + * + * This component represents the BEGIN:VCARD and END:VCARD found in every + * vcard. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class VCard extends VObject\Document { + + /** + * The default name for this component. + * + * This should be 'VCALENDAR' or 'VCARD'. + * + * @var string + */ + static $defaultName = 'VCARD'; + + /** + * Caching the version number. + * + * @var int + */ + private $version = null; + + /** + * This is a list of components, and which classes they should map to. + * + * @var array + */ + static $componentMap = [ + 'VCARD' => 'Sabre\\VObject\\Component\\VCard', + ]; + + /** + * List of value-types, and which classes they map to. + * + * @var array + */ + static $valueMap = [ + 'BINARY' => 'Sabre\\VObject\\Property\\Binary', + 'BOOLEAN' => 'Sabre\\VObject\\Property\\Boolean', + 'CONTENT-ID' => 'Sabre\\VObject\\Property\\FlatText', // vCard 2.1 only + 'DATE' => 'Sabre\\VObject\\Property\\VCard\\Date', + 'DATE-TIME' => 'Sabre\\VObject\\Property\\VCard\\DateTime', + 'DATE-AND-OR-TIME' => 'Sabre\\VObject\\Property\\VCard\\DateAndOrTime', // vCard only + 'FLOAT' => 'Sabre\\VObject\\Property\\FloatValue', + 'INTEGER' => 'Sabre\\VObject\\Property\\IntegerValue', + 'LANGUAGE-TAG' => 'Sabre\\VObject\\Property\\VCard\\LanguageTag', + 'TIMESTAMP' => 'Sabre\\VObject\\Property\\VCard\\TimeStamp', + 'TEXT' => 'Sabre\\VObject\\Property\\Text', + 'TIME' => 'Sabre\\VObject\\Property\\Time', + 'UNKNOWN' => 'Sabre\\VObject\\Property\\Unknown', // jCard / jCal-only. + 'URI' => 'Sabre\\VObject\\Property\\Uri', + 'URL' => 'Sabre\\VObject\\Property\\Uri', // vCard 2.1 only + 'UTC-OFFSET' => 'Sabre\\VObject\\Property\\UtcOffset', + ]; + + /** + * List of properties, and which classes they map to. + * + * @var array + */ + static $propertyMap = [ + + // vCard 2.1 properties and up + 'N' => 'Sabre\\VObject\\Property\\Text', + 'FN' => 'Sabre\\VObject\\Property\\FlatText', + 'PHOTO' => 'Sabre\\VObject\\Property\\Binary', + 'BDAY' => 'Sabre\\VObject\\Property\\VCard\\DateAndOrTime', + 'ADR' => 'Sabre\\VObject\\Property\\Text', + 'LABEL' => 'Sabre\\VObject\\Property\\FlatText', // Removed in vCard 4.0 + 'TEL' => 'Sabre\\VObject\\Property\\FlatText', + 'EMAIL' => 'Sabre\\VObject\\Property\\FlatText', + 'MAILER' => 'Sabre\\VObject\\Property\\FlatText', // Removed in vCard 4.0 + 'GEO' => 'Sabre\\VObject\\Property\\FlatText', + 'TITLE' => 'Sabre\\VObject\\Property\\FlatText', + 'ROLE' => 'Sabre\\VObject\\Property\\FlatText', + 'LOGO' => 'Sabre\\VObject\\Property\\Binary', + // 'AGENT' => 'Sabre\\VObject\\Property\\', // Todo: is an embedded vCard. Probably rare, so + // not supported at the moment + 'ORG' => 'Sabre\\VObject\\Property\\Text', + 'NOTE' => 'Sabre\\VObject\\Property\\FlatText', + 'REV' => 'Sabre\\VObject\\Property\\VCard\\TimeStamp', + 'SOUND' => 'Sabre\\VObject\\Property\\FlatText', + 'URL' => 'Sabre\\VObject\\Property\\Uri', + 'UID' => 'Sabre\\VObject\\Property\\FlatText', + 'VERSION' => 'Sabre\\VObject\\Property\\FlatText', + 'KEY' => 'Sabre\\VObject\\Property\\FlatText', + 'TZ' => 'Sabre\\VObject\\Property\\Text', + + // vCard 3.0 properties + 'CATEGORIES' => 'Sabre\\VObject\\Property\\Text', + 'SORT-STRING' => 'Sabre\\VObject\\Property\\FlatText', + 'PRODID' => 'Sabre\\VObject\\Property\\FlatText', + 'NICKNAME' => 'Sabre\\VObject\\Property\\Text', + 'CLASS' => 'Sabre\\VObject\\Property\\FlatText', // Removed in vCard 4.0 + + // rfc2739 properties + 'FBURL' => 'Sabre\\VObject\\Property\\Uri', + 'CAPURI' => 'Sabre\\VObject\\Property\\Uri', + 'CALURI' => 'Sabre\\VObject\\Property\\Uri', + 'CALADRURI' => 'Sabre\\VObject\\Property\\Uri', + + // rfc4770 properties + 'IMPP' => 'Sabre\\VObject\\Property\\Uri', + + // vCard 4.0 properties + 'SOURCE' => 'Sabre\\VObject\\Property\\Uri', + 'XML' => 'Sabre\\VObject\\Property\\FlatText', + 'ANNIVERSARY' => 'Sabre\\VObject\\Property\\VCard\\DateAndOrTime', + 'CLIENTPIDMAP' => 'Sabre\\VObject\\Property\\Text', + 'LANG' => 'Sabre\\VObject\\Property\\VCard\\LanguageTag', + 'GENDER' => 'Sabre\\VObject\\Property\\Text', + 'KIND' => 'Sabre\\VObject\\Property\\FlatText', + 'MEMBER' => 'Sabre\\VObject\\Property\\Uri', + 'RELATED' => 'Sabre\\VObject\\Property\\Uri', + + // rfc6474 properties + 'BIRTHPLACE' => 'Sabre\\VObject\\Property\\FlatText', + 'DEATHPLACE' => 'Sabre\\VObject\\Property\\FlatText', + 'DEATHDATE' => 'Sabre\\VObject\\Property\\VCard\\DateAndOrTime', + + // rfc6715 properties + 'EXPERTISE' => 'Sabre\\VObject\\Property\\FlatText', + 'HOBBY' => 'Sabre\\VObject\\Property\\FlatText', + 'INTEREST' => 'Sabre\\VObject\\Property\\FlatText', + 'ORG-DIRECTORY' => 'Sabre\\VObject\\Property\\FlatText', + + ]; + + /** + * Returns the current document type. + * + * @return int + */ + 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 + */ + 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 + */ + function validate($options = 0) { + + $warnings = []; + + $versionMap = [ + self::VCARD21 => '2.1', + self::VCARD30 => '3.0', + self::VCARD40 => '4.0', + ]; + + $version = $this->select('VERSION'); + if (count($version) === 1) { + $version = (string)$this->VERSION; + if ($version !== '2.1' && $version !== '3.0' && $version !== '4.0') { + $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 ($version === '2.1' && ($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 (count($uid) === 0) { + 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 (count($fn) !== 1) { + + $repaired = false; + if (($options & self::REPAIR) && count($fn) === 0) { + // 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; + } + + } + $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 + */ + 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 wether 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 + */ + 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 + */ + 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 + */ + 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. + * + * @return void + */ + function xmlSerialize(Xml\Writer $writer) { + + $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': + continue; + + 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 + */ + function getClassNameForPropertyName($propertyName) { + + $className = parent::getClassNameForPropertyName($propertyName); + + // In vCard 4, BINARY no longer exists, and we need URI instead. + if ($className == 'Sabre\\VObject\\Property\\Binary' && $this->getDocumentType() === self::VCARD40) { + return 'Sabre\\VObject\\Property\\Uri'; + } + return $className; + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Component/VEvent.php b/htdocs/includes/sabre/sabre/vobject/lib/Component/VEvent.php new file mode 100644 index 00000000000..7f686119004 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Component/VEvent.php @@ -0,0 +1,153 @@ +<?php + +namespace Sabre\VObject\Component; + +use DateTimeInterface; +use Sabre\VObject; +use Sabre\VObject\Recur\EventIterator; +use Sabre\VObject\Recur\NoInstancesException; + +/** + * VEvent component. + * + * This component contains some additional functionality specific for VEVENT's. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class VEvent extends VObject\Component { + + /** + * 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 DateTimeInterface $start + * @param DateTimeInterface $end + * + * @return bool + */ + function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end) { + + if ($this->RRULE) { + + try { + + $it = new EventIterator($this, null, $start->getTimezone()); + + } catch (NoInstancesException $e) { + + // If we've catched 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' => date('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 + */ + 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/htdocs/includes/sabre/sabre/vobject/lib/Component/VFreeBusy.php b/htdocs/includes/sabre/sabre/vobject/lib/Component/VFreeBusy.php new file mode 100644 index 00000000000..72294cc9f3b --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Component/VFreeBusy.php @@ -0,0 +1,102 @@ +<?php + +namespace Sabre\VObject\Component; + +use DateTimeInterface; +use Sabre\VObject; + +/** + * The VFreeBusy component. + * + * This component adds functionality to a component, specific for VFREEBUSY + * components. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class VFreeBusy extends VObject\Component { + + /** + * Checks based on the contained FREEBUSY information, if a timeslot is + * available. + * + * @param DateTimeInterface $start + * @param DateTimeInterface $end + * + * @return bool + */ + function isFree(DateTimeInterface $start, DatetimeInterface $end) { + + foreach ($this->select('FREEBUSY') as $freebusy) { + + // We are only interested in FBTYPE=BUSY (the default), + // FBTYPE=BUSY-TENTATIVE or FBTYPE=BUSY-UNAVAILABLE. + if (isset($freebusy['FBTYPE']) && strtoupper(substr((string)$freebusy['FBTYPE'], 0, 4)) !== 'BUSY') { + 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 + */ + function getValidationRules() { + + return [ + 'UID' => 1, + 'DTSTAMP' => 1, + + 'CONTACT' => '?', + 'DTSTART' => '?', + 'DTEND' => '?', + 'ORGANIZER' => '?', + 'URL' => '?', + + 'ATTENDEE' => '*', + 'COMMENT' => '*', + 'FREEBUSY' => '*', + 'REQUEST-STATUS' => '*', + ]; + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Component/VJournal.php b/htdocs/includes/sabre/sabre/vobject/lib/Component/VJournal.php new file mode 100644 index 00000000000..a1b1a863d2d --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Component/VJournal.php @@ -0,0 +1,107 @@ +<?php + +namespace Sabre\VObject\Component; + +use DateTimeInterface; +use Sabre\VObject; + +/** + * VJournal component. + * + * This component contains some additional functionality specific for VJOURNALs. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class VJournal extends VObject\Component { + + /** + * 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 DateTimeInterface $start + * @param DateTimeInterface $end + * + * @return bool + */ + function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end) { + + $dtstart = isset($this->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 + */ + 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' => date('Ymd\\THis\\Z'), + ]; + + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Component/VTimeZone.php b/htdocs/includes/sabre/sabre/vobject/lib/Component/VTimeZone.php new file mode 100644 index 00000000000..f6eb6cba18a --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Component/VTimeZone.php @@ -0,0 +1,66 @@ +<?php + +namespace Sabre\VObject\Component; + +use Sabre\VObject; + +/** + * The VTimeZone component. + * + * This component adds functionality to a component, specific for VTIMEZONE + * components. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class VTimeZone extends VObject\Component { + + /** + * Returns the PHP DateTimeZone for this VTIMEZONE component. + * + * If we can't accurately determine the timezone, this method will return + * UTC. + * + * @return \DateTimeZone + */ + function getTimeZone() { + + return VObject\TimeZoneUtil::getTimeZone((string)$this->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 + */ + 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/htdocs/includes/sabre/sabre/vobject/lib/Component/VTodo.php b/htdocs/includes/sabre/sabre/vobject/lib/Component/VTodo.php new file mode 100644 index 00000000000..144ced694ac --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Component/VTodo.php @@ -0,0 +1,193 @@ +<?php + +namespace Sabre\VObject\Component; + +use DateTimeInterface; +use Sabre\VObject; + +/** + * VTodo component. + * + * This component contains some additional functionality specific for VTODOs. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class VTodo extends VObject\Component { + + /** + * 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 DateTimeInterface $start + * @param DateTimeInterface $end + * + * @return bool + */ + function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end) { + + $dtstart = isset($this->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 + */ + 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 + */ + 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/htdocs/includes/sabre/sabre/vobject/lib/DateTimeParser.php b/htdocs/includes/sabre/sabre/vobject/lib/DateTimeParser.php new file mode 100644 index 00000000000..f9a802d2533 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/DateTimeParser.php @@ -0,0 +1,580 @@ +<?php + +namespace Sabre\VObject; + +use DateInterval; +use DateTimeImmutable; +use DateTimeZone; + +/** + * DateTimeParser. + * + * This class is responsible for parsing the several different date and time + * formats iCalendar and vCards have. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class DateTimeParser { + + /** + * Parses an iCalendar (rfc5545) formatted datetime and returns a + * DateTimeImmutable object. + * + * Specifying a reference timezone is optional. It will only be used + * if the non-UTC format is used. The argument is used as a reference, the + * returned DateTimeImmutable object will still be in the UTC timezone. + * + * @param string $dt + * @param DateTimeZone $tz + * + * @return DateTimeImmutable + */ + static function parseDateTime($dt, DateTimeZone $tz = null) { + + // Format is YYYYMMDD + "T" + hhmmss + $result = preg_match('/^([0-9]{4})([0-1][0-9])([0-3][0-9])T([0-2][0-9])([0-5][0-9])([0-5][0-9])([Z]?)$/', $dt, $matches); + + if (!$result) { + throw new InvalidDataException('The supplied iCalendar datetime value is incorrect: ' . $dt); + } + + if ($matches[7] === 'Z' || is_null($tz)) { + $tz = new DateTimeZone('UTC'); + } + + try { + $date = new DateTimeImmutable($matches[1] . '-' . $matches[2] . '-' . $matches[3] . ' ' . $matches[4] . ':' . $matches[5] . ':' . $matches[6], $tz); + } catch (\Exception $e) { + throw new InvalidDataException('The supplied iCalendar datetime value is incorrect: ' . $dt); + } + + return $date; + + } + + /** + * Parses an iCalendar (rfc5545) formatted date and returns a DateTimeImmutable object. + * + * @param string $date + * @param DateTimeZone $tz + * + * @return DateTimeImmutable + */ + static function parseDate($date, DateTimeZone $tz = null) { + + // Format is YYYYMMDD + $result = preg_match('/^([0-9]{4})([0-1][0-9])([0-3][0-9])$/', $date, $matches); + + if (!$result) { + throw new InvalidDataException('The supplied iCalendar date value is incorrect: ' . $date); + } + + if (is_null($tz)) { + $tz = new DateTimeZone('UTC'); + } + + try { + $date = new DateTimeImmutable($matches[1] . '-' . $matches[2] . '-' . $matches[3], $tz); + } catch (\Exception $e) { + throw new InvalidDataException('The supplied iCalendar date value is incorrect: ' . $date); + } + + return $date; + + } + + /** + * Parses an iCalendar (RFC5545) formatted duration value. + * + * This method will either return a DateTimeInterval object, or a string + * suitable for strtotime or DateTime::modify. + * + * @param string $duration + * @param bool $asString + * + * @return DateInterval|string + */ + static function parseDuration($duration, $asString = false) { + + $result = preg_match('/^(?<plusminus>\+|-)?P((?<week>\d+)W)?((?<day>\d+)D)?(T((?<hour>\d+)H)?((?<minute>\d+)M)?((?<second>\d+)S)?)?$/', $duration, $matches); + if (!$result) { + throw new InvalidDataException('The supplied iCalendar duration value is incorrect: ' . $duration); + } + + if (!$asString) { + + $invert = false; + + if ($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 ($duration === 'P') { + $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 + */ + static function parse($date, $referenceTz = null) { + + if ($date[0] === 'P' || ($date[0] === '-' && $date[1] === 'P')) { + return self::parseDuration($date); + } elseif (strlen($date) === 8) { + 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 + */ + static function parseVCardDateTime($date) { + + $regex = '/^ + (?: # date part + (?: + (?: (?<year> [0-9]{4}) (?: -)?| --) + (?<month> [0-9]{2})? + |---) + (?<date> [0-9]{2})? + )? + (?:T # time part + (?<hour> [0-9]{2} | -) + (?<minute> [0-9]{2} | -)? + (?<second> [0-9]{2})? + + (?: \.[0-9]{3})? # milliseconds + (?P<timezone> # timezone offset + + Z | (?: \+|-)(?: [0-9]{4}) + + )? + + )? + $/x'; + + if (!preg_match($regex, $date, $matches)) { + + // Attempting to parse the extended format. + $regex = '/^ + (?: # date part + (?: (?<year> [0-9]{4}) - | -- ) + (?<month> [0-9]{2}) - + (?<date> [0-9]{2}) + )? + (?:T # time part + + (?: (?<hour> [0-9]{2}) : | -) + (?: (?<minute> [0-9]{2}) : | -)? + (?<second> [0-9]{2})? + + (?: \.[0-9]{3})? # milliseconds + (?P<timezone> # 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 + */ + static function parseVCardTime($date) { + + $regex = '/^ + (?<hour> [0-9]{2} | -) + (?<minute> [0-9]{2} | -)? + (?<second> [0-9]{2})? + + (?: \.[0-9]{3})? # milliseconds + (?P<timezone> # timezone offset + + Z | (?: \+|-)(?: [0-9]{4}) + + )? + $/x'; + + + if (!preg_match($regex, $date, $matches)) { + + // Attempting to parse the extended format. + $regex = '/^ + (?: (?<hour> [0-9]{2}) : | -) + (?: (?<minute> [0-9]{2}) : | -)? + (?<second> [0-9]{2})? + + (?: \.[0-9]{3})? # milliseconds + (?P<timezone> # 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 + */ + static function parseVCardDateAndOrTime($date) { + + // \d{8}|\d{4}-\d\d|--\d\d(\d\d)?|---\d\d + $valueDate = '/^(?J)(?:' . + '(?<year>\d{4})(?<month>\d\d)(?<date>\d\d)' . + '|(?<year>\d{4})-(?<month>\d\d)' . + '|--(?<month>\d\d)(?<date>\d\d)?' . + '|---(?<date>\d\d)' . + ')$/'; + + // (\d\d(\d\d(\d\d)?)?|-\d\d(\d\d)?|--\d\d)(Z|[+\-]\d\d(\d\d)?)? + $valueTime = '/^(?J)(?:' . + '((?<hour>\d\d)((?<minute>\d\d)(?<second>\d\d)?)?' . + '|-(?<minute>\d\d)(?<second>\d\d)?' . + '|--(?<second>\d\d))' . + '(?<timezone>(Z|[+\-]\d\d(\d\d)?))?' . + ')$/'; + + // (\d{8}|--\d{4}|---\d\d)T\d\d(\d\d(\d\d)?)?(Z|[+\-]\d\d(\d\d?)? + $valueDateTime = '/^(?:' . + '((?<year0>\d{4})(?<month0>\d\d)(?<date0>\d\d)' . + '|--(?<month1>\d\d)(?<date1>\d\d)' . + '|---(?<date2>\d\d))' . + 'T' . + '(?<hour>\d\d)((?<minute>\d\d)(?<second>\d\d)?)?' . + '(?<timezone>(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/htdocs/includes/sabre/sabre/vobject/lib/Document.php b/htdocs/includes/sabre/sabre/vobject/lib/Document.php new file mode 100644 index 00000000000..03252ab06a2 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Document.php @@ -0,0 +1,270 @@ +<?php + +namespace Sabre\VObject; + +/** + * Document. + * + * A document is just like a component, except that it's also the top level + * element. + * + * Both a VCALENDAR and a VCARD are considered documents. + * + * This class also provides a registry for document types. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +abstract class Document extends Component { + + /** + * Unknown document type. + */ + const UNKNOWN = 1; + + /** + * vCalendar 1.0. + */ + const VCALENDAR10 = 2; + + /** + * iCalendar 2.0. + */ + const ICALENDAR20 = 3; + + /** + * vCard 2.1. + */ + const VCARD21 = 4; + + /** + * vCard 3.0. + */ + const VCARD30 = 5; + + /** + * vCard 4.0. + */ + const VCARD40 = 6; + + /** + * The default name for this component. + * + * This should be 'VCALENDAR' or 'VCARD'. + * + * @var string + */ + static $defaultName; + + /** + * List of properties, and which classes they map to. + * + * @var array + */ + static $propertyMap = []; + + /** + * List of components, along with which classes they map to. + * + * @var array + */ + static $componentMap = []; + + /** + * List of value-types, and which classes they map to. + * + * @var array + */ + static $valueMap = []; + + /** + * Creates a new document. + * + * We're changing the default behavior slightly here. First, we don't want + * to have to specify a name (we already know it), and we want to allow + * children to be specified in the first argument. + * + * But, the default behavior also works. + * + * So the two sigs: + * + * new Document(array $children = [], $defaults = true); + * new Document(string $name, array $children = [], $defaults = true) + * + * @return void + */ + function __construct() { + + $args = func_get_args(); + if (count($args) === 0 || is_array($args[0])) { + array_unshift($args, $this, static::$defaultName); + call_user_func_array(['parent', '__construct'], $args); + } else { + array_unshift($args, $this); + call_user_func_array(['parent', '__construct'], $args); + } + + } + + /** + * Returns the current document type. + * + * @return int + */ + function getDocumentType() { + + return self::UNKNOWN; + + } + + /** + * Creates a new component or property. + * + * If it's a known component, we will automatically call createComponent. + * otherwise, we'll assume it's a property and call createProperty instead. + * + * @param string $name + * @param string $arg1,... Unlimited number of args + * + * @return mixed + */ + function create($name) { + + if (isset(static::$componentMap[strtoupper($name)])) { + + return call_user_func_array([$this, 'createComponent'], func_get_args()); + + } else { + + return call_user_func_array([$this, 'createProperty'], func_get_args()); + + } + + } + + /** + * Creates a new component. + * + * This method automatically searches for the correct component class, based + * on its name. + * + * 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 $name + * @param array $children + * @param bool $defaults + * + * @return Component + */ + function createComponent($name, array $children = null, $defaults = true) { + + $name = strtoupper($name); + $class = 'Sabre\\VObject\\Component'; + + 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 + * + * @return Property + */ + function createProperty($name, $value = null, array $parameters = null, $valueType = null) { + + // If there's a . in the name, it means it's prefixed by a groupname. + if (($i = strpos($name, '.')) !== false) { + $group = substr($name, 0, $i); + $name = strtoupper(substr($name, $i + 1)); + } else { + $name = strtoupper($name); + $group = null; + } + + $class = null; + + if ($valueType) { + // The valueType argument comes first to figure out the correct + // class. + $class = $this->getClassNameForPropertyValue($valueType); + } + + if (is_null($class)) { + // If a VALUE parameter is supplied, we should use that. + if (isset($parameters['VALUE'])) { + $class = $this->getClassNameForPropertyValue($parameters['VALUE']); + if (is_null($class)) { + throw new InvalidDataException('Unsupported VALUE parameter for ' . $name . ' property. You supplied "' . $parameters['VALUE'] . '"'); + } + } + else { + $class = $this->getClassNameForPropertyName($name); + } + } + if (is_null($parameters)) $parameters = []; + + return new $class($this, $name, $value, $parameters, $group); + + } + + /** + * 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 + */ + 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 + */ + function getClassNameForPropertyName($propertyName) { + + if (isset(static::$propertyMap[$propertyName])) { + return static::$propertyMap[$propertyName]; + } else { + return 'Sabre\\VObject\\Property\\Unknown'; + } + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/ElementList.php b/htdocs/includes/sabre/sabre/vobject/lib/ElementList.php new file mode 100644 index 00000000000..95924924702 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/ElementList.php @@ -0,0 +1,54 @@ +<?php + +namespace Sabre\VObject; + +use ArrayIterator; +use LogicException; + +/** + * VObject ElementList. + * + * This class represents a list of elements. Lists are the result of queries, + * such as doing $vcalendar->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 + */ + 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 + */ + function offsetUnset($offset) { + + throw new LogicException('You can not remove objects from an ElementList'); + + } + + /* }}} */ + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/EofException.php b/htdocs/includes/sabre/sabre/vobject/lib/EofException.php new file mode 100644 index 00000000000..e9bd5587835 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/EofException.php @@ -0,0 +1,15 @@ +<?php + +namespace Sabre\VObject; + +/** + * Exception thrown by parser when the end of the stream has been reached, + * before this was expected. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class EofException extends ParseException { + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/FreeBusyData.php b/htdocs/includes/sabre/sabre/vobject/lib/FreeBusyData.php new file mode 100644 index 00000000000..0a6c72bb230 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/FreeBusyData.php @@ -0,0 +1,193 @@ +<?php + +namespace Sabre\VObject; + +/** + * FreeBusyData is a helper class that manages freebusy information. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class FreeBusyData { + + /** + * Start timestamp + * + * @var int + */ + protected $start; + + /** + * End timestamp + * + * @var int + */ + protected $end; + + /** + * A list of free-busy times. + * + * @var array + */ + protected $data; + + function __construct($start, $end) { + + $this->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 + * @return void + */ + 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, + ]; + + $preceedingItem = $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 ($itemsToDelete === -1) { + $itemsToDelete = 0; + if ($newItem['end'] < $preceedingItem['end']) { + $newItems[] = [ + 'start' => $newItem['end'] + 1, + 'end' => $preceedingItem['end'], + 'type' => $preceedingItem['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] + ); + } + + } + + function getData() { + + return $this->data; + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/FreeBusyGenerator.php b/htdocs/includes/sabre/sabre/vobject/lib/FreeBusyGenerator.php new file mode 100644 index 00000000000..e30b136c43c --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/FreeBusyGenerator.php @@ -0,0 +1,604 @@ +<?php + +namespace Sabre\VObject; + +use DateTimeImmutable; +use DateTimeInterface; +use DateTimeZone; +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Recur\EventIterator; +use Sabre\VObject\Recur\NoInstancesException; + +/** + * This class helps with generating FREEBUSY reports based on existing sets of + * objects. + * + * It only looks at VEVENT and VFREEBUSY objects from the sourcedata, and + * generates a single VFREEBUSY object. + * + * VFREEBUSY components are described in RFC5545, The rules for what should + * go in a single freebusy report is taken from RFC4791, section 7.10. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class FreeBusyGenerator { + + /** + * Input objects. + * + * @var array + */ + protected $objects = []; + + /** + * Start of range. + * + * @var DateTimeInterface|null + */ + protected $start; + + /** + * End of range. + * + * @var DateTimeInterface|null + */ + protected $end; + + /** + * VCALENDAR object. + * + * @var Document + */ + protected $baseObject; + + /** + * Reference timezone. + * + * When we are calculating busy times, and we come across so-called + * floating times (times without a timezone), we use the reference timezone + * instead. + * + * This is also used for all-day events. + * + * This defaults to UTC. + * + * @var DateTimeZone + */ + protected $timeZone; + + /** + * A VAVAILABILITY document. + * + * If this is set, it's information will be included when calculating + * freebusy time. + * + * @var Document + */ + protected $vavailability; + + /** + * Creates the generator. + * + * Check the setTimeRange and setObjects methods for details about the + * arguments. + * + * @param DateTimeInterface $start + * @param DateTimeInterface $end + * @param mixed $objects + * @param DateTimeZone $timeZone + */ + function __construct(DateTimeInterface $start = null, DateTimeInterface $end = null, $objects = null, DateTimeZone $timeZone = null) { + + $this->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. + * + * @param Document $vcalendar + * @return void + */ + function setBaseObject(Document $vcalendar) { + + $this->baseObject = $vcalendar; + + } + + /** + * Sets a VAVAILABILITY document. + * + * @param Document $vcalendar + * @return void + */ + function setVAvailability(Document $vcalendar) { + + $this->vavailability = $vcalendar; + + } + + /** + * Sets the input objects. + * + * You must either specify a valendar object as a string, or as the parse + * Component. + * It's also possible to specify multiple objects as an array. + * + * @param mixed $objects + * + * @return void + */ + 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 + * + * @return void + */ + 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. + * + * @param DateTimeZone $timeZone + * + * @return void + */ + function setTimeZone(DateTimeZone $timeZone) { + + $this->timeZone = $timeZone; + + } + + /** + * Parses the input data and returns a correct VFREEBUSY object, wrapped in + * a VCALENDAR. + * + * @return Component + */ + 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. + * + * @param FreeBusyData $fbData + * @param VCalendar $vavailability + * @return void + */ + 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 ($priorityA === 0) $priorityA = 10; + if ($priorityB === 0) $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 FreeBusyData $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) && (strtoupper($component->TRANSP) === 'TRANSPARENT')) { + break; + } + if (isset($component->STATUS)) { + $status = strtoupper($component->STATUS); + if ($status === 'CANCELLED') { + break; + } + if ($status === 'TENTATIVE') { + $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]); + continue; + } + + 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 ($fbType === 'FREE') + continue; + + $values = explode(',', $freebusy); + foreach ($values as $value) { + list($startTime, $endTime) = explode('/', $value); + $startTime = DateTimeParser::parseDateTime($startTime); + + if (substr($endTime, 0, 1) === 'P' || substr($endTime, 0, 2) === '-P') { + $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 ($busyType === 'FREE') { + 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 ($busyType !== 'BUSY') { + $prop['FBTYPE'] = $busyType; + } + $vfreebusy->add($prop); + + } + + return $calendar; + + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/ITip/Broker.php b/htdocs/includes/sabre/sabre/vobject/lib/ITip/Broker.php new file mode 100644 index 00000000000..effa7431793 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/ITip/Broker.php @@ -0,0 +1,989 @@ +<?php + +namespace Sabre\VObject\ITip; + +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\DateTimeParser; +use Sabre\VObject\Reader; +use Sabre\VObject\Recur\EventIterator; + +/** + * The ITip\Broker class is a utility class that helps with processing + * so-called iTip messages. + * + * iTip is defined in rfc5546, stands for iCalendar Transport-Independent + * Interoperability Protocol, and describes the underlying mechanism for + * using iCalendar for scheduling for for example through email (also known as + * IMip) and CalDAV Scheduling. + * + * This class helps by: + * + * 1. Creating individual invites based on an iCalendar event for each + * attendee. + * 2. Generating invite updates based on an iCalendar update. This may result + * in new invites, updates and cancellations for attendees, if that list + * changed. + * 3. On the receiving end, it can create a local iCalendar event based on + * a received invite. + * 4. It can also process an invite update on a local event, ensuring that any + * overridden properties from attendees are retained. + * 5. It can create a accepted or declined iTip reply based on an invite. + * 6. It can process a reply from an invite and update an events attendee + * status based on a reply. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Broker { + + /** + * This setting determines whether the rules for the SCHEDULE-AGENT + * parameter should be followed. + * + * This is a parameter defined on ATTENDEE properties, introduced by RFC + * 6638. This parameter allows a caldav client to tell the server 'Don't do + * any scheduling operations'. + * + * If this setting is turned on, any attendees with SCHEDULE-AGENT set to + * CLIENT will be ignored. This is the desired behavior for a CalDAV + * server, but if you're writing an iTip application that doesn't deal with + * CalDAV, you may want to ignore this parameter. + * + * @var bool + */ + public $scheduleAgentServerRules = true; + + /** + * The broker will try during 'parseEvent' figure out whether the change + * was significant. + * + * It uses a few different ways to do this. One of these ways is seeing if + * certain properties changed values. This list of specified here. + * + * This list is taken from: + * * http://tools.ietf.org/html/rfc5546#section-2.1.4 + * + * @var string[] + */ + public $significantChangeProperties = [ + 'DTSTART', + 'DTEND', + 'DURATION', + 'DUE', + 'RRULE', + 'RDATE', + 'EXDATE', + 'STATUS', + ]; + + /** + * This method is used to process an incoming itip message. + * + * Examples: + * + * 1. A user is an attendee to an event. The organizer sends an updated + * meeting using a new iTip message with METHOD:REQUEST. This function + * will process the message and update the attendee's event accordingly. + * + * 2. The organizer cancelled the event using METHOD:CANCEL. We will update + * the users event to state STATUS:CANCELLED. + * + * 3. An attendee sent a reply to an invite using METHOD:REPLY. We can + * update the organizers event to update the ATTENDEE with its correct + * PARTSTAT. + * + * The $existingObject is updated in-place. If there is no existing object + * (because it's a new invite for example) a new object will be created. + * + * If an existing object does not exist, and the method was CANCEL or + * REPLY, the message effectively gets ignored, and no 'existingObject' + * will be created. + * + * The updated $existingObject is also returned from this function. + * + * If the iTip message was not supported, we will always return false. + * + * @param Message $itipMessage + * @param VCalendar $existingObject + * + * @return VCalendar|null + */ + function processMessage(Message $itipMessage, VCalendar $existingObject = null) { + + // We only support events at the moment. + if ($itipMessage->component !== 'VEVENT') { + 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 $oldCalendar + * + * @return array + */ + function parseEvent($calendar = null, $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 Message $itipMessage + * @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 Message $itipMessage + * @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 Message $itipMessage + * @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) { + $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : '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) { + $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master'; + if ($recurId === 'master') { + $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(); + + if (isset($newObject->{'RECURRENCE-ID'}) && $newObject->{'RECURRENCE-ID'}->getValue() === $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. + * + * @param VCalendar $calendar + * @param array $eventInfo + * @param array $oldEventInfo + * + * @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']; + + if (!$attendee['newInstances']) { + + // If there are no instances the attendee is a part of, it + // means the attendee was removed and we need to send him a + // CANCEL. + $message->method = 'CANCEL'; + + // Creating the new iCalendar body. + $icalMsg = new VCalendar(); + $icalMsg->METHOD = $message->method; + $event = $icalMsg->add('VEVENT', [ + 'UID' => $message->uid, + 'SEQUENCE' => $message->sequence, + ]); + 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'; + + // Creating the new iCalendar body. + $icalMsg = new VCalendar(); + $icalMsg->METHOD = $message->method; + + foreach ($calendar->select('VTIMEZONE') as $timezone) { + $icalMsg->add(clone $timezone); + } + + // 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. + + $message->significantChange = + $attendee['forceSend'] === 'REQUEST' || + array_keys($attendee['oldInstances']) != array_keys($attendee['newInstances']) || + $oldEventInfo['significantChangeHash'] !== $eventInfo['significantChangeHash']; + + foreach ($attendee['newInstances'] as $instanceId => $instanceInfo) { + + $currentEvent = clone $eventInfo['instances'][$instanceId]; + if ($instanceId === 'master') { + + // 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'; + } + + } + + } + + $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 VCalendar $calendar + * @param array $eventInfo + * @param array $oldEventInfo + * @param string $attendee + * + * @return Message[] + */ + protected function parseEventForAttendee(VCalendar $calendar, array $eventInfo, array $oldEventInfo, $attendee) { + + if ($this->scheduleAgentServerRules && $eventInfo['organizerScheduleAgent'] === 'CLIENT') { + return []; + } + + // Don't bother generating messages for events that have already been + // cancelled. + if ($eventInfo['status'] === 'CANCELLED') { + 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']) && $instances['master']['newstatus'] !== 'DECLINED') { + 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'; + + $hasReply = false; + + foreach ($instances as $instance) { + + if ($instance['oldstatus'] == $instance['newstatus'] && $eventInfo['organizerForceSend'] !== 'REPLY') { + // 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 ($instance['id'] !== 'master') { + $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 = []; + + foreach ($calendar->VEVENT as $vevent) { + + 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 ($organizer !== $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->STATUS)) { + $status = strtoupper($vevent->STATUS->getValue()); + } + + $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master'; + if (is_null($timezone)) { + if ($recurId === 'master') { + $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']) && + strtoupper($attendee['SCHEDULE-AGENT']->getValue()) === 'CLIENT' + ) { + 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, + 'force-send' => $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); + + $significantChangeHash .= $prop . ':'; + + if ($prop === 'EXDATE') { + + $significantChangeHash .= implode(',', $exdate) . ';'; + + } else { + + foreach ($propertyValues as $val) { + $significantChangeHash .= $val->getValue() . ';'; + } + + } + } + } + + } + $significantChangeHash = md5($significantChangeHash); + + return compact( + 'uid', + 'organizer', + 'organizerName', + 'organizerScheduleAgent', + 'organizerForceSend', + 'instances', + 'attendees', + 'sequence', + 'exdate', + 'timezone', + 'significantChangeHash', + 'status' + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/ITip/ITipException.php b/htdocs/includes/sabre/sabre/vobject/lib/ITip/ITipException.php new file mode 100644 index 00000000000..ad5e53ab4ae --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/ITip/ITipException.php @@ -0,0 +1,15 @@ +<?php + +namespace Sabre\VObject\ITip; + +use Exception; + +/** + * This message is emitted in case of serious problems with iTip messages. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class ITipException extends Exception { +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/ITip/Message.php b/htdocs/includes/sabre/sabre/vobject/lib/ITip/Message.php new file mode 100644 index 00000000000..bebe2e4fc17 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/ITip/Message.php @@ -0,0 +1,141 @@ +<?php + +namespace Sabre\VObject\ITip; + +/** + * This class represents an iTip message. + * + * A message holds all the information relevant to the message, including the + * object itself. + * + * It should for the most part be treated as immutable. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Message { + + /** + * The object's UID. + * + * @var string + */ + public $uid; + + /** + * The component type, such as VEVENT. + * + * @var string + */ + public $component; + + /** + * Contains the ITip method, which is something like REQUEST, REPLY or + * CANCEL. + * + * @var string + */ + public $method; + + /** + * The current sequence number for the event. + * + * @var int + */ + public $sequence; + + /** + * The senders' email address. + * + * Note that this does not imply that this has to be used in a From: field + * if the message is sent by email. It may also be populated in Reply-To: + * or not at all. + * + * @var string + */ + public $sender; + + /** + * The name of the sender. This is often populated from a CN parameter from + * either the ORGANIZER or ATTENDEE, depending on the message. + * + * @var string|null + */ + public $senderName; + + /** + * The recipient's email address. + * + * @var string + */ + public $recipient; + + /** + * The name of the recipient. This is usually populated with the CN + * parameter from the ATTENDEE or ORGANIZER property, if it's available. + * + * @var string|null + */ + public $recipientName; + + /** + * After the message has been delivered, this should contain a string such + * as : 1.1;Sent or 1.2;Delivered. + * + * In case of a failure, this will hold the error status code. + * + * See: + * http://tools.ietf.org/html/rfc6638#section-7.3 + * + * @var string + */ + public $scheduleStatus; + + /** + * The iCalendar / iTip body. + * + * @var \Sabre\VObject\Component\VCalendar + */ + public $message; + + /** + * This will be set to true, if the iTip broker considers the change + * 'significant'. + * + * In practice, this means that we'll only mark it true, if for instance + * DTSTART changed. This allows systems to only send iTip messages when + * significant changes happened. This is especially useful for iMip, as + * normally a ton of messages may be generated for normal calendar use. + * + * To see the list of properties that are considered 'significant', check + * out Sabre\VObject\ITip\Broker::$significantChangeProperties. + * + * @var bool + */ + public $significantChange = true; + + /** + * Returns the schedule status as a string. + * + * For example: + * 1.2 + * + * @return mixed bool|string + */ + function getScheduleStatus() { + + if (!$this->scheduleStatus) { + + return false; + + } else { + + list($scheduleStatus) = explode(';', $this->scheduleStatus); + return $scheduleStatus; + + } + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/ITip/SameOrganizerForAllComponentsException.php b/htdocs/includes/sabre/sabre/vobject/lib/ITip/SameOrganizerForAllComponentsException.php new file mode 100644 index 00000000000..423b3983172 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/ITip/SameOrganizerForAllComponentsException.php @@ -0,0 +1,18 @@ +<?php + +namespace Sabre\VObject\ITip; + +/** + * SameOrganizerForAllComponentsException. + * + * This exception is emitted when an event is encountered with more than one + * component (e.g.: exceptions), but the organizer is not identical in every + * component. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class SameOrganizerForAllComponentsException extends ITipException { + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/InvalidDataException.php b/htdocs/includes/sabre/sabre/vobject/lib/InvalidDataException.php new file mode 100644 index 00000000000..50ebc0f4984 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/InvalidDataException.php @@ -0,0 +1,14 @@ +<?php + +namespace Sabre\VObject; + +/** + * This exception is thrown whenever an invalid value is found anywhere in a + * iCalendar or vCard object. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class InvalidDataException extends \Exception { +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Node.php b/htdocs/includes/sabre/sabre/vobject/lib/Node.php new file mode 100644 index 00000000000..e2845da75f7 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Node.php @@ -0,0 +1,265 @@ +<?php + +namespace Sabre\VObject; + +use Sabre\Xml; + +/** + * A node is the root class for every element in an iCalendar of vCard object. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +abstract class Node + implements \IteratorAggregate, + \ArrayAccess, + \Countable, + \JsonSerializable, + Xml\XmlSerializable { + + /** + * The following constants are used by the validate() method. + * + * If REPAIR is set, the validator will attempt to repair any broken data + * (if possible). + */ + const REPAIR = 1; + + /** + * If this option is set, the validator will operate on the vcards on the + * assumption that the vcards need to be valid for CardDAV. + * + * This means for example that the UID is required, whereas it is not for + * regular vcards. + */ + const PROFILE_CARDDAV = 2; + + /** + * If this option is set, the validator will operate on iCalendar objects + * on the assumption that the vcards need to be valid for CalDAV. + * + * This means for example that calendars can only contain objects with + * identical component types and UIDs. + */ + const PROFILE_CALDAV = 4; + + /** + * Reference to the parent object, if this is not the top object. + * + * @var Node + */ + public $parent; + + /** + * Iterator override. + * + * @var ElementList + */ + protected $iterator = null; + + /** + * The root document. + * + * @var Component + */ + protected $root; + + /** + * Serializes the node into a mimedir format. + * + * @return string + */ + abstract function serialize(); + + /** + * 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 + */ + abstract function jsonSerialize(); + + /** + * This method serializes the data into XML. This is used to create xCard or + * xCal documents. + * + * @param Xml\Writer $writer XML writer. + * + * @return void + */ + abstract function xmlSerialize(Xml\Writer $writer); + + /** + * 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. + * + * @return void + */ + function destroy() { + + $this->parent = null; + $this->root = null; + + } + + /* {{{ IteratorAggregator interface */ + + /** + * Returns the iterator for this object. + * + * @return ElementList + */ + 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 + * + * @param ElementList $iterator + * + * @return void + */ + 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 + */ + function validate($options = 0) { + + return []; + + } + + /* }}} */ + + /* {{{ Countable interface */ + + /** + * Returns the number of elements. + * + * @return int + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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/htdocs/includes/sabre/sabre/vobject/lib/PHPUnitAssertions.php b/htdocs/includes/sabre/sabre/vobject/lib/PHPUnitAssertions.php new file mode 100644 index 00000000000..87ec75e8f5f --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/PHPUnitAssertions.php @@ -0,0 +1,82 @@ +<?php + +namespace Sabre\VObject; + +/** + * PHPUnit Assertions + * + * This trait can be added to your unittest to make it easier to test iCalendar + * and/or vCards. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +trait PHPUnitAssertions { + + /** + * This method tests wether two vcards or icalendar objects are + * semantically identical. + * + * It supports objects being supplied as strings, streams or + * Sabre\VObject\Component instances. + * + * PRODID is removed from both objects as this is often changes and would + * just get in the way. + * + * CALSCALE will automatically get removed if it's set to GREGORIAN. + * + * Any property that has the value **ANY** will be treated as a wildcard. + * + * @param resource|string|Component $expected + * @param resource|string|Component $actual + * @param string $message + */ + function assertVObjectEqualsVObject($expected, $actual, $message = '') { + + $self = $this; + $getObj = function($input) use ($self) { + + if (is_resource($input)) { + $input = stream_get_contents($input); + } + if (is_string($input)) { + $input = Reader::read($input); + } + if (!$input instanceof Component) { + $this->fail('Input must be a string, stream or VObject component'); + } + unset($input->PRODID); + if ($input instanceof Component\VCalendar && (string)$input->CALSCALE === 'GREGORIAN') { + 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/htdocs/includes/sabre/sabre/vobject/lib/Parameter.php b/htdocs/includes/sabre/sabre/vobject/lib/Parameter.php new file mode 100644 index 00000000000..a99a33eec0e --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Parameter.php @@ -0,0 +1,394 @@ +<?php + +namespace Sabre\VObject; + +use ArrayIterator; +use Sabre\Xml; + +/** + * VObject Parameter. + * + * This class represents a parameter. A parameter is always tied to a property. + * In the case of: + * DTSTART;VALUE=DATE:20101108 + * VALUE=DATE would be the parameter name and value. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Parameter extends Node { + + /** + * Parameter name. + * + * @var string + */ + public $name; + + /** + * vCard 2.1 allows parameters to be encoded without a name. + * + * We can deduce the parameter name based on it's value. + * + * @var bool + */ + public $noName = false; + + /** + * Parameter value. + * + * @var string + */ + protected $value; + + /** + * Sets up the object. + * + * It's recommended to use the create:: factory method instead. + * + * @param string $name + * @param string $value + */ + function __construct(Document $root, $name, $value = null) { + + $this->name = strtoupper($name); + $this->root = $root; + if (is_null($name)) { + $this->noName = true; + $this->name = static::guessParameterNameByValue($value); + } + + // 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 + */ + 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 + * + * @return void + */ + 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 + */ + function getValue() { + + if (is_array($this->value)) { + return implode(',', $this->value); + } else { + return $this->value; + } + + } + + /** + * Sets multiple values for this parameter. + * + * @param array $value + * + * @return void + */ + 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 + */ + 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 + * + * @return void + */ + 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 + */ + function has($value) { + + return in_array( + strtolower($value), + array_map('strtolower', (array)$this->value) + ); + + } + + /** + * Turns the object back into a serialized blob. + * + * @return string + */ + function serialize() { + + $value = $this->getParts(); + + if (count($value) === 0) { + return $this->name . '='; + } + + if ($this->root->getDocumentType() === Document::VCARD21 && $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) + // severaly 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 + */ + 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. + * + * @return void + */ + function xmlSerialize(Xml\Writer $writer) { + + foreach (explode(',', $this->value) as $value) { + $writer->writeElement('text', $value); + } + + } + + /** + * Called when this object is being cast to a string. + * + * @return string + */ + function __toString() { + + return (string)$this->getValue(); + + } + + /** + * Returns the iterator for this object. + * + * @return ElementList + */ + function getIterator() { + + if (!is_null($this->iterator)) + return $this->iterator; + + return $this->iterator = new ArrayIterator((array)$this->value); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/ParseException.php b/htdocs/includes/sabre/sabre/vobject/lib/ParseException.php new file mode 100644 index 00000000000..d96d20720d6 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/ParseException.php @@ -0,0 +1,13 @@ +<?php + +namespace Sabre\VObject; + +/** + * Exception thrown by Reader if an invalid object was attempted to be parsed. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class ParseException extends \Exception { +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Parser/Json.php b/htdocs/includes/sabre/sabre/vobject/lib/Parser/Json.php new file mode 100644 index 00000000000..a77258a2ef4 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Parser/Json.php @@ -0,0 +1,197 @@ +<?php + +namespace Sabre\VObject\Parser; + +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Component\VCard; +use Sabre\VObject\EofException; +use Sabre\VObject\ParseException; + +/** + * Json Parser. + * + * This parser parses both the jCal and jCard formats. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Json extends Parser { + + /** + * The input data. + * + * @var array + */ + protected $input; + + /** + * Root component. + * + * @var Document + */ + protected $root; + + /** + * 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 resource|string|array|null $input + * @param int $options + * + * @return Sabre\VObject\Document + */ + function parse($input = null, $options = 0) { + + if (!is_null($input)) { + $this->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. + * + * @param array $jComp + * + * @return \Sabre\VObject\Component + */ + 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. + * + * @param array $jProp + * + * @return \Sabre\VObject\Property + */ + 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 ($defaultPropertyClass === 'Sabre\VObject\Property\FlatText') { + $defaultPropertyClass = 'Sabre\VObject\Property\Text'; + } + + // 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 + * + * @return void + */ + 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/htdocs/includes/sabre/sabre/vobject/lib/Parser/MimeDir.php b/htdocs/includes/sabre/sabre/vobject/lib/Parser/MimeDir.php new file mode 100644 index 00000000000..fa75a1a3b60 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Parser/MimeDir.php @@ -0,0 +1,696 @@ +<?php + +namespace Sabre\VObject\Parser; + +use Sabre\VObject\Component; +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Component\VCard; +use Sabre\VObject\Document; +use Sabre\VObject\EofException; +use Sabre\VObject\ParseException; + +/** + * MimeDir parser. + * + * This class parses iCalendar 2.0 and vCard 2.1, 3.0 and 4.0 files. This + * parser will return one of the following two objects from the parse method: + * + * Sabre\VObject\Component\VCalendar + * Sabre\VObject\Component\VCard + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class MimeDir extends Parser { + + /** + * The input stream. + * + * @var resource + */ + protected $input; + + /** + * Root component. + * + * @var Component + */ + protected $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. + * + * @var string + */ + protected $charset = 'UTF-8'; + + /** + * The list of character sets we support when decoding. + * + * This would be a const expression but for now we need to support PHP 5.5 + */ + protected static $SUPPORTED_CHARSETS = [ + 'UTF-8', + 'ISO-8859-1', + 'Windows-1252', + ]; + + /** + * Parses an iCalendar or vCard file. + * + * Pass a stream or a string. If null is parsed, the existing buffer is + * used. + * + * @param string|resource|null $input + * @param int $options + * + * @return Sabre\VObject\Document + */ + function parse($input = null, $options = 0) { + + $this->root = null; + + if (!is_null($input)) { + $this->setInput($input); + } + + 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 + */ + 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 + * + * @return void + */ + function setInput($input) { + + // Resetting the parser + $this->lineIndex = 0; + $this->startLine = 0; + + if (is_string($input)) { + // Convering 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. + * + * @return void + */ + 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) + && ord($line[0]) === 0xef + && ord($line[1]) === 0xbb + && ord($line[2]) === 0xbf) { + $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: + $line = $this->readLine(); + if (strtoupper(substr($line, 0, 4)) === 'END:') { + 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 (strtoupper(substr($line, 0, 6)) === 'BEGIN:') { + + $component = $this->root->createComponent(substr($line, 6), [], false); + + while (true) { + + // Reading until we hit END: + $line = $this->readLine(); + if (strtoupper(substr($line, 0, 4)) === 'END:') { + 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 null|string + */ + 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) && $rawLine === false)) { + throw new EofException('End of document reached prematurely'); + } + if ($rawLine === false) { + 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 ($nextLine[0] === "\t" || $nextLine[0] === " ") { + $line .= substr($nextLine, 1); + $rawLine .= "\n " . substr($nextLine, 1); + } else { + $this->lineBuffer = $nextLine; + break; + } + + } + $this->rawLine = $rawLine; + return $line; + + } + + /** + * Reads a property or component from a line. + * + * @return void + */ + 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<name> [$propNameToken]+ ) (?=[;:]) # property name + | + (?<=:)(?P<propValue> .+)$ # property value + | + ;(?P<paramName> [$paramNameToken]+) (?=[=;:]) # parameter name + | + (=|,)(?P<paramValue> # parameter value + (?: [$safeChar]*) | + \"(?: [$qSafeChar]+)\" + ) (?=[;:,]) + /xi"; + + //echo $regex, "\n"; die(); + 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)) { + 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; + } 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 it's 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); + + foreach ($namelessParameters as $namelessParameter) { + $propObj->add(null, $namelessParameter); + } + + if (strtoupper($propObj['ENCODING']) === 'QUOTED-PRINTABLE') { + $propObj->setQuotedPrintableValue($this->extractQuotedPrintableValue()); + } else { + $charset = $this->charset; + if ($this->root->getDocumentType() === Document::VCARD21 && isset($propObj['CHARSET'])) { + // vCard 2.1 allows the character set to be specified per property. + $charset = (string)$propObj['CHARSET']; + } + switch ($charset) { + case 'UTF-8' : + break; + case 'ISO-8859-1' : + $property['value'] = utf8_encode($property['value']); + break; + case 'Windows-1252' : + $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 delimeters 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 (null) 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[] + */ + 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 + * + * @return void + */ + 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) === '=') { + // Reading the line + $this->readLine(); + // Grabbing the raw form + $value .= "\n" . $this->rawLine; + } + } + + return $value; + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Parser/Parser.php b/htdocs/includes/sabre/sabre/vobject/lib/Parser/Parser.php new file mode 100644 index 00000000000..ca8bc0addd3 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Parser/Parser.php @@ -0,0 +1,80 @@ +<?php + +namespace Sabre\VObject\Parser; + +/** + * Abstract parser. + * + * This class serves as a base-class for the different parsers. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +abstract class Parser { + + /** + * Turning on this option makes the parser more forgiving. + * + * In the case of the MimeDir parser, this means that the parser will + * accept slashes and underscores in property names, and it will also + * attempt to fix Microsoft vCard 2.1's broken line folding. + */ + const OPTION_FORGIVING = 1; + + /** + * If this option is turned on, any lines we cannot parse will be ignored + * by the reader. + */ + const OPTION_IGNORE_INVALID_LINES = 2; + + /** + * Bitmask of parser options. + * + * @var int + */ + protected $options; + + /** + * Creates the parser. + * + * Optionally, it's possible to parse the input stream here. + * + * @param mixed $input + * @param int $options Any parser options (OPTION constants). + * + * @return void + */ + function __construct($input = null, $options = 0) { + + if (!is_null($input)) { + $this->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 function parse($input = null, $options = 0); + + /** + * Sets the input data. + * + * @param mixed $input + * + * @return void + */ + abstract function setInput($input); + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Parser/XML.php b/htdocs/includes/sabre/sabre/vobject/lib/Parser/XML.php new file mode 100644 index 00000000000..5ac42398477 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Parser/XML.php @@ -0,0 +1,428 @@ +<?php + +namespace Sabre\VObject\Parser; + +use Sabre\VObject\Component; +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Component\VCard; +use Sabre\VObject\EofException; +use Sabre\VObject\ParseException; +use Sabre\Xml as SabreXml; + +/** + * XML Parser. + * + * This parser parses both the xCal and xCard formats. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Ivan Enderlin + * @license http://sabre.io/license/ Modified BSD License + */ +class XML extends Parser { + + const XCAL_NAMESPACE = 'urn:ietf:params:xml:ns:icalendar-2.0'; + const XCARD_NAMESPACE = 'urn:ietf:params:xml:ns:vcard-4.0'; + + /** + * The input data. + * + * @var array + */ + protected $input; + + /** + * A pointer/reference to the input. + * + * @var array + */ + private $pointer; + + /** + * Document, root component. + * + * @var Sabre\VObject\Document + */ + protected $root; + + /** + * Creates the parser. + * + * Optionally, it's possible to parse the input stream here. + * + * @param mixed $input + * @param int $options Any parser options (OPTION constants). + * + * @return void + */ + function __construct($input = null, $options = 0) { + + if (0 === $options) { + $options = parent::OPTION_FORGIVING; + } + + parent::__construct($input, $options); + + } + + /** + * Parse xCal or xCard. + * + * @param resource|string $input + * @param int $options + * + * @throws \Exception + * + * @return Sabre\VObject\Document + */ + function parse($input = null, $options = 0) { + + if (!is_null($input)) { + $this->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 <vcard /> element. + break; + + } + break; + + default: + throw new ParseException('Unsupported XML standard'); + + } + + return $this->root; + } + + /** + * Parse a xCalendar component. + * + * @param Component $parentComponent + * + * @return void + */ + 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. + * + * @param Component $parentComponent + * + * @return void + */ + protected function parseVCardComponents(Component $parentComponent) { + + $this->pointer = &$this->pointer['value']; + $this->parseProperties($parentComponent); + + } + + /** + * Parse xCalendar and xCard properties. + * + * @param Component $parentComponent + * @param string $propertyNamePrefix + * + * @return void + */ + 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 ($namespace !== self::XCAL_NAMESPACE + && $namespace !== self::XCARD_NAMESPACE) { + + $propertyName = 'xml'; + $value = '<' . $tagName . ' xmlns="' . $namespace . '"'; + + foreach ($xmlProperty['attributes'] as $attributeName => $attributeValue) { + $value .= ' ' . $attributeName . '="' . str_replace('"', '\"', $attributeValue) . '"'; + } + + $value .= '>' . $xmlProperty['value'] . '</' . $tagName . '>'; + + $propertyValue = [$value]; + + $this->createProperty( + $parentComponent, + $propertyName, + $propertyParameters, + $propertyType, + $propertyValue + ); + + continue; + } + + // xCard group. + if ($propertyName === 'group') { + + 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. + + 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. + * + * @param Component $parentComponent + * + * @return void + */ + 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 Component $parentComponent + * @param string $name + * @param array $parameters + * @param string $type + * @param mixed $value + * + * @return void + */ + 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 + * + * @return void + */ + 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'] + = 'Sabre\VObject\Parser\XML\Element\KeyValue'; + $reader->elementMap['{' . self::XCAL_NAMESPACE . '}recur'] + = 'Sabre\VObject\Parser\XML\Element\KeyValue'; + $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/htdocs/includes/sabre/sabre/vobject/lib/Parser/XML/Element/KeyValue.php b/htdocs/includes/sabre/sabre/vobject/lib/Parser/XML/Element/KeyValue.php new file mode 100644 index 00000000000..14d7984332b --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Parser/XML/Element/KeyValue.php @@ -0,0 +1,70 @@ +<?php + +namespace Sabre\VObject\Parser\XML\Element; + +use Sabre\Xml as SabreXml; + +/** + * Our own sabre/xml key-value element. + * + * It just removes the clark notation. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Ivan Enderlin + * @license http://sabre.io/license/ Modified BSD License + */ +class KeyValue extends SabreXml\Element\KeyValue { + + /** + * The deserialize method is called during xml parsing. + * + * This method is called staticly, 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. + * + * @param XML\Reader $reader + * + * @return mixed + */ + static function xmlDeserialize(SabreXml\Reader $reader) { + + // If there's no children, we don't do anything. + if ($reader->isEmptyElement) { + $reader->next(); + return []; + } + + $values = []; + $reader->read(); + + do { + + if ($reader->nodeType === SabreXml\Reader::ELEMENT) { + + $name = $reader->localName; + $values[$name] = $reader->parseCurrentElement()['value']; + + } else { + $reader->read(); + } + + } while ($reader->nodeType !== SabreXml\Reader::END_ELEMENT); + + $reader->read(); + + return $values; + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Property.php b/htdocs/includes/sabre/sabre/vobject/lib/Property.php new file mode 100644 index 00000000000..1aaa3ed5869 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Property.php @@ -0,0 +1,662 @@ +<?php + +namespace Sabre\VObject; + +use Sabre\Xml; + +/** + * Property. + * + * A property is always in a KEY:VALUE structure, and may optionally contain + * parameters. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +abstract class Property extends Node { + + /** + * Property name. + * + * This will contain a string such as DTSTART, SUMMARY, FN. + * + * @var string + */ + public $name; + + /** + * Property group. + * + * This is only used in vcards + * + * @var string + */ + public $group; + + /** + * List of parameters. + * + * @var array + */ + public $parameters = []; + + /** + * Current value. + * + * @var mixed + */ + protected $value; + + /** + * In case this is a multi-value property. This string will be used as a + * delimiter. + * + * @var string|null + */ + public $delimiter = ';'; + + /** + * Creates the generic property. + * + * Parameters must be specified in key=>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 + * + * @return void + */ + function __construct(Component $root, $name, $value = null, array $parameters = [], $group = 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); + } + + } + + /** + * Updates the current value. + * + * This may be either a single, or multiple strings in an array. + * + * @param string|array $value + * + * @return void + */ + 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 + */ + function getValue() { + + if (is_array($this->value)) { + if (count($this->value) == 0) { + return; + } elseif (count($this->value) === 1) { + return $this->value[0]; + } else { + return $this->getRawMimeDirValue(); + } + } else { + return $this->value; + } + + } + + /** + * Sets a multi-valued property. + * + * @param array $parts + * + * @return void + */ + 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 + */ + 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 it's name. + * + * @param string $name + * @param string|null|array $value + */ + function add($name, $value = null) { + $noName = false; + if ($name === null) { + $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 + */ + 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 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 + * + * @return void + */ + abstract function setRawMimeDirValue($val); + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + abstract function getRawMimeDirValue(); + + /** + * Turns the object back into a serialized blob. + * + * @return string + */ + function serialize() { + + $str = $this->name; + if ($this->group) $str = $this->group . '.' . $this->name; + + foreach ($this->parameters() as $param) { + + $str .= ';' . $param->serialize(); + + } + + $str .= ':' . $this->getRawMimeDirValue(); + + $out = ''; + while (strlen($str) > 0) { + if (strlen($str) > 75) { + $out .= mb_strcut($str, 0, 75, 'utf-8') . "\r\n"; + $str = ' ' . mb_strcut($str, 75, strlen($str), 'utf-8'); + } else { + $out .= $str . "\r\n"; + $str = ''; + break; + } + } + + return $out; + + } + + /** + * Returns the value, in the format it should be encoded for JSON. + * + * This method must always return an array. + * + * @return array + */ + 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. + * + * @param array $value + * + * @return void + */ + function setJsonValue(array $value) { + + if (count($value) === 1) { + $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 + */ + function jsonSerialize() { + + $parameters = []; + + foreach ($this->parameters as $parameter) { + if ($parameter->name === 'VALUE') { + 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. + * + * @param array $value + * + * @return void + */ + 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. + * + * @return void + */ + function xmlSerialize(Xml\Writer $writer) { + + $parameters = []; + + foreach ($this->parameters as $parameter) { + + if ($parameter->name === 'VALUE') { + 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. + * + * @return void + */ + 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 + */ + function __toString() { + + return (string)$this->getValue(); + + } + + /* ArrayAccess interface {{{ */ + + /** + * Checks if an array element exists. + * + * @param mixed $name + * + * @return bool + */ + 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 + */ + 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 + */ + 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 + */ + 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. + * + * @return void + */ + 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 + */ + 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 ($this->root->getDocumentType() === Document::VCARD40) { + $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']; + 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. + * + * @return void + */ + function destroy() { + + parent::destroy(); + foreach ($this->parameters as $param) { + $param->destroy(); + } + $this->parameters = []; + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Property/Binary.php b/htdocs/includes/sabre/sabre/vobject/lib/Property/Binary.php new file mode 100644 index 00000000000..d54cae25da2 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Property/Binary.php @@ -0,0 +1,128 @@ +<?php + +namespace Sabre\VObject\Property; + +use Sabre\VObject\Property; + +/** + * BINARY property. + * + * This object represents BINARY values. + * + * Binary values are most commonly used by the iCalendar ATTACH property, and + * the vCard PHOTO property. + * + * This property will transparently encode and decode to base64. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Binary extends Property { + + /** + * In case this is a multi-value property. This string will be used as a + * delimiter. + * + * @var string|null + */ + public $delimiter = null; + + /** + * Updates the current value. + * + * This may be either a single, or multiple strings in an array. + * + * @param string|array $value + * + * @return void + */ + function setValue($value) { + + if (is_array($value)) { + + if (count($value) === 1) { + $this->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 + * + * @return void + */ + function setRawMimeDirValue($val) { + + $this->value = base64_decode($val); + + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + 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 + */ + 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 + */ + 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. + * + * @param array $value + * + * @return void + */ + function setJsonValue(array $value) { + + $value = array_map('base64_decode', $value); + parent::setJsonValue($value); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Property/Boolean.php b/htdocs/includes/sabre/sabre/vobject/lib/Property/Boolean.php new file mode 100644 index 00000000000..6f5887e25cd --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Property/Boolean.php @@ -0,0 +1,84 @@ +<?php + +namespace Sabre\VObject\Property; + +use + Sabre\VObject\Property; + +/** + * Boolean property. + * + * This object represents BOOLEAN values. These are always the case-insenstive + * string TRUE or FALSE. + * + * Automatic conversion to PHP's true and false are done. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Boolean extends Property { + + /** + * 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 + * + * @return void + */ + function setRawMimeDirValue($val) { + + $val = strtoupper($val) === 'TRUE' ? true : false; + $this->setValue($val); + + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + 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 + */ + function getValueType() { + + return 'BOOLEAN'; + + } + + /** + * Hydrate data from a XML subtree, as it would appear in a xCard or xCal + * object. + * + * @param array $value + * + * @return void + */ + function setXmlValue(array $value) { + + $value = array_map( + function($value) { + return 'true' === $value; + }, + $value + ); + parent::setXmlValue($value); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Property/FlatText.php b/htdocs/includes/sabre/sabre/vobject/lib/Property/FlatText.php new file mode 100644 index 00000000000..2c7b43c29d8 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Property/FlatText.php @@ -0,0 +1,50 @@ +<?php + +namespace Sabre\VObject\Property; + +/** + * FlatText property. + * + * This object represents certain TEXT values. + * + * Specifically, this property is used for text values where there is only 1 + * part. Semi-colons and colons will be de-escaped when deserializing, but if + * any semi-colons or commas appear without a backslash, we will not assume + * that they are delimiters. + * + * vCard 2.1 specifically has a whole bunch of properties where this may + * happen, as it only defines a delimiter for a few properties. + * + * vCard 4.0 states something similar. An unescaped semi-colon _may_ be a + * delimiter, depending on the property. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class FlatText extends Text { + + /** + * Field separator. + * + * @var string + */ + public $delimiter = ','; + + /** + * Sets the value as a quoted-printable encoded string. + * + * Overriding this so we're not splitting on a ; delimiter. + * + * @param string $val + * + * @return void + */ + function setQuotedPrintableValue($val) { + + $val = quoted_printable_decode($val); + $this->setValue($val); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Property/FloatValue.php b/htdocs/includes/sabre/sabre/vobject/lib/Property/FloatValue.php new file mode 100644 index 00000000000..15b1195491c --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Property/FloatValue.php @@ -0,0 +1,142 @@ +<?php + +namespace Sabre\VObject\Property; + +use Sabre\VObject\Property; +use Sabre\Xml; + +/** + * Float property. + * + * This object represents FLOAT values. These can be 1 or more floating-point + * numbers. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class FloatValue extends Property { + + /** + * In case this is a multi-value property. This string will be used as a + * delimiter. + * + * @var string|null + */ + public $delimiter = ';'; + + /** + * 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 + * + * @return void + */ + function setRawMimeDirValue($val) { + + $val = explode($this->delimiter, $val); + foreach ($val as &$item) { + $item = (float)$item; + } + $this->setParts($val); + + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + 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 + */ + 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 + */ + 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 ($this->name === 'GEO') { + return [$val]; + } + + return $val; + + } + + /** + * Hydrate data from a XML subtree, as it would appear in a xCard or xCal + * object. + * + * @param array $value + * + * @return void + */ + 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. + * + * @return void + */ + protected function xmlSerializeValue(Xml\Writer $writer) { + + // Special-casing the GEO property. + // + // See: + // http://tools.ietf.org/html/rfc6321#section-3.4.1.2 + if ($this->name === 'GEO') { + + $value = array_map('floatval', $this->getParts()); + + $writer->writeElement('latitude', $value[0]); + $writer->writeElement('longitude', $value[1]); + + } + else { + parent::xmlSerializeValue($writer); + } + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Property/ICalendar/CalAddress.php b/htdocs/includes/sabre/sabre/vobject/lib/Property/ICalendar/CalAddress.php new file mode 100644 index 00000000000..a0c4a9b9add --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Property/ICalendar/CalAddress.php @@ -0,0 +1,61 @@ +<?php + +namespace Sabre\VObject\Property\ICalendar; + +use + Sabre\VObject\Property\Text; + +/** + * CalAddress property. + * + * This object encodes CAL-ADDRESS values, as defined in rfc5545 + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class CalAddress extends Text { + + /** + * In case this is a multi-value property. This string will be used as a + * delimiter. + * + * @var string|null + */ + public $delimiter = null; + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + function getValueType() { + + return 'CAL-ADDRESS'; + + } + + /** + * This returns a normalized form of the value. + * + * This is primarily used right now to turn mixed-cased schemes in user + * uris to lower-case. + * + * Evolution in particular tends to encode mailto: as MAILTO:. + * + * @return string + */ + function getNormalizedValue() { + + $input = $this->getValue(); + if (!strpos($input, ':')) { + return $input; + } + list($schema, $everythingElse) = explode(':', $input, 2); + return strtolower($schema) . ':' . $everythingElse; + + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Property/ICalendar/Date.php b/htdocs/includes/sabre/sabre/vobject/lib/Property/ICalendar/Date.php new file mode 100644 index 00000000000..378a0d60a3e --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Property/ICalendar/Date.php @@ -0,0 +1,18 @@ +<?php + +namespace Sabre\VObject\Property\ICalendar; + +/** + * DateTime property. + * + * This object represents DATE values, as defined here: + * + * http://tools.ietf.org/html/rfc5545#section-3.3.5 + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Date extends DateTime { + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Property/ICalendar/DateTime.php b/htdocs/includes/sabre/sabre/vobject/lib/Property/ICalendar/DateTime.php new file mode 100644 index 00000000000..d580d4f68fe --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Property/ICalendar/DateTime.php @@ -0,0 +1,404 @@ +<?php + +namespace Sabre\VObject\Property\ICalendar; + +use DateTimeInterface; +use DateTimeZone; +use Sabre\VObject\DateTimeParser; +use Sabre\VObject\InvalidDataException; +use Sabre\VObject\Property; +use Sabre\VObject\TimeZoneUtil; + +/** + * DateTime property. + * + * This object represents DATE-TIME values, as defined here: + * + * http://tools.ietf.org/html/rfc5545#section-3.3.4 + * + * This particular object has a bit of hackish magic that it may also in some + * cases represent a DATE value. This is because it's a common usecase to be + * able to change a DATE-TIME into a DATE. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class DateTime extends Property { + + /** + * In case this is a multi-value property. This string will be used as a + * delimiter. + * + * @var string|null + */ + public $delimiter = ','; + + /** + * Sets a multi-valued property. + * + * You may also specify DateTime objects here. + * + * @param array $parts + * + * @return void + */ + function setParts(array $parts) { + + if (isset($parts[0]) && $parts[0] instanceof DateTimeInterface) { + $this->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 + * + * @return void + */ + 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 + * + * @return void + */ + function setRawMimeDirValue($val) { + + $this->setValue(explode($this->delimiter, $val)); + + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + 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 + */ + function hasTime() { + + return strtoupper((string)$this['VALUE']) !== 'DATE'; + + } + + /** + * Returns true if this is a floating DATE or DATE-TIME. + * + * Note that DATE is always floating. + */ + function isFloating() { + + return + !$this->hasTime() || + ( + !isset($this['TZID']) && + strpos($this->getValue(), 'Z') === false + ); + + } + + /** + * 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 + */ + 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[] + */ + 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 DateTimeInterface $dt + * @param bool isFloating If set to true, timezones will be ignored. + * + * @return void + */ + 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 otehr values will be adjusted for that timezone + * + * @param DateTimeInterface[] $dt + * @param bool isFloating If set to true, timezones will be ignored. + * + * @return void + */ + 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 + */ + 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 + */ + 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. + * + * @param array $value + * + * @return void + */ + 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 + */ + function offsetSet($name, $value) { + + parent::offsetSet($name, $value); + if (strtoupper($name) !== 'VALUE') { + 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 + */ + function validate($options = 0) { + + $messages = parent::validate($options); + $valueType = $this->getValueType(); + $values = $this->getParts(); + try { + foreach ($values as $value) { + 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, + ]; + } + return $messages; + + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Property/ICalendar/Duration.php b/htdocs/includes/sabre/sabre/vobject/lib/Property/ICalendar/Duration.php new file mode 100644 index 00000000000..7b7e1ce8ea5 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Property/ICalendar/Duration.php @@ -0,0 +1,85 @@ +<?php + +namespace Sabre\VObject\Property\ICalendar; + +use Sabre\VObject\DateTimeParser; +use Sabre\VObject\Property; + +/** + * Duration property. + * + * This object represents DURATION values, as defined here: + * + * http://tools.ietf.org/html/rfc5545#section-3.3.6 + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Duration extends Property { + + /** + * In case this is a multi-value property. This string will be used as a + * delimiter. + * + * @var string|null + */ + public $delimiter = ','; + + /** + * 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 + * + * @return void + */ + function setRawMimeDirValue($val) { + + $this->setValue(explode($this->delimiter, $val)); + + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + 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 + */ + 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 + */ + function getDateInterval() { + + $parts = $this->getParts(); + $value = $parts[0]; + return DateTimeParser::parseDuration($value); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Property/ICalendar/Period.php b/htdocs/includes/sabre/sabre/vobject/lib/Property/ICalendar/Period.php new file mode 100644 index 00000000000..d35b425aa8e --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Property/ICalendar/Period.php @@ -0,0 +1,155 @@ +<?php + +namespace Sabre\VObject\Property\ICalendar; + +use Sabre\VObject\DateTimeParser; +use Sabre\VObject\Property; +use Sabre\Xml; + +/** + * Period property. + * + * This object represents PERIOD values, as defined here: + * + * http://tools.ietf.org/html/rfc5545#section-3.8.2.6 + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Period extends Property { + + /** + * In case this is a multi-value property. This string will be used as a + * delimiter. + * + * @var string|null + */ + public $delimiter = ','; + + /** + * 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 + * + * @return void + */ + function setRawMimeDirValue($val) { + + $this->setValue(explode($this->delimiter, $val)); + + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + 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 + */ + 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. + * + * @param array $value + * + * @return void + */ + 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 + */ + function getJsonValue() { + + $return = []; + foreach ($this->getParts() as $item) { + + list($start, $end) = explode('/', $item, 2); + + $start = DateTimeParser::parseDateTime($start); + + // This is a duration value. + if ($end[0] === 'P') { + $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. + * + * @return void + */ + protected function xmlSerializeValue(Xml\Writer $writer) { + + $writer->startElement(strtolower($this->getValueType())); + $value = $this->getJsonValue(); + $writer->writeElement('start', $value[0][0]); + + if ($value[0][1][0] === 'P') { + $writer->writeElement('duration', $value[0][1]); + } + else { + $writer->writeElement('end', $value[0][1]); + } + + $writer->endElement(); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Property/ICalendar/Recur.php b/htdocs/includes/sabre/sabre/vobject/lib/Property/ICalendar/Recur.php new file mode 100644 index 00000000000..434b770884b --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Property/ICalendar/Recur.php @@ -0,0 +1,359 @@ +<?php + +namespace Sabre\VObject\Property\ICalendar; + +use Sabre\VObject\Property; +use Sabre\Xml; + +/** + * Recur property. + * + * This object represents RECUR properties. + * These values are just used for RRULE and the now deprecated EXRULE. + * + * The RRULE property may look something like this: + * + * RRULE:FREQ=MONTHLY;BYDAY=1,2,3;BYHOUR=5. + * + * This property exposes this as a key=>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 + * + * @return void + */ + 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 (strpos($v, ',') !== false) { + $v = explode(',', $v); + } + if (strcmp($k, 'until') === 0) { + $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 + */ + 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. + * + * @param array $parts + * @return void + */ + 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 + */ + 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 + * + * @return void + */ + function setRawMimeDirValue($val) { + + $this->setValue($val); + + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + 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 + */ + 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 + */ + function getJsonValue() { + + $values = []; + foreach ($this->getParts() as $k => $v) { + if (strcmp($k, 'UNTIL') === 0) { + $date = new DateTime($this->root, null, $v); + $values[strtolower($k)] = $date->getJsonValue()[0]; + } elseif (strcmp($k, 'COUNT') === 0) { + $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. + * + * @return void + */ + 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 + */ + static function stringToArray($value) { + + $value = strtoupper($value); + $newValue = []; + foreach (explode(';', $value) as $part) { + + // Skipping empty parts. + if (empty($part)) { + continue; + } + list($partName, $partValue) = explode('=', $part); + + // The value itself had multiple values.. + if (strpos($partValue, ',') !== false) { + $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 + */ + 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 ($key == 'BYMONTH') { + $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 ($key == 'BYWEEKNO') { + $byWeekNo = (array)$value; + foreach ($byWeekNo as $i => $v) { + if (!is_numeric($v) || (int)$v < -53 || (int)$v == 0 || (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 ($key == 'BYYEARDAY') { + $byYearDay = (array)$value; + foreach ($byYearDay as $i => $v) { + if (!is_numeric($v) || (int)$v < -366 || (int)$v == 0 || (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/htdocs/includes/sabre/sabre/vobject/lib/Property/IntegerValue.php b/htdocs/includes/sabre/sabre/vobject/lib/Property/IntegerValue.php new file mode 100644 index 00000000000..5bd1887fad2 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Property/IntegerValue.php @@ -0,0 +1,88 @@ +<?php + +namespace Sabre\VObject\Property; + +use + Sabre\VObject\Property; + +/** + * Integer property. + * + * This object represents INTEGER values. These are always a single integer. + * They may be preceeded by either + or -. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class IntegerValue extends Property { + + /** + * 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 + * + * @return void + */ + function setRawMimeDirValue($val) { + + $this->setValue((int)$val); + + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + 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 + */ + 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 + */ + function getJsonValue() { + + return [(int)$this->getValue()]; + + } + + /** + * Hydrate data from a XML subtree, as it would appear in a xCard or xCal + * object. + * + * @param array $value + * + * @return void + */ + function setXmlValue(array $value) { + + $value = array_map('intval', $value); + parent::setXmlValue($value); + + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Property/Text.php b/htdocs/includes/sabre/sabre/vobject/lib/Property/Text.php new file mode 100644 index 00000000000..476dcde4d33 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Property/Text.php @@ -0,0 +1,413 @@ +<?php + +namespace Sabre\VObject\Property; + +use Sabre\VObject\Component; +use Sabre\VObject\Document; +use Sabre\VObject\Parser\MimeDir; +use Sabre\VObject\Property; +use Sabre\Xml; + +/** + * Text property. + * + * This object represents TEXT values. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Text extends Property { + + /** + * In case this is a multi-value property. This string will be used as a + * delimiter. + * + * @var string + */ + public $delimiter = ','; + + /** + * List of properties that are considered 'structured'. + * + * @var array + */ + protected $structuredValues = [ + // vCard + 'N', + 'ADR', + 'ORG', + 'GENDER', + 'CLIENTPIDMAP', + + // iCalendar + 'REQUEST-STATUS', + ]; + + /** + * Some text components have a minimum number of components. + * + * N must for instance be represented as 5 components, separated by ;, even + * if the last few components are unused. + * + * @var array + */ + protected $minimumPropertyValues = [ + 'N' => 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 + * + * @return void + */ + 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 + * + * @return void + */ + function setRawMimeDirValue($val) { + + $this->setValue(MimeDir::unescapeValue($val, $this->delimiter)); + + } + + /** + * Sets the value as a quoted-printable encoded string. + * + * @param string $val + * + * @return void + */ + 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 preceeded with a \. + $regex = '# (?<!\\\\) ; #x'; + $matches = preg_split($regex, $val); + $this->setValue($matches); + + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + 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) { + $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 + */ + 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 + */ + function getValueType() { + + return 'TEXT'; + + } + + /** + * Turns the object back into a serialized blob. + * + * @return string + */ + function serialize() { + + // We need to kick in a special type of encoding, if it's a 2.1 vcard. + if ($this->root->getDocumentType() !== Document::VCARD21) { + 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 ($param->getValue() === 'QUOTED-PRINTABLE') { + continue; + } + $str .= ';' . $param->serialize(); + + } + + + + // If the resulting value contains a \n, we must encode it as + // quoted-printable. + if (strpos($val, "\n") !== false) { + + $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; + $out = ''; + while (strlen($str) > 0) { + if (strlen($str) > 75) { + $out .= mb_strcut($str, 0, 75, 'utf-8') . "\r\n"; + $str = ' ' . mb_strcut($str, 75, strlen($str), 'utf-8'); + } else { + $out .= $str . "\r\n"; + $str = ''; + break; + } + } + + return $out; + + } + + } + + /** + * This method serializes only the value of a property. This is used to + * create xCard or xCal documents. + * + * @param Xml\Writer $writer XML writer. + * + * @return void + */ + 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 + */ + 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/htdocs/includes/sabre/sabre/vobject/lib/Property/Time.php b/htdocs/includes/sabre/sabre/vobject/lib/Property/Time.php new file mode 100644 index 00000000000..dbafc3b85cd --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Property/Time.php @@ -0,0 +1,144 @@ +<?php + +namespace Sabre\VObject\Property; + +use Sabre\VObject\DateTimeParser; + +/** + * Time property. + * + * This object encodes TIME values. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Time extends Text { + + /** + * In case this is a multi-value property. This string will be used as a + * delimiter. + * + * @var string|null + */ + public $delimiter = null; + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + function getValueType() { + + return 'TIME'; + + } + + /** + * Sets the JSON value, as it would appear in a jCard or jCal object. + * + * The value must always be an array. + * + * @param array $value + * + * @return void + */ + function setJsonValue(array $value) { + + // Removing colons from value. + $value = str_replace( + ':', + '', + $value + ); + + if (count($value) === 1) { + $this->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 + */ + 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 ($parts['timezone'] === 'Z') { + $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. + * + * @param array $value + * + * @return void + */ + function setXmlValue(array $value) { + + $value = array_map( + function($value) { + return str_replace(':', '', $value); + }, + $value + ); + parent::setXmlValue($value); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Property/Unknown.php b/htdocs/includes/sabre/sabre/vobject/lib/Property/Unknown.php new file mode 100644 index 00000000000..7a337386835 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Property/Unknown.php @@ -0,0 +1,44 @@ +<?php + +namespace Sabre\VObject\Property; + +/** + * Unknown property. + * + * This object represents any properties not recognized by the parser. + * This type of value has been introduced by the jCal, jCard specs. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Unknown extends Text { + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + function getJsonValue() { + + return [$this->getRawMimeDirValue()]; + + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + function getValueType() { + + return 'UNKNOWN'; + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Property/Uri.php b/htdocs/includes/sabre/sabre/vobject/lib/Property/Uri.php new file mode 100644 index 00000000000..88fcfaab888 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Property/Uri.php @@ -0,0 +1,122 @@ +<?php + +namespace Sabre\VObject\Property; + +use Sabre\VObject\Parameter; +use Sabre\VObject\Property; + +/** + * URI property. + * + * This object encodes URI values. vCard 2.1 calls these URL. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Uri extends Text { + + /** + * In case this is a multi-value property. This string will be used as a + * delimiter. + * + * @var string|null + */ + public $delimiter = null; + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + function getValueType() { + + return 'URI'; + + } + + /** + * Returns an iterable list of children. + * + * @return array + */ + function parameters() { + + $parameters = parent::parameters(); + if (!isset($parameters['VALUE']) && in_array($this->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 + * + * @return void + */ + 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 blackslash. 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 ($this->name === 'URL') { + $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 + */ + function getRawMimeDirValue() { + + if (is_array($this->value)) { + $value = $this->value[0]; + } else { + $value = $this->value; + } + + return strtr($value, [',' => '\,']); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Property/UtcOffset.php b/htdocs/includes/sabre/sabre/vobject/lib/Property/UtcOffset.php new file mode 100644 index 00000000000..61895c48eb7 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Property/UtcOffset.php @@ -0,0 +1,77 @@ +<?php + +namespace Sabre\VObject\Property; + +/** + * UtcOffset property. + * + * This object encodes UTC-OFFSET values. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class UtcOffset extends Text { + + /** + * In case this is a multi-value property. This string will be used as a + * delimiter. + * + * @var string|null + */ + public $delimiter = null; + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + function getValueType() { + + return 'UTC-OFFSET'; + + } + + /** + * Sets the JSON value, as it would appear in a jCard or jCal object. + * + * The value must always be an array. + * + * @param array $value + * + * @return void + */ + function setJsonValue(array $value) { + + $value = array_map( + function($value) { + return str_replace(':', '', $value); + }, + $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 + */ + function getJsonValue() { + + return array_map( + function($value) { + return substr($value, 0, -2) . ':' . + substr($value, -2); + }, + parent::getJsonValue() + ); + + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Property/VCard/Date.php b/htdocs/includes/sabre/sabre/vobject/lib/Property/VCard/Date.php new file mode 100644 index 00000000000..1ef6dff3443 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Property/VCard/Date.php @@ -0,0 +1,43 @@ +<?php + +namespace Sabre\VObject\Property\VCard; + +/** + * Date property. + * + * This object encodes vCard DATE values. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Date extends DateAndOrTime { + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + function getValueType() { + + return 'DATE'; + + } + + /** + * Sets the property as a DateTime object. + * + * @param \DateTimeInterface $dt + * + * @return void + */ + function setDateTime(\DateTimeInterface $dt) { + + $this->value = $dt->format('Ymd'); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Property/VCard/DateAndOrTime.php b/htdocs/includes/sabre/sabre/vobject/lib/Property/VCard/DateAndOrTime.php new file mode 100644 index 00000000000..3b4ae3bb50f --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Property/VCard/DateAndOrTime.php @@ -0,0 +1,405 @@ +<?php + +namespace Sabre\VObject\Property\VCard; + +use DateTime; +use DateTimeImmutable; +use DateTimeInterface; +use Sabre\VObject\DateTimeParser; +use Sabre\VObject\InvalidDataException; +use Sabre\VObject\Property; +use Sabre\Xml; + +/** + * DateAndOrTime property. + * + * This object encodes DATE-AND-OR-TIME values. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class DateAndOrTime extends Property { + + /** + * Field separator. + * + * @var null|string + */ + public $delimiter = null; + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + function getValueType() { + + return 'DATE-AND-OR-TIME'; + + } + + /** + * Sets a multi-valued property. + * + * You may also specify DateTimeInterface objects here. + * + * @param array $parts + * + * @return void + */ + function setParts(array $parts) { + + if (count($parts) > 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 + * + * @return void + */ + function setValue($value) { + + if ($value instanceof DateTimeInterface) { + $this->setDateTime($value); + } else { + parent::setValue($value); + } + + } + + /** + * Sets the property as a DateTime object. + * + * @param DateTimeInterface $dt + * + * @return void + */ + 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 + */ + function getDateTime() { + + $now = new DateTime(); + + $tzFormat = $now->getTimezone()->getOffset($now) === 0 ? '\\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 + */ + 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. + * + * @return void + */ + 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 + * + * @return void + */ + function setRawMimeDirValue($val) { + + $this->setValue($val); + + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + 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 + */ + 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/htdocs/includes/sabre/sabre/vobject/lib/Property/VCard/DateTime.php b/htdocs/includes/sabre/sabre/vobject/lib/Property/VCard/DateTime.php new file mode 100644 index 00000000000..e7c804ca784 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Property/VCard/DateTime.php @@ -0,0 +1,30 @@ +<?php + +namespace Sabre\VObject\Property\VCard; + +/** + * DateTime property. + * + * This object encodes DATE-TIME values for vCards. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class DateTime extends DateAndOrTime { + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + function getValueType() { + + return 'DATE-TIME'; + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Property/VCard/LanguageTag.php b/htdocs/includes/sabre/sabre/vobject/lib/Property/VCard/LanguageTag.php new file mode 100644 index 00000000000..aa7e9178d51 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Property/VCard/LanguageTag.php @@ -0,0 +1,60 @@ +<?php + +namespace Sabre\VObject\Property\VCard; + +use + Sabre\VObject\Property; + +/** + * LanguageTag property. + * + * This object represents LANGUAGE-TAG values as used in vCards. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class LanguageTag extends Property { + + /** + * 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 + * + * @return void + */ + function setRawMimeDirValue($val) { + + $this->setValue($val); + + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + 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 + */ + function getValueType() { + + return 'LANGUAGE-TAG'; + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Property/VCard/TimeStamp.php b/htdocs/includes/sabre/sabre/vobject/lib/Property/VCard/TimeStamp.php new file mode 100644 index 00000000000..9d311f99d1b --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Property/VCard/TimeStamp.php @@ -0,0 +1,86 @@ +<?php + +namespace Sabre\VObject\Property\VCard; + +use Sabre\VObject\DateTimeParser; +use Sabre\VObject\Property\Text; +use Sabre\Xml; + +/** + * TimeStamp property. + * + * This object encodes TIMESTAMP values. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class TimeStamp extends Text { + + /** + * In case this is a multi-value property. This string will be used as a + * delimiter. + * + * @var string|null + */ + public $delimiter = null; + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + function getValueType() { + + return 'TIMESTAMP'; + + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + function getJsonValue() { + + $parts = DateTimeParser::parseVCardDateTime($this->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. + * + * @return void + */ + 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/htdocs/includes/sabre/sabre/vobject/lib/Reader.php b/htdocs/includes/sabre/sabre/vobject/lib/Reader.php new file mode 100644 index 00000000000..70992933796 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Reader.php @@ -0,0 +1,98 @@ +<?php + +namespace Sabre\VObject; + +/** + * iCalendar/vCard/jCal/jCard/xCal/xCard reader object. + * + * This object provides a few (static) convenience methods to quickly access + * the parsers. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Reader { + + /** + * If this option is passed to the reader, it will be less strict about the + * validity of the lines. + */ + const OPTION_FORGIVING = 1; + + /** + * If this option is turned on, any lines we cannot parse will be ignored + * by the reader. + */ + const OPTION_IGNORE_INVALID_LINES = 2; + + /** + * Parses a vCard or iCalendar 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 + * @param string $charset + * @return Document + */ + static function read($data, $options = 0, $charset = 'UTF-8') { + + $parser = new Parser\MimeDir(); + $parser->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 it's 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 + */ + 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 + */ + static function readXML($data, $options = 0) { + + $parser = new Parser\XML(); + $result = $parser->parse($data, $options); + + return $result; + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Recur/EventIterator.php b/htdocs/includes/sabre/sabre/vobject/lib/Recur/EventIterator.php new file mode 100644 index 00000000000..d313305a0dd --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Recur/EventIterator.php @@ -0,0 +1,513 @@ +<?php + +namespace Sabre\VObject\Recur; + +use DateTimeImmutable; +use DateTimeInterface; +use DateTimeZone; +use InvalidArgumentException; +use Sabre\VObject\Component; +use Sabre\VObject\Component\VEvent; +use Sabre\VObject\Settings; + +/** + * This class is used to determine new for a recurring event, when the next + * events occur. + * + * This iterator may loop infinitely in the future, therefore it is important + * that if you use this class, you set hard limits for the amount of iterations + * you want to handle. + * + * Note that currently there is not full support for the entire iCalendar + * specification, as it's very complex and contains a lot of permutations + * that's not yet used very often in software. + * + * For the focus has been on features as they actually appear in Calendaring + * software, but this may well get expanded as needed / on demand + * + * The following RRULE properties are supported + * * UNTIL + * * INTERVAL + * * COUNT + * * FREQ=DAILY + * * BYDAY + * * BYHOUR + * * BYMONTH + * * FREQ=WEEKLY + * * BYDAY + * * BYHOUR + * * WKST + * * FREQ=MONTHLY + * * BYMONTHDAY + * * BYDAY + * * BYSETPOS + * * FREQ=YEARLY + * * BYMONTH + * * BYYEARDAY + * * BYWEEKNO + * * BYMONTHDAY (only if BYMONTH is also set) + * * BYDAY (only if BYMONTH is also set) + * + * Anything beyond this is 'undefined', which means that it may get ignored, or + * you may get unexpected results. The effect is that in some applications the + * specified recurrence may look incorrect, or is missing. + * + * The recurrence iterator also does not yet support THISANDFUTURE. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class EventIterator implements \Iterator { + + /** + * Reference timeZone for floating dates and times. + * + * @var DateTimeZone + */ + protected $timeZone; + + /** + * True if we're iterating an all-day event. + * + * @var bool + */ + protected $allDay = false; + + /** + * Creates the iterator. + * + * There's three ways to set up the iterator. + * + * 1. You can pass a VCALENDAR component and a UID. + * 2. You can pass an array of VEVENTs (all UIDS should match). + * 3. You can pass a single VEVENT component. + * + * Only the second method is recomended. The other 1 and 3 will be removed + * at some point in the future. + * + * The $uid parameter is only required for the first method. + * + * @param Component|array $input + * @param string|null $uid + * @param DateTimeZone $timeZone Reference timezone for floating dates and + * times. + */ + function __construct($input, $uid = null, DateTimeZone $timeZone = null) { + + if (is_null($timeZone)) { + $timeZone = new DateTimeZone('UTC'); + } + $this->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 + */ + function current() { + + if ($this->currentDate) { + return clone $this->currentDate; + } + + } + + /** + * This method returns the start date for the current iteration of the + * event. + * + * @return DateTimeImmutable + */ + function getDtStart() { + + if ($this->currentDate) { + return clone $this->currentDate; + } + + } + + /** + * This method returns the end date for the current iteration of the + * event. + * + * @return DateTimeImmutable + */ + function getDtEnd() { + + if (!$this->valid()) { + return; + } + $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 it's DTSTART and DTEND + * altered. + * + * @return VEvent + */ + 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 + */ + 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 + */ + function valid() { + + if ($this->counter > Settings::$maxRecurrences && Settings::$maxRecurrences !== -1) { + throw new MaxInstancesExceededException('Recurring events are only allowed to generate ' . Settings::$maxRecurrences); + } + return !!$this->currentDate; + + } + + /** + * Sets the iterator back to the starting point. + */ + 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 + */ + 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. + * + * @param DateTimeInterface $dateTime + */ + function fastForward(DateTimeInterface $dateTime) { + + while ($this->valid() && $this->getDtEnd() <= $dateTime) { + $this->next(); + } + + } + + /** + * Returns true if this recurring event never ends. + * + * @return bool + */ + 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/htdocs/includes/sabre/sabre/vobject/lib/Recur/MaxInstancesExceededException.php b/htdocs/includes/sabre/sabre/vobject/lib/Recur/MaxInstancesExceededException.php new file mode 100644 index 00000000000..264df7d2bd5 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Recur/MaxInstancesExceededException.php @@ -0,0 +1,16 @@ +<?php + +namespace Sabre\VObject\Recur; + +use Exception; + +/** + * This exception will get thrown when a recurrence rule generated more than + * the maximum number of instances. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License + */ +class MaxInstancesExceededException extends Exception { +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Recur/NoInstancesException.php b/htdocs/includes/sabre/sabre/vobject/lib/Recur/NoInstancesException.php new file mode 100644 index 00000000000..8f8bb472bf5 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Recur/NoInstancesException.php @@ -0,0 +1,18 @@ +<?php + +namespace Sabre\VObject\Recur; + +use Exception; + +/** + * This exception gets thrown when a recurrence iterator produces 0 instances. + * + * This may happen when every occurence in a rrule is also in EXDATE. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License + */ +class NoInstancesException extends Exception { + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Recur/RDateIterator.php b/htdocs/includes/sabre/sabre/vobject/lib/Recur/RDateIterator.php new file mode 100644 index 00000000000..f44960e123f --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Recur/RDateIterator.php @@ -0,0 +1,182 @@ +<?php + +namespace Sabre\VObject\Recur; + +use DateTimeInterface; +use Iterator; +use Sabre\VObject\DateTimeParser; + +/** + * RRuleParser. + * + * This class receives an RRULE string, and allows you to iterate to get a list + * of dates in that recurrence. + * + * For instance, passing: FREQ=DAILY;LIMIT=5 will cause the iterator to contain + * 5 items, one for each day. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class RDateIterator implements Iterator { + + /** + * Creates the Iterator. + * + * @param string|array $rrule + * @param DateTimeInterface $start + */ + function __construct($rrule, DateTimeInterface $start) { + + $this->startDate = $start; + $this->parseRDate($rrule); + $this->currentDate = clone $this->startDate; + + } + + /* Implementation of the Iterator interface {{{ */ + + function current() { + + if (!$this->valid()) return; + return clone $this->currentDate; + + } + + /** + * Returns the current item number. + * + * @return int + */ + function key() { + + return $this->counter; + + } + + /** + * Returns whether the current item is a valid item for the recurrence + * iterator. + * + * @return bool + */ + function valid() { + + return ($this->counter <= count($this->dates)); + + } + + /** + * Resets the iterator. + * + * @return void + */ + function rewind() { + + $this->currentDate = clone $this->startDate; + $this->counter = 0; + + } + + /** + * Goes on to the next iteration. + * + * @return void + */ + 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 + */ + function isInfinite() { + + return false; + + } + + /** + * This method allows you to quickly go to the next occurrence after the + * specified date. + * + * @param DateTimeInterface $dt + * + * @return void + */ + 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 + * + * @return void + */ + 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/htdocs/includes/sabre/sabre/vobject/lib/Recur/RRuleIterator.php b/htdocs/includes/sabre/sabre/vobject/lib/Recur/RRuleIterator.php new file mode 100644 index 00000000000..20f34ef42a7 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Recur/RRuleIterator.php @@ -0,0 +1,1013 @@ +<?php + +namespace Sabre\VObject\Recur; + +use DateTimeImmutable; +use DateTimeInterface; +use Iterator; +use Sabre\VObject\DateTimeParser; +use Sabre\VObject\InvalidDataException; +use Sabre\VObject\Property; + +/** + * RRuleParser. + * + * This class receives an RRULE string, and allows you to iterate to get a list + * of dates in that recurrence. + * + * For instance, passing: FREQ=DAILY;LIMIT=5 will cause the iterator to contain + * 5 items, one for each day. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class RRuleIterator implements Iterator { + + /** + * Creates the Iterator. + * + * @param string|array $rrule + * @param DateTimeInterface $start + */ + function __construct($rrule, DateTimeInterface $start) { + + $this->startDate = $start; + $this->parseRRule($rrule); + $this->currentDate = clone $this->startDate; + + } + + /* Implementation of the Iterator interface {{{ */ + + function current() { + + if (!$this->valid()) return; + return clone $this->currentDate; + + } + + /** + * Returns the current item number. + * + * @return int + */ + 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 + */ + function valid() { + + if (!is_null($this->count)) { + return $this->counter < $this->count; + } + return is_null($this->until) || $this->currentDate <= $this->until; + + } + + /** + * Resets the iterator. + * + * @return void + */ + function rewind() { + + $this->currentDate = clone $this->startDate; + $this->counter = 0; + + } + + /** + * Goes on to the next iteration. + * + * @return void + */ + 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 + */ + function isInfinite() { + + return !$this->count && !$this->until; + + } + + /** + * This method allows you to quickly go to the next occurrence after the + * specified date. + * + * @param DateTimeInterface $dt + * + * @return void + */ + 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; + + /** + * 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 preceeded 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 {{{ */ + + /** + * Does the processing for advancing the iterator for hourly frequency. + * + * @return void + */ + protected function nextHourly() { + + $this->currentDate = $this->currentDate->modify('+' . $this->interval . ' hours'); + + } + + /** + * Does the processing for advancing the iterator for daily frequency. + * + * @return void + */ + protected function nextDaily() { + + if (!$this->byHour && !$this->byDay) { + $this->currentDate = $this->currentDate->modify('+' . $this->interval . ' days'); + return; + } + + if (!empty($this->byHour)) { + $recurrenceHours = $this->getHours(); + } + + if (!empty($this->byDay)) { + $recurrenceDays = $this->getDays(); + } + + if (!empty($this->byMonth)) { + $recurrenceMonths = $this->getMonths(); + } + + do { + if ($this->byHour) { + if ($this->currentDate->format('G') == '23') { + // 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'); + + } 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. + * + * @return void + */ + protected function nextWeekly() { + + if (!$this->byHour && !$this->byDay) { + $this->currentDate = $this->currentDate->modify('+' . $this->interval . ' weeks'); + return; + } + + if ($this->byHour) { + $recurrenceHours = $this->getHours(); + } + + 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->currentDate = $this->currentDate->modify('+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 || $currentHour == '0')) { + $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. + * + * @return void + */ + 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->currentDate = $this->currentDate->modify('+' . $this->interval . ' months'); + } else { + $increase = 0; + do { + $increase++; + $tempDate = clone $this->currentDate; + $tempDate = $tempDate->modify('+ ' . ($this->interval * $increase) . ' months'); + } while ($tempDate->format('j') != $currentDayOfMonth); + $this->currentDate = $tempDate; + } + return; + } + + 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; + + } + + $this->currentDate = $this->currentDate->setDate( + (int)$this->currentDate->format('Y'), + (int)$this->currentDate->format('n'), + (int)$occurrence + ); + + } + + /** + * Does the processing for advancing the iterator for yearly frequency. + * + * @return void + */ + 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 ($currentMonth == 2 && $currentDayOfMonth == 29) { + + $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 ($nextDate->format('n') != 2); + + $this->currentDate = $nextDate; + + return; + + } + + if ($this->byWeekNo !== null) { // 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->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 ($this->byYearDay !== null) { // 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; + $date->setDate($currentYear, 1, 1); + if ($byYearDay > 0) { + $date->add(new \DateInterval('P' . $byYearDay . 'D')); + } else { + $date->sub(new \DateInterval('P' . abs($byYearDay) . '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->currentDate = $this->currentDate->modify('+' . $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) { + + 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) { + 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 + ); + + } + + // If we made it here, it means we got a valid occurrence + $this->currentDate = $this->currentDate->setDate( + (int)$currentYear, + (int)$currentMonth, + (int)$occurrence + ); + 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 + ); + + return; + + } + + } + + /* }}} */ + + /** + * This method receives a string from an RRULE property, and populates this + * class with all the values. + * + * @param string|array $rrule + * + * @return void + */ + 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' : + // No break + + 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 || (int)$byYearDay == 0 || (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 || (int)$byWeekNo == 0 || (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) betweeen 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 preceeded 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/htdocs/includes/sabre/sabre/vobject/lib/Settings.php b/htdocs/includes/sabre/sabre/vobject/lib/Settings.php new file mode 100644 index 00000000000..3f274ba8eee --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Settings.php @@ -0,0 +1,56 @@ +<?php + +namespace Sabre\VObject; + +/** + * This class provides a list of global defaults for vobject. + * + * Some of these started to appear in various classes, so it made a bit more + * sense to centralize them, so it's easier for user to find and change these. + * + * The global nature of them does mean that changing the settings for one + * instance has a global influence. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Settings { + + /** + * The minimum date we accept for various calculations with dates, such as + * recurrences. + * + * The choice of 1900 is pretty arbitrary, but it covers most common + * use-cases. In particular, it covers birthdates for virtually everyone + * alive on earth, which is less than 5 people at the time of writing. + */ + static $minDate = '1900-01-01'; + + /** + * The maximum date we accept for various calculations with dates, such as + * recurrences. + * + * The choice of 2100 is pretty arbitrary, but should cover most + * appointments made for many years to come. + */ + static $maxDate = '2100-01-01'; + + /** + * The maximum number of recurrences that will be generated. + * + * This setting limits the maximum of recurring events that this library + * generates in its recurrence iterators. + * + * This is a security measure. Without this, it would be possible to craft + * specific events that recur many, many times, potentially DDOSing the + * server. + * + * The default (3500) allows creation of a dialy event that goes on for 10 + * years, which is hopefully long enough for most. + * + * Set this value to -1 to disable this control altogether. + */ + static $maxRecurrences = 3500; + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Splitter/ICalendar.php b/htdocs/includes/sabre/sabre/vobject/lib/Splitter/ICalendar.php new file mode 100644 index 00000000000..c0007ba01ae --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Splitter/ICalendar.php @@ -0,0 +1,113 @@ +<?php + +namespace Sabre\VObject\Splitter; + +use Sabre\VObject; +use Sabre\VObject\Component\VCalendar; + +/** + * Splitter. + * + * This class is responsible for splitting up iCalendar objects. + * + * This class expects a single VCALENDAR object with one or more + * calendar-objects inside. Objects with identical UID's will be combined into + * a single object. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Dominik Tobschall (http://tobschall.de/) + * @author Armin Hackmann + * @license http://sabre.io/license/ Modified BSD License + */ +class ICalendar implements SplitterInterface { + + /** + * Timezones. + * + * @var array + */ + protected $vtimezones = []; + + /** + * iCalendar objects. + * + * @var array + */ + protected $objects = []; + + /** + * Constructor. + * + * The splitter should receive an readable file stream as it's input. + * + * @param resource $input + * @param int $options Parser options, see the OPTIONS constants. + */ + function __construct($input, $options = 0) { + + $data = VObject\Reader::read($input, $options); + + if (!$data instanceof VObject\Component\VCalendar) { + throw new VObject\ParseException('Supplied input could not be parsed as VCALENDAR.'); + } + + foreach ($data->children() as $component) { + if (!$component instanceof VObject\Component) { + continue; + } + + // Get all timezones + if ($component->name === 'VTIMEZONE') { + $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 + */ + 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/htdocs/includes/sabre/sabre/vobject/lib/Splitter/SplitterInterface.php b/htdocs/includes/sabre/sabre/vobject/lib/Splitter/SplitterInterface.php new file mode 100644 index 00000000000..8f827cc4b47 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Splitter/SplitterInterface.php @@ -0,0 +1,39 @@ +<?php + +namespace Sabre\VObject\Splitter; + +/** + * VObject splitter. + * + * The splitter is responsible for reading a large vCard or iCalendar object, + * and splitting it into multiple objects. + * + * This is for example for Card and CalDAV, which require every event and vcard + * to exist in their own objects, instead of one large one. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Dominik Tobschall (http://tobschall.de/) + * @license http://sabre.io/license/ Modified BSD License + */ +interface SplitterInterface { + + /** + * Constructor. + * + * The splitter should receive an readable file stream as it's input. + * + * @param resource $input + */ + function __construct($input); + + /** + * 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 + */ + function getNext(); + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Splitter/VCard.php b/htdocs/includes/sabre/sabre/vobject/lib/Splitter/VCard.php new file mode 100644 index 00000000000..0bb82abe93b --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Splitter/VCard.php @@ -0,0 +1,78 @@ +<?php + +namespace Sabre\VObject\Splitter; + +use Sabre\VObject; +use Sabre\VObject\Parser\MimeDir; + +/** + * Splitter. + * + * This class is responsible for splitting up VCard objects. + * + * It is assumed that the input stream contains 1 or more VCARD objects. This + * class checks for BEGIN:VCARD and END:VCARD and parses each encountered + * component individually. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Dominik Tobschall (http://tobschall.de/) + * @author Armin Hackmann + * @license http://sabre.io/license/ Modified BSD License + */ +class VCard implements SplitterInterface { + + /** + * File handle. + * + * @var resource + */ + protected $input; + + /** + * Persistent parser. + * + * @var MimeDir + */ + protected $parser; + + /** + * Constructor. + * + * The splitter should receive an readable file stream as it's input. + * + * @param resource $input + * @param int $options Parser options, see the OPTIONS constants. + */ + function __construct($input, $options = 0) { + + $this->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 + */ + 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/htdocs/includes/sabre/sabre/vobject/lib/StringUtil.php b/htdocs/includes/sabre/sabre/vobject/lib/StringUtil.php new file mode 100644 index 00000000000..b8615f2ba12 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/StringUtil.php @@ -0,0 +1,66 @@ +<?php + +namespace Sabre\VObject; + +/** + * Useful utilities for working with various strings. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class StringUtil { + + /** + * Returns true or false depending on if a string is valid UTF-8. + * + * @param string $str + * + * @return bool + */ + static function isUTF8($str) { + + // Control characters + if (preg_match('%[\x00-\x08\x0B-\x0C\x0E\x0F]%', $str)) { + return false; + } + + return (bool)preg_match('%%u', $str); + + } + + /** + * This method tries its best to convert the input string to UTF-8. + * + * Currently only ISO-5991-1 input and UTF-8 input is supported, but this + * may be expanded upon if we receive other examples. + * + * @param string $str + * + * @return string + */ + static function convertToUTF8($str) { + + $encoding = mb_detect_encoding($str, ['UTF-8', 'ISO-8859-1', 'WINDOWS-1252'], true); + + switch ($encoding) { + case 'ISO-8859-1' : + $newStr = utf8_encode($str); + break; + /* Unreachable code. Not sure yet how we can improve this + * situation. + case 'WINDOWS-1252' : + $newStr = iconv('cp1252', 'UTF-8', $str); + break; + */ + default : + $newStr = $str; + + } + + // Removing any control characters + return (preg_replace('%(?:[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F])%', '', $newStr)); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/TimeZoneUtil.php b/htdocs/includes/sabre/sabre/vobject/lib/TimeZoneUtil.php new file mode 100644 index 00000000000..925183e8d94 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/TimeZoneUtil.php @@ -0,0 +1,276 @@ +<?php + +namespace Sabre\VObject; + +/** + * Time zone name translation. + * + * This file translates well-known time zone names into "Olson database" time zone names. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Frank Edelhaeuser (fedel@users.sourceforge.net) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class TimeZoneUtil { + + static $map = null; + + /** + * List of microsoft exchange timezone ids. + * + * Source: http://msdn.microsoft.com/en-us/library/aa563018(loband).aspx + */ + 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 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. + * + * @param string $tzid + * @param Sabre\VObject\Component $vcalendar + * + * @return DateTimeZone + */ + static function getTimeZone($tzid, Component $vcalendar = null, $failIfUncertain = false) { + + // First we will just see if the tzid is a support timezone identifier. + // + // The only exception is if the timezone starts with (. This is to + // handle cases where certain microsoft products generate timezone + // identifiers that for instance look like: + // + // (GMT+01.00) Sarajevo/Warsaw/Zagreb + // + // Since PHP 5.5.10, the first bit will be used as the timezone and + // this method will return just GMT+01:00. This is wrong, because it + // doesn't take DST into account. + if ($tzid[0] !== '(') { + + // PHP has a bug that logs PHP warnings even it shouldn't: + // https://bugs.php.net/bug.php?id=67881 + // + // That's why we're checking if we'll be able to successfull instantiate + // \DateTimeZone() before doing so. Otherwise we could simply instantiate + // and catch the exception. + $tzIdentifiers = \DateTimeZone::listIdentifiers(); + + try { + if ( + (in_array($tzid, $tzIdentifiers)) || + (preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches)) || + (in_array($tzid, self::getIdentifiersBC())) + ) { + return new \DateTimeZone($tzid); + } + } catch (\Exception $e) { + } + + } + + self::loadTzMaps(); + + // Next, we check if the tzid is somewhere in our tzid map. + if (isset(self::$map[$tzid])) { + return new \DateTimeZone(self::$map[$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. + if (preg_match('/^\((UTC|GMT)(\+|\-)[\d]{2}\:[\d]{2}\) (.*)/', $tzid, $matches)) { + $tzidAlternate = $matches[3]; + if (isset(self::$map[$tzidAlternate])) { + return new \DateTimeZone(self::$map[$tzidAlternate]); + } + } + + // Maybe the author was hyper-lazy and just included an offset. We + // support it, but we aren't happy about it. + if (preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches)) { + + // Note that the path in the source will never be taken from PHP 5.5.10 + // onwards. PHP 5.5.10 supports the "GMT+0100" style of format, so it + // already gets returned early in this function. Once we drop support + // for versions under PHP 5.5.10, this bit can be taken out of the + // source. + // @codeCoverageIgnoreStart + return new \DateTimeZone('Etc/GMT' . $matches[1] . ltrim(substr($matches[2], 0, 2), '0')); + // @codeCoverageIgnoreEnd + } + + if ($vcalendar) { + + // If that didn't work, we will scan VTIMEZONE objects + foreach ($vcalendar->select('VTIMEZONE') as $vtimezone) { + + if ((string)$vtimezone->TZID === $tzid) { + + // Some clients add 'X-LIC-LOCATION' with the olson name. + if (isset($vtimezone->{'X-LIC-LOCATION'})) { + + $lic = (string)$vtimezone->{'X-LIC-LOCATION'}; + + // Libical generators may specify strings like + // "SystemV/EST5EDT". For those we must remove the + // SystemV part. + if (substr($lic, 0, 8) === 'SystemV/') { + $lic = substr($lic, 8); + } + + return self::getTimeZone($lic, null, $failIfUncertain); + + } + // Microsoft may add a magic number, which we also have an + // answer for. + if (isset($vtimezone->{'X-MICROSOFT-CDO-TZID'})) { + $cdoId = (int)$vtimezone->{'X-MICROSOFT-CDO-TZID'}->getValue(); + + // 2 can mean both Europe/Lisbon and Europe/Sarajevo. + if ($cdoId === 2 && strpos((string)$vtimezone->TZID, 'Sarajevo') !== false) { + return new \DateTimeZone('Europe/Sarajevo'); + } + + if (isset(self::$microsoftExchangeMap[$cdoId])) { + return new \DateTimeZone(self::$microsoftExchangeMap[$cdoId]); + } + } + + } + + } + + } + + 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 UTC. + return new \DateTimeZone(date_default_timezone_get()); + + } + + /** + * This method will load in all the tz mapping information, if it's not yet + * done. + */ + 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 + */ + static function getIdentifiersBC() { + return include __DIR__ . '/timezonedata/php-bc.php'; + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/UUIDUtil.php b/htdocs/includes/sabre/sabre/vobject/lib/UUIDUtil.php new file mode 100644 index 00000000000..24ebe3cf80d --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/UUIDUtil.php @@ -0,0 +1,69 @@ +<?php + +namespace Sabre\VObject; + +/** + * UUID Utility. + * + * This class has static methods to generate and validate UUID's. + * UUIDs are used a decent amount within various *DAV standards, so it made + * sense to include it. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class UUIDUtil { + + /** + * Returns a pseudo-random v4 UUID. + * + * This function is based on a comment by Andrew Moore on php.net + * + * @see http://www.php.net/manual/en/function.uniqid.php#94959 + * + * @return string + */ + static function getUUID() { + + return sprintf( + + '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + + // 32 bits for "time_low" + mt_rand(0, 0xffff), mt_rand(0, 0xffff), + + // 16 bits for "time_mid" + mt_rand(0, 0xffff), + + // 16 bits for "time_hi_and_version", + // four most significant bits holds version number 4 + mt_rand(0, 0x0fff) | 0x4000, + + // 16 bits, 8 bits for "clk_seq_hi_res", + // 8 bits for "clk_seq_low", + // two most significant bits holds zero and one for variant DCE1.1 + mt_rand(0, 0x3fff) | 0x8000, + + // 48 bits for "node" + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) + ); + } + + /** + * Checks if a string is a valid UUID. + * + * @param string $uuid + * + * @return bool + */ + static function validateUUID($uuid) { + + return preg_match( + '/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i', + $uuid + ) !== 0; + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/VCardConverter.php b/htdocs/includes/sabre/sabre/vobject/lib/VCardConverter.php new file mode 100644 index 00000000000..1f6d016f142 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/VCardConverter.php @@ -0,0 +1,467 @@ +<?php + +namespace Sabre\VObject; + +/** + * This utility converts vcards from one version to another. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class VCardConverter { + + /** + * Converts a vCard object to a new version. + * + * targetVersion must be one of: + * Document::VCARD21 + * Document::VCARD30 + * Document::VCARD40 + * + * Currently only 3.0 and 4.0 as input and output versions. + * + * 2.1 has some minor support for the input version, it's incomplete at the + * moment though. + * + * If input and output version are identical, a clone is returned. + * + * @param Component\VCard $input + * @param int $targetVersion + */ + function convert(Component\VCard $input, $targetVersion) { + + $inputVersion = $input->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 = $targetVersion === Document::VCARD40 ? '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 Component\VCard $input + * @param Component\VCard $output + * @param Property $property + * @param int $targetVersion + * + * @return void + */ + 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(); + } + $newProperty = $output->createProperty( + $property->name, + $property->getParts(), + [], // parameters will get added a bit later. + $valueType + ); + + + if ($targetVersion === Document::VCARD30) { + + 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 ($newProperty->name == 'ANNIVERSARY') { + // 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', '_$!<Anniversary>!$_'); + } + + } elseif ($property->name === 'KIND') { + + 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 ($targetVersion === Document::VCARD40) { + + // 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 (strtoupper($property->getValue()) === 'COMPANY') { + $newProperty = $output->createProperty('KIND', 'ORG'); + } + break; + case 'X-ADDRESSBOOKSERVER-KIND' : + if (strtoupper($property->getValue()) === 'GROUP') { + $newProperty = $output->createProperty('KIND', 'GROUP'); + } + 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() !== '_$!<Anniversary>!$_') { + 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() === '_$!<Anniversary>!$_') { + // We can safely remove these, as they are converted to + // ANNIVERSARY properties. + return; + } + break; + + } + + } + + // set property group + $newProperty->group = $property->group; + + if ($targetVersion === Document::VCARD40) { + $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 instantating 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 Component\VCard $output + * @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 Component\VCard $output + * @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 (substr($value, 0, 5) !== 'data:') { + 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. + * + * @param Property $newProperty + * @param array $parameters + * + * @return void + */ + 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 (strtoupper($paramPart) === 'PREF') { + $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. + * + * @param Property $newProperty + * @param array $parameters + * + * @return void + */ + 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 (strtoupper($param->getValue()) !== 'QUOTED-PRINTABLE') { + $newProperty->add($param->name, $param->getParts()); + } + break; + + /* + * Converting PREF=1 to TYPE=PREF. + * + * Any other PREF numbers we'll drop. + */ + case 'PREF' : + if ($param->getValue() == '1') { + $newProperty->add('TYPE', 'PREF'); + } + break; + + default : + $newProperty->add($param->name, $param->getParts()); + break; + + } + + } + + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Version.php b/htdocs/includes/sabre/sabre/vobject/lib/Version.php new file mode 100644 index 00000000000..346e2044df1 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Version.php @@ -0,0 +1,19 @@ +<?php + +namespace Sabre\VObject; + +/** + * This class contains the version number for the VObject package. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Version { + + /** + * Full version number. + */ + const VERSION = '4.1.2'; + +} diff --git a/htdocs/includes/sabre/sabre/vobject/lib/Writer.php b/htdocs/includes/sabre/sabre/vobject/lib/Writer.php new file mode 100644 index 00000000000..f8a58758d6c --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/Writer.php @@ -0,0 +1,81 @@ +<?php + +namespace Sabre\VObject; + +use Sabre\Xml; + +/** + * iCalendar/vCard/jCal/jCard/xCal/xCard writer object. + * + * This object provides a few (static) convenience methods to quickly access + * the serializers. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Ivan Enderlin + * @license http://sabre.io/license/ Modified BSD License + */ +class Writer { + + /** + * Serializes a vCard or iCalendar object. + * + * @param Component $component + * + * @return string + */ + static function write(Component $component) { + + return $component->serialize(); + + } + + /** + * Serializes a jCal or jCard object. + * + * @param Component $component + * @param int $options + * + * @return string + */ + static function writeJson(Component $component, $options = 0) { + + return json_encode($component, $options); + + } + + /** + * Serializes a xCal or xCard object. + * + * @param Component $component + * + * @return string + */ + 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/htdocs/includes/sabre/sabre/vobject/lib/timezonedata/exchangezones.php b/htdocs/includes/sabre/sabre/vobject/lib/timezonedata/exchangezones.php new file mode 100644 index 00000000000..38138354a76 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/timezonedata/exchangezones.php @@ -0,0 +1,93 @@ +<?php + +/** + * Microsoft exchange timezones + * Source: + * http://msdn.microsoft.com/en-us/library/ms988620%28v=exchg.65%29.aspx. + * + * Correct timezones deduced with help from: + * http://en.wikipedia.org/wiki/List_of_tz_database_time_zones + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +return [ + 'Universal Coordinated Time' => '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', + '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); 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/htdocs/includes/sabre/sabre/vobject/lib/timezonedata/lotuszones.php b/htdocs/includes/sabre/sabre/vobject/lib/timezonedata/lotuszones.php new file mode 100644 index 00000000000..79d555a92f0 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/timezonedata/lotuszones.php @@ -0,0 +1,101 @@ +<?php + +/** + * The following list are timezone names that could be generated by + * Lotus / Domino. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +return [ + 'Dateline' => '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/htdocs/includes/sabre/sabre/vobject/lib/timezonedata/php-bc.php b/htdocs/includes/sabre/sabre/vobject/lib/timezonedata/php-bc.php new file mode 100644 index 00000000000..906ccb0e4d4 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/timezonedata/php-bc.php @@ -0,0 +1,154 @@ +<?php + +/** + * A list of additional PHP timezones that are returned by + * DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC) + * valid for new DateTimeZone(). + * + * This list does not include those timezone identifiers that we have to map to + * a different identifier for some PHP versions (see php-workaround.php). + * + * Instead of using DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC) + * directly, we use this file because DateTimeZone::ALL_WITH_BC is not properly + * supported by all PHP version and HHVM. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +return [ + 'Africa/Asmera', + 'Africa/Timbuktu', + 'America/Argentina/ComodRivadavia', + 'America/Atka', + 'America/Buenos_Aires', + 'America/Catamarca', + 'America/Coral_Harbour', + 'America/Cordoba', + 'America/Ensenada', + 'America/Fort_Wayne', + 'America/Indianapolis', + 'America/Jujuy', + 'America/Knox_IN', + 'America/Louisville', + 'America/Mendoza', + 'America/Montreal', + 'America/Porto_Acre', + 'America/Rosario', + 'America/Shiprock', + 'America/Virgin', + 'Antarctica/South_Pole', + 'Asia/Ashkhabad', + 'Asia/Calcutta', + 'Asia/Chungking', + 'Asia/Dacca', + 'Asia/Istanbul', + 'Asia/Katmandu', + 'Asia/Macao', + 'Asia/Saigon', + 'Asia/Tel_Aviv', + 'Asia/Thimbu', + 'Asia/Ujung_Pandang', + 'Asia/Ulan_Bator', + 'Atlantic/Faeroe', + 'Atlantic/Jan_Mayen', + 'Australia/ACT', + 'Australia/Canberra', + 'Australia/LHI', + 'Australia/North', + 'Australia/NSW', + 'Australia/Queensland', + 'Australia/South', + 'Australia/Tasmania', + 'Australia/Victoria', + 'Australia/West', + 'Australia/Yancowinna', + 'Brazil/Acre', + 'Brazil/DeNoronha', + 'Brazil/East', + 'Brazil/West', + 'Canada/Atlantic', + 'Canada/Central', + 'Canada/East-Saskatchewan', + 'Canada/Eastern', + 'Canada/Mountain', + 'Canada/Newfoundland', + 'Canada/Pacific', + 'Canada/Saskatchewan', + 'Canada/Yukon', + 'CET', + 'Chile/Continental', + 'Chile/EasterIsland', + 'EET', + 'EST', + 'Etc/GMT', + 'Etc/GMT+0', + 'Etc/GMT+1', + 'Etc/GMT+10', + 'Etc/GMT+11', + 'Etc/GMT+12', + 'Etc/GMT+2', + 'Etc/GMT+3', + 'Etc/GMT+4', + 'Etc/GMT+5', + 'Etc/GMT+6', + 'Etc/GMT+7', + 'Etc/GMT+8', + 'Etc/GMT+9', + 'Etc/GMT-0', + 'Etc/GMT-1', + 'Etc/GMT-10', + 'Etc/GMT-11', + 'Etc/GMT-12', + 'Etc/GMT-13', + 'Etc/GMT-14', + 'Etc/GMT-2', + 'Etc/GMT-3', + 'Etc/GMT-4', + 'Etc/GMT-5', + 'Etc/GMT-6', + 'Etc/GMT-7', + 'Etc/GMT-8', + 'Etc/GMT-9', + 'Etc/GMT0', + 'Etc/Greenwich', + 'Etc/UCT', + 'Etc/Universal', + 'Etc/UTC', + 'Etc/Zulu', + 'Europe/Belfast', + 'Europe/Nicosia', + 'Europe/Tiraspol', + 'GB', + 'GMT', + 'GMT+0', + 'GMT-0', + 'HST', + 'MET', + 'Mexico/BajaNorte', + 'Mexico/BajaSur', + 'Mexico/General', + 'MST', + 'NZ', + 'Pacific/Ponape', + 'Pacific/Samoa', + 'Pacific/Truk', + 'Pacific/Yap', + 'PRC', + 'ROC', + 'ROK', + 'UCT', + 'US/Alaska', + 'US/Aleutian', + 'US/Arizona', + 'US/Central', + 'US/East-Indiana', + 'US/Eastern', + 'US/Hawaii', + 'US/Indiana-Starke', + 'US/Michigan', + 'US/Mountain', + 'US/Pacific', + 'US/Pacific-New', + 'US/Samoa', + 'WET', +]; diff --git a/htdocs/includes/sabre/sabre/vobject/lib/timezonedata/php-workaround.php b/htdocs/includes/sabre/sabre/vobject/lib/timezonedata/php-workaround.php new file mode 100644 index 00000000000..6b9cb6ef745 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/timezonedata/php-workaround.php @@ -0,0 +1,46 @@ +<?php + +/** + * A list of PHP timezones that were supported until 5.5.9, removed in + * PHP 5.5.10 and re-introduced in PHP 5.5.17. + * + * DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC) returns them, + * but they are invalid for new DateTimeZone(). Fixed in PHP 5.5.17. + * https://bugs.php.net/bug.php?id=66985 + * + * Some more info here: + * http://evertpot.com/php-5-5-10-timezone-changes/ + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +return [ + 'CST6CDT' => '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/htdocs/includes/sabre/sabre/vobject/lib/timezonedata/windowszones.php b/htdocs/includes/sabre/sabre/vobject/lib/timezonedata/windowszones.php new file mode 100644 index 00000000000..29f3a6cb809 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/lib/timezonedata/windowszones.php @@ -0,0 +1,143 @@ +<?php + +/** + * Automatically generated timezone file + * + * Last update: 2016-08-24T17:35:38-04:00 + * Source: http://unicode.org/repos/cldr/trunk/common/supplemental/windowsZones.xml + * + * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/). + * @license http://sabre.io/license/ Modified BSD License + */ + +return [ + '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' => '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', + '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', + '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', + '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', + 'Singapore Standard Time' => 'Asia/Singapore', + 'South Africa Standard Time' => 'Africa/Johannesburg', + 'Sri Lanka Standard Time' => 'Asia/Colombo', + '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-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', +]; diff --git a/htdocs/includes/sabre/sabre/vobject/resources/schema/xcal.rng b/htdocs/includes/sabre/sabre/vobject/resources/schema/xcal.rng new file mode 100644 index 00000000000..4a51460e742 --- /dev/null +++ b/htdocs/includes/sabre/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/htdocs/includes/sabre/sabre/vobject/resources/schema/xcard.rng b/htdocs/includes/sabre/sabre/vobject/resources/schema/xcard.rng new file mode 100644 index 00000000000..c0b7cfb3545 --- /dev/null +++ b/htdocs/includes/sabre/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/htdocs/includes/sabre/sabre/vobject/tests/VObject/AttachIssueTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/AttachIssueTest.php new file mode 100644 index 00000000000..68c9872bb5b --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/AttachIssueTest.php @@ -0,0 +1,22 @@ +<?php + +namespace Sabre\VObject; + +class AttachIssueTest extends \PHPUnit_Framework_TestCase { + + function testRead() { + + $event = <<<ICS +BEGIN:VCALENDAR\r +BEGIN:VEVENT\r +ATTACH;FMTTYPE=;ENCODING=:Zm9v\r +END:VEVENT\r +END:VCALENDAR\r + +ICS; + $obj = Reader::read($event); + $this->assertEquals($event, $obj->serialize()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/BirthdayCalendarGeneratorTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/BirthdayCalendarGeneratorTest.php new file mode 100644 index 00000000000..54c478eaf46 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/BirthdayCalendarGeneratorTest.php @@ -0,0 +1,562 @@ +<?php + +namespace Sabre\VObject; + +class BirthdayCalendarGeneratorTest extends \PHPUnit_Framework_TestCase { + + use PHPUnitAssertions; + + function testVcardStringWithValidBirthday() { + + $generator = new BirthdayCalendarGenerator(); + $input = <<<VCF +BEGIN:VCARD +VERSION:3.0 +N:Gump;Forrest;;Mr. +FN:Forrest Gump +BDAY:19850407 +UID:foo +END:VCARD +VCF; + + $expected = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:**ANY** +DTSTAMP:**ANY** +SUMMARY:Forrest Gump's Birthday +DTSTART;VALUE=DATE:19850407 +RRULE:FREQ=YEARLY +TRANSP:TRANSPARENT +X-SABRE-BDAY;X-SABRE-VCARD-UID=foo;X-SABRE-VCARD-FN=Forrest Gump:BDAY +END:VEVENT +END:VCALENDAR +ICS; + + $generator->setObjects($input); + $output = $generator->getResult(); + + $this->assertVObjectEqualsVObject( + $expected, + $output + ); + + } + + function testArrayOfVcardStringsWithValidBirthdays() { + + $generator = new BirthdayCalendarGenerator(); + $input = []; + + $input[] = <<<VCF +BEGIN:VCARD +VERSION:3.0 +N:Gump;Forrest;;Mr. +FN:Forrest Gump +BDAY:19850407 +UID:foo +END:VCARD +VCF; + + $input[] = <<<VCF +BEGIN:VCARD +VERSION:3.0 +N:Doe;John;;Mr. +FN:John Doe +BDAY:19820210 +UID:bar +END:VCARD +VCF; + + $expected = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:**ANY** +DTSTAMP:**ANY** +SUMMARY:Forrest Gump's Birthday +DTSTART;VALUE=DATE:19850407 +RRULE:FREQ=YEARLY +TRANSP:TRANSPARENT +X-SABRE-BDAY;X-SABRE-VCARD-UID=foo;X-SABRE-VCARD-FN=Forrest Gump:BDAY +END:VEVENT +BEGIN:VEVENT +UID:**ANY** +DTSTAMP:**ANY** +SUMMARY:John Doe's Birthday +DTSTART;VALUE=DATE:19820210 +RRULE:FREQ=YEARLY +TRANSP:TRANSPARENT +X-SABRE-BDAY;X-SABRE-VCARD-UID=bar;X-SABRE-VCARD-FN=John Doe:BDAY +END:VEVENT +END:VCALENDAR +ICS; + + $generator->setObjects($input); + $output = $generator->getResult(); + + $this->assertVObjectEqualsVObject( + $expected, + $output + ); + + } + + function testArrayOfVcardStringsWithValidBirthdaysViaConstructor() { + + $input = []; + + $input[] = <<<VCF +BEGIN:VCARD +VERSION:3.0 +N:Gump;Forrest;;Mr. +FN:Forrest Gump +BDAY:19850407 +UID:foo +END:VCARD +VCF; + + $input[] = <<<VCF +BEGIN:VCARD +VERSION:3.0 +N:Doe;John;;Mr. +FN:John Doe +BDAY:19820210 +UID:bar +END:VCARD +VCF; + + $generator = new BirthdayCalendarGenerator($input); + + $expected = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:**ANY** +DTSTAMP:**ANY** +SUMMARY:Forrest Gump's Birthday +DTSTART;VALUE=DATE:19850407 +RRULE:FREQ=YEARLY +TRANSP:TRANSPARENT +X-SABRE-BDAY;X-SABRE-VCARD-UID=foo;X-SABRE-VCARD-FN=Forrest Gump:BDAY +END:VEVENT +BEGIN:VEVENT +UID:**ANY** +DTSTAMP:**ANY** +SUMMARY:John Doe's Birthday +DTSTART;VALUE=DATE:19820210 +RRULE:FREQ=YEARLY +TRANSP:TRANSPARENT +X-SABRE-BDAY;X-SABRE-VCARD-UID=bar;X-SABRE-VCARD-FN=John Doe:BDAY +END:VEVENT +END:VCALENDAR +ICS; + + $generator->setObjects($input); + $output = $generator->getResult(); + + $this->assertVObjectEqualsVObject( + $expected, + $output + ); + + } + + function testVcardObjectWithValidBirthday() { + + $generator = new BirthdayCalendarGenerator(); + $input = <<<VCF +BEGIN:VCARD +VERSION:3.0 +N:Gump;Forrest;;Mr. +FN:Forrest Gump +BDAY:19850407 +UID:foo +END:VCARD +VCF; + + $input = Reader::read($input); + + $expected = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:**ANY** +DTSTAMP:**ANY** +SUMMARY:Forrest Gump's Birthday +DTSTART;VALUE=DATE:19850407 +RRULE:FREQ=YEARLY +TRANSP:TRANSPARENT +X-SABRE-BDAY;X-SABRE-VCARD-UID=foo;X-SABRE-VCARD-FN=Forrest Gump:BDAY +END:VEVENT +END:VCALENDAR +ICS; + + $generator->setObjects($input); + $output = $generator->getResult(); + + $this->assertVObjectEqualsVObject( + $expected, + $output + ); + + } + + function testArrayOfVcardObjectsWithValidBirthdays() { + + $generator = new BirthdayCalendarGenerator(); + $input = []; + + $input[] = <<<VCF +BEGIN:VCARD +VERSION:3.0 +N:Gump;Forrest;;Mr. +FN:Forrest Gump +BDAY:19850407 +UID:foo +END:VCARD +VCF; + + $input[] = <<<VCF +BEGIN:VCARD +VERSION:3.0 +N:Doe;John;;Mr. +FN:John Doe +BDAY:19820210 +UID:bar +END:VCARD +VCF; + + foreach ($input as $key => $value) { + $input[$key] = Reader::read($value); + } + + $expected = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:**ANY** +DTSTAMP:**ANY** +SUMMARY:Forrest Gump's Birthday +DTSTART;VALUE=DATE:19850407 +RRULE:FREQ=YEARLY +TRANSP:TRANSPARENT +X-SABRE-BDAY;X-SABRE-VCARD-UID=foo;X-SABRE-VCARD-FN=Forrest Gump:BDAY +END:VEVENT +BEGIN:VEVENT +UID:**ANY** +DTSTAMP:**ANY** +SUMMARY:John Doe's Birthday +DTSTART;VALUE=DATE:19820210 +RRULE:FREQ=YEARLY +TRANSP:TRANSPARENT +X-SABRE-BDAY;X-SABRE-VCARD-UID=bar;X-SABRE-VCARD-FN=John Doe:BDAY +END:VEVENT +END:VCALENDAR +ICS; + + $generator->setObjects($input); + $output = $generator->getResult(); + + $this->assertVObjectEqualsVObject( + $expected, + $output + ); + + } + + function testVcardStringWithValidBirthdayWithXAppleOmitYear() { + + $generator = new BirthdayCalendarGenerator(); + $input = <<<VCF +BEGIN:VCARD +VERSION:3.0 +N:Gump;Forrest;;Mr. +FN:Forrest Gump +BDAY;X-APPLE-OMIT-YEAR=1604:1604-04-07 +UID:foo +END:VCARD +VCF; + + $expected = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:**ANY** +DTSTAMP:**ANY** +SUMMARY:Forrest Gump's Birthday +DTSTART;VALUE=DATE:20000407 +RRULE:FREQ=YEARLY +TRANSP:TRANSPARENT +X-SABRE-BDAY;X-SABRE-VCARD-UID=foo;X-SABRE-VCARD-FN=Forrest Gump;X-SABRE-OMIT-YEAR=2000:BDAY +END:VEVENT +END:VCALENDAR +ICS; + + $generator->setObjects($input); + $output = $generator->getResult(); + + $this->assertVObjectEqualsVObject( + $expected, + $output + ); + + } + + function testVcardStringWithValidBirthdayWithoutYear() { + + $generator = new BirthdayCalendarGenerator(); + $input = <<<VCF +BEGIN:VCARD +VERSION:4.0 +N:Gump;Forrest;;Mr. +FN:Forrest Gump +BDAY:--04-07 +UID:foo +END:VCARD +VCF; + + $expected = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:**ANY** +DTSTAMP:**ANY** +SUMMARY:Forrest Gump's Birthday +DTSTART;VALUE=DATE:20000407 +RRULE:FREQ=YEARLY +TRANSP:TRANSPARENT +X-SABRE-BDAY;X-SABRE-VCARD-UID=foo;X-SABRE-VCARD-FN=Forrest Gump;X-SABRE-OMIT-YEAR=2000:BDAY +END:VEVENT +END:VCALENDAR +ICS; + + $generator->setObjects($input); + $output = $generator->getResult(); + + $this->assertVObjectEqualsVObject( + $expected, + $output + ); + + } + + function testVcardStringWithInvalidBirthday() { + + $generator = new BirthdayCalendarGenerator(); + $input = <<<VCF +BEGIN:VCARD +VERSION:3.0 +N:Gump;Forrest;;Mr. +FN:Forrest Gump +BDAY:foo +X-SABRE-BDAY;X-SABRE-VCARD-UID=foo;X-SABRE-VCARD-FN=Forrest Gump:BDAY +END:VCARD +VCF; + + $expected = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +END:VCALENDAR +ICS; + + $generator->setObjects($input); + $output = $generator->getResult(); + + $this->assertVObjectEqualsVObject( + $expected, + $output + ); + + } + + function testVcardStringWithNoBirthday() { + + $generator = new BirthdayCalendarGenerator(); + $input = <<<VCF +BEGIN:VCARD +VERSION:3.0 +N:Gump;Forrest;;Mr. +FN:Forrest Gump +UID:foo +END:VCARD +VCF; + + $expected = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +END:VCALENDAR +ICS; + + $generator->setObjects($input); + $output = $generator->getResult(); + + $this->assertVObjectEqualsVObject( + $expected, + $output + ); + + } + + function testVcardStringWithValidBirthdayLocalized() { + + $generator = new BirthdayCalendarGenerator(); + $input = <<<VCF +BEGIN:VCARD +VERSION:3.0 +N:Gump;Forrest;;Mr. +FN:Forrest Gump +BDAY:19850407 +UID:foo +END:VCARD +VCF; + + $expected = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:**ANY** +DTSTAMP:**ANY** +SUMMARY:Forrest Gump's Geburtstag +DTSTART;VALUE=DATE:19850407 +RRULE:FREQ=YEARLY +TRANSP:TRANSPARENT +X-SABRE-BDAY;X-SABRE-VCARD-UID=foo;X-SABRE-VCARD-FN=Forrest Gump:BDAY +END:VEVENT +END:VCALENDAR +ICS; + + $generator->setObjects($input); + $generator->setFormat('%1$s\'s Geburtstag'); + $output = $generator->getResult(); + + $this->assertVObjectEqualsVObject( + $expected, + $output + ); + + } + + function testVcardStringWithEmptyBirthdayProperty() { + + $generator = new BirthdayCalendarGenerator(); + $input = <<<VCF +BEGIN:VCARD +VERSION:3.0 +N:Gump;Forrest;;Mr. +FN:Forrest Gump +BDAY: +UID:foo +END:VCARD +VCF; + + $expected = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +END:VCALENDAR +ICS; + + $generator->setObjects($input); + $output = $generator->getResult(); + + $this->assertVObjectEqualsVObject( + $expected, + $output + ); + + } + + /** + * @expectedException \Sabre\VObject\ParseException + */ + function testParseException() { + + $generator = new BirthdayCalendarGenerator(); + $input = <<<FOO +BEGIN:FOO +FOO:Bar +END:FOO +FOO; + + $generator->setObjects($input); + + } + + /** + * @expectedException \InvalidArgumentException + */ + function testInvalidArgumentException() { + + $generator = new BirthdayCalendarGenerator(); + $input = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +SUMMARY:Foo +DTSTART;VALUE=DATE:19850407 +END:VEVENT +END:VCALENDAR +ICS; + + $generator->setObjects($input); + + } + + /** + * @expectedException \InvalidArgumentException + */ + function testInvalidArgumentExceptionForPartiallyInvalidArray() { + + $generator = new BirthdayCalendarGenerator(); + $input = []; + + $input[] = <<<VCF +BEGIN:VCARD +VERSION:3.0 +N:Gump;Forrest;;Mr. +FN:Forrest Gump +BDAY:19850407 +UID:foo +END:VCARD +VCF; + $calendar = new Component\VCalendar(); + + $input = $calendar->add('VEVENT', [ + 'SUMMARY' => 'Foo', + 'DTSTART' => new \DateTime('NOW'), + ]); + + $generator->setObjects($input); + + } + + function testBrokenVcardWithoutFN() { + + $generator = new BirthdayCalendarGenerator(); + $input = <<<VCF +BEGIN:VCARD +VERSION:3.0 +N:Gump;Forrest;;Mr. +BDAY:19850407 +UID:foo +END:VCARD +VCF; + + $expected = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +END:VCALENDAR +ICS; + + $generator->setObjects($input); + $output = $generator->getResult(); + + $this->assertVObjectEqualsVObject( + $expected, + $output + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/CliTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/CliTest.php new file mode 100644 index 00000000000..67037873e37 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/CliTest.php @@ -0,0 +1,642 @@ +<?php + +namespace Sabre\VObject; + +/** + * Tests the cli. + * + * Warning: these tests are very rudimentary. + */ +class CliTest extends \PHPUnit_Framework_TestCase { + + function setUp() { + + $this->cli = new CliMock(); + $this->cli->stderr = fopen('php://memory', 'r+'); + $this->cli->stdout = fopen('php://memory', 'r+'); + + } + + function testInvalidArg() { + + $this->assertEquals( + 1, + $this->cli->main(['vobject', '--hi']) + ); + rewind($this->cli->stderr); + $this->assertTrue(strlen(stream_get_contents($this->cli->stderr)) > 100); + + } + + function testQuiet() { + + $this->assertEquals( + 1, + $this->cli->main(['vobject', '-q']) + ); + $this->assertTrue($this->cli->quiet); + + rewind($this->cli->stderr); + $this->assertEquals(0, strlen(stream_get_contents($this->cli->stderr))); + + } + + function testHelp() { + + $this->assertEquals( + 0, + $this->cli->main(['vobject', '-h']) + ); + rewind($this->cli->stderr); + $this->assertTrue(strlen(stream_get_contents($this->cli->stderr)) > 100); + + } + + function testFormat() { + + $this->assertEquals( + 1, + $this->cli->main(['vobject', '--format=jcard']) + ); + + rewind($this->cli->stderr); + $this->assertTrue(strlen(stream_get_contents($this->cli->stderr)) > 100); + + $this->assertEquals('jcard', $this->cli->format); + + } + + function testFormatInvalid() { + + $this->assertEquals( + 1, + $this->cli->main(['vobject', '--format=foo']) + ); + + rewind($this->cli->stderr); + $this->assertTrue(strlen(stream_get_contents($this->cli->stderr)) > 100); + + $this->assertNull($this->cli->format); + + } + + function testInputFormatInvalid() { + + $this->assertEquals( + 1, + $this->cli->main(['vobject', '--inputformat=foo']) + ); + + rewind($this->cli->stderr); + $this->assertTrue(strlen(stream_get_contents($this->cli->stderr)) > 100); + + $this->assertNull($this->cli->format); + + } + + + function testNoInputFile() { + + $this->assertEquals( + 1, + $this->cli->main(['vobject', 'color']) + ); + + rewind($this->cli->stderr); + $this->assertTrue(strlen(stream_get_contents($this->cli->stderr)) > 100); + + } + + function testTooManyArgs() { + + $this->assertEquals( + 1, + $this->cli->main(['vobject', 'color', 'a', 'b', 'c']) + ); + + } + + function testUnknownCommand() { + + $this->assertEquals( + 1, + $this->cli->main(['vobject', 'foo', '-']) + ); + + } + + function testConvertJson() { + + $inputStream = fopen('php://memory', 'r+'); + + fwrite($inputStream, <<<ICS +BEGIN:VCARD +VERSION:3.0 +FN:Cowboy Henk +END:VCARD +ICS + ); + rewind($inputStream); + $this->cli->stdin = $inputStream; + + $this->assertEquals( + 0, + $this->cli->main(['vobject', 'convert', '--format=json', '-']) + ); + + rewind($this->cli->stdout); + $version = Version::VERSION; + $this->assertEquals( + '["vcard",[["version",{},"text","4.0"],["prodid",{},"text","-\/\/Sabre\/\/Sabre VObject ' . $version . '\/\/EN"],["fn",{},"text","Cowboy Henk"]]]', + stream_get_contents($this->cli->stdout) + ); + + } + + function testConvertJCardPretty() { + + if (version_compare(PHP_VERSION, '5.4.0') < 0) { + $this->markTestSkipped('This test required PHP 5.4.0'); + } + + $inputStream = fopen('php://memory', 'r+'); + + fwrite($inputStream, <<<ICS +BEGIN:VCARD +VERSION:3.0 +FN:Cowboy Henk +END:VCARD +ICS + ); + rewind($inputStream); + $this->cli->stdin = $inputStream; + + $this->assertEquals( + 0, + $this->cli->main(['vobject', 'convert', '--format=jcard', '--pretty', '-']) + ); + + rewind($this->cli->stdout); + + // PHP 5.5.12 changed the output + + $expected = <<<JCARD +[ + "vcard", + [ + [ + "versi +JCARD; + + $this->assertStringStartsWith( + $expected, + stream_get_contents($this->cli->stdout) + ); + + } + + function testConvertJCalFail() { + + $inputStream = fopen('php://memory', 'r+'); + + fwrite($inputStream, <<<ICS +BEGIN:VCARD +VERSION:3.0 +FN:Cowboy Henk +END:VCARD +ICS + ); + rewind($inputStream); + $this->cli->stdin = $inputStream; + + $this->assertEquals( + 2, + $this->cli->main(['vobject', 'convert', '--format=jcal', '--inputformat=mimedir', '-']) + ); + + } + + function testConvertMimeDir() { + + $inputStream = fopen('php://memory', 'r+'); + + fwrite($inputStream, <<<JCARD +[ + "vcard", + [ + [ + "version", + { + + }, + "text", + "4.0" + ], + [ + "prodid", + { + + }, + "text", + "-\/\/Sabre\/\/Sabre VObject 3.1.0\/\/EN" + ], + [ + "fn", + { + + }, + "text", + "Cowboy Henk" + ] + ] +] +JCARD + ); + rewind($inputStream); + $this->cli->stdin = $inputStream; + + $this->assertEquals( + 0, + $this->cli->main(['vobject', 'convert', '--format=mimedir', '--inputformat=json', '--pretty', '-']) + ); + + rewind($this->cli->stdout); + $expected = <<<VCF +BEGIN:VCARD +VERSION:4.0 +PRODID:-//Sabre//Sabre VObject 3.1.0//EN +FN:Cowboy Henk +END:VCARD + +VCF; + + $this->assertEquals( + strtr($expected, ["\n" => "\r\n"]), + stream_get_contents($this->cli->stdout) + ); + + } + + function testConvertDefaultFormats() { + + $outputFile = SABRE_TEMPDIR . 'bar.json'; + + $this->assertEquals( + 2, + $this->cli->main(['vobject', 'convert', 'foo.json', $outputFile]) + ); + + $this->assertEquals('json', $this->cli->inputFormat); + $this->assertEquals('json', $this->cli->format); + + } + + function testConvertDefaultFormats2() { + + $outputFile = SABRE_TEMPDIR . 'bar.ics'; + + $this->assertEquals( + 2, + $this->cli->main(['vobject', 'convert', 'foo.ics', $outputFile]) + ); + + $this->assertEquals('mimedir', $this->cli->inputFormat); + $this->assertEquals('mimedir', $this->cli->format); + + } + + function testVCard3040() { + + $inputStream = fopen('php://memory', 'r+'); + + fwrite($inputStream, <<<VCARD +BEGIN:VCARD +VERSION:3.0 +PRODID:-//Sabre//Sabre VObject 3.1.0//EN +FN:Cowboy Henk +END:VCARD + +VCARD + ); + rewind($inputStream); + $this->cli->stdin = $inputStream; + + $this->assertEquals( + 0, + $this->cli->main(['vobject', 'convert', '--format=vcard40', '--pretty', '-']) + ); + + rewind($this->cli->stdout); + + $version = Version::VERSION; + $expected = <<<VCF +BEGIN:VCARD +VERSION:4.0 +PRODID:-//Sabre//Sabre VObject $version//EN +FN:Cowboy Henk +END:VCARD + +VCF; + + $this->assertEquals( + strtr($expected, ["\n" => "\r\n"]), + stream_get_contents($this->cli->stdout) + ); + + } + + function testVCard4030() { + + $inputStream = fopen('php://memory', 'r+'); + + fwrite($inputStream, <<<VCARD +BEGIN:VCARD +VERSION:4.0 +PRODID:-//Sabre//Sabre VObject 3.1.0//EN +FN:Cowboy Henk +END:VCARD + +VCARD + ); + rewind($inputStream); + $this->cli->stdin = $inputStream; + + $this->assertEquals( + 0, + $this->cli->main(['vobject', 'convert', '--format=vcard30', '--pretty', '-']) + ); + + $version = Version::VERSION; + + rewind($this->cli->stdout); + $expected = <<<VCF +BEGIN:VCARD +VERSION:3.0 +PRODID:-//Sabre//Sabre VObject $version//EN +FN:Cowboy Henk +END:VCARD + +VCF; + + $this->assertEquals( + strtr($expected, ["\n" => "\r\n"]), + stream_get_contents($this->cli->stdout) + ); + + } + + function testVCard4021() { + + $inputStream = fopen('php://memory', 'r+'); + + fwrite($inputStream, <<<VCARD +BEGIN:VCARD +VERSION:4.0 +PRODID:-//Sabre//Sabre VObject 3.1.0//EN +FN:Cowboy Henk +END:VCARD + +VCARD + ); + rewind($inputStream); + $this->cli->stdin = $inputStream; + + $this->assertEquals( + 2, + $this->cli->main(['vobject', 'convert', '--format=vcard21', '--pretty', '-']) + ); + + } + + function testValidate() { + + $inputStream = fopen('php://memory', 'r+'); + + fwrite($inputStream, <<<VCARD +BEGIN:VCARD +VERSION:4.0 +PRODID:-//Sabre//Sabre VObject 3.1.0//EN +UID:foo +FN:Cowboy Henk +END:VCARD + +VCARD + ); + rewind($inputStream); + $this->cli->stdin = $inputStream; + $result = $this->cli->main(['vobject', 'validate', '-']); + + $this->assertEquals( + 0, + $result + ); + + } + + function testValidateFail() { + + $inputStream = fopen('php://memory', 'r+'); + + fwrite($inputStream, <<<VCARD +BEGIN:VCALENDAR +VERSION:2.0 +END:VCARD + +VCARD + ); + rewind($inputStream); + $this->cli->stdin = $inputStream; + // vCard 2.0 is not supported yet, so this returns a failure. + $this->assertEquals( + 2, + $this->cli->main(['vobject', 'validate', '-']) + ); + + } + + function testValidateFail2() { + + $inputStream = fopen('php://memory', 'r+'); + + fwrite($inputStream, <<<VCARD +BEGIN:VCALENDAR +VERSION:5.0 +END:VCALENDAR + +VCARD + ); + rewind($inputStream); + $this->cli->stdin = $inputStream; + + $this->assertEquals( + 2, + $this->cli->main(['vobject', 'validate', '-']) + ); + + } + + function testRepair() { + + $inputStream = fopen('php://memory', 'r+'); + + fwrite($inputStream, <<<VCARD +BEGIN:VCARD +VERSION:5.0 +END:VCARD + +VCARD + ); + rewind($inputStream); + $this->cli->stdin = $inputStream; + + $this->assertEquals( + 2, + $this->cli->main(['vobject', 'repair', '-']) + ); + + rewind($this->cli->stdout); + $this->assertRegExp("/^BEGIN:VCARD\r\nVERSION:2.1\r\nUID:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\r\nEND:VCARD\r\n$/", stream_get_contents($this->cli->stdout)); + } + + function testRepairNothing() { + + $inputStream = fopen('php://memory', 'r+'); + + fwrite($inputStream, <<<VCARD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject 3.1.0//EN +BEGIN:VEVENT +UID:foo +DTSTAMP:20140122T233226Z +DTSTART:20140101T120000Z +END:VEVENT +END:VCALENDAR + +VCARD + ); + rewind($inputStream); + $this->cli->stdin = $inputStream; + + $result = $this->cli->main(['vobject', 'repair', '-']); + + rewind($this->cli->stderr); + $error = stream_get_contents($this->cli->stderr); + + $this->assertEquals( + 0, + $result, + "This should have been error free. stderr output:\n" . $error + ); + + } + + /** + * Note: this is a very shallow test, doesn't dig into the actual output, + * but just makes sure there's no errors thrown. + * + * The colorizer is not a critical component, it's mostly a debugging tool. + */ + function testColorCalendar() { + + $inputStream = fopen('php://memory', 'r+'); + + $version = Version::VERSION; + + /** + * This object is not valid, but it's designed to hit every part of the + * colorizer source. + */ + fwrite($inputStream, <<<VCARD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject {$version}//EN +BEGIN:VTIMEZONE +END:VTIMEZONE +BEGIN:VEVENT +ATTENDEE;RSVP=TRUE:mailto:foo@example.org +REQUEST-STATUS:5;foo +ATTACH:blabla +END:VEVENT +END:VCALENDAR + +VCARD + ); + rewind($inputStream); + $this->cli->stdin = $inputStream; + + $result = $this->cli->main(['vobject', 'color', '-']); + + rewind($this->cli->stderr); + $error = stream_get_contents($this->cli->stderr); + + $this->assertEquals( + 0, + $result, + "This should have been error free. stderr output:\n" . $error + ); + + } + + /** + * Note: this is a very shallow test, doesn't dig into the actual output, + * but just makes sure there's no errors thrown. + * + * The colorizer is not a critical component, it's mostly a debugging tool. + */ + function testColorVCard() { + + $inputStream = fopen('php://memory', 'r+'); + + $version = Version::VERSION; + + /** + * This object is not valid, but it's designed to hit every part of the + * colorizer source. + */ + fwrite($inputStream, <<<VCARD +BEGIN:VCARD +VERSION:4.0 +PRODID:-//Sabre//Sabre VObject {$version}//EN +ADR:1;2;3;4a,4b;5;6 +group.TEL:123454768 +END:VCARD + +VCARD + ); + rewind($inputStream); + $this->cli->stdin = $inputStream; + + $result = $this->cli->main(['vobject', 'color', '-']); + + rewind($this->cli->stderr); + $error = stream_get_contents($this->cli->stderr); + + $this->assertEquals( + 0, + $result, + "This should have been error free. stderr output:\n" . $error + ); + + } +} + +class CliMock extends Cli { + + public $quiet = false; + + public $format; + + public $pretty; + + public $stdin; + + public $stdout; + + public $stderr; + + public $inputFormat; + + public $outputFormat; + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/AvailableTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/AvailableTest.php new file mode 100644 index 00000000000..a13f67ac23b --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/AvailableTest.php @@ -0,0 +1,73 @@ +<?php + +namespace Sabre\VObject\Component; + +use DateTimeImmutable; +use DateTimeZone; +use Sabre\VObject\Reader; + +/** + * We use `RFCxxx` has a placeholder for the + * https://tools.ietf.org/html/draft-daboo-calendar-availability-05 name. + */ +class AvailableTest extends \PHPUnit_Framework_TestCase { + + function testAvailableComponent() { + + $vcal = <<<VCAL +BEGIN:VCALENDAR +BEGIN:AVAILABLE +END:AVAILABLE +END:VCALENDAR +VCAL; + $document = Reader::read($vcal); + $this->assertInstanceOf(__NAMESPACE__ . '\Available', $document->AVAILABLE); + + } + + function testGetEffectiveStartEnd() { + + $vcal = <<<VCAL +BEGIN:VCALENDAR +BEGIN:AVAILABLE +DTSTART:20150717T162200Z +DTEND:20150717T172200Z +END:AVAILABLE +END:VCALENDAR +VCAL; + + $document = Reader::read($vcal); + $tz = new DateTimeZone('UTC'); + $this->assertEquals( + [ + new DateTimeImmutable('2015-07-17 16:22:00', $tz), + new DateTimeImmutable('2015-07-17 17:22:00', $tz), + ], + $document->AVAILABLE->getEffectiveStartEnd() + ); + + } + + function testGetEffectiveStartEndDuration() { + + $vcal = <<<VCAL +BEGIN:VCALENDAR +BEGIN:AVAILABLE +DTSTART:20150717T162200Z +DURATION:PT1H +END:AVAILABLE +END:VCALENDAR +VCAL; + + $document = Reader::read($vcal); + $tz = new DateTimeZone('UTC'); + $this->assertEquals( + [ + new DateTimeImmutable('2015-07-17 16:22:00', $tz), + new DateTimeImmutable('2015-07-17 17:22:00', $tz), + ], + $document->AVAILABLE->getEffectiveStartEnd() + ); + + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VAlarmTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VAlarmTest.php new file mode 100644 index 00000000000..b398e71e824 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VAlarmTest.php @@ -0,0 +1,177 @@ +<?php + +namespace Sabre\VObject\Component; + +use DateTime; +use Sabre\VObject\Reader; + +class VAlarmTest extends \PHPUnit_Framework_TestCase { + + /** + * @dataProvider timeRangeTestData + */ + function testInTimeRange(VAlarm $valarm, $start, $end, $outcome) { + + $this->assertEquals($outcome, $valarm->isInTimeRange($start, $end)); + + } + + function timeRangeTestData() { + + $tests = []; + + $calendar = new VCalendar(); + + // Hard date and time + $valarm1 = $calendar->createComponent('VALARM'); + $valarm1->add( + $calendar->createProperty('TRIGGER', '20120312T130000Z', ['VALUE' => 'DATE-TIME']) + ); + + $tests[] = [$valarm1, new DateTime('2012-03-01 01:00:00'), new DateTime('2012-04-01 01:00:00'), true]; + $tests[] = [$valarm1, new DateTime('2012-03-01 01:00:00'), new DateTime('2012-03-10 01:00:00'), false]; + + // Relation to start time of event + $valarm2 = $calendar->createComponent('VALARM'); + $valarm2->add( + $calendar->createProperty('TRIGGER', '-P1D', ['VALUE' => 'DURATION']) + ); + + $vevent2 = $calendar->createComponent('VEVENT'); + $vevent2->DTSTART = '20120313T130000Z'; + $vevent2->add($valarm2); + + $tests[] = [$valarm2, new DateTime('2012-03-01 01:00:00'), new DateTime('2012-04-01 01:00:00'), true]; + $tests[] = [$valarm2, new DateTime('2012-03-01 01:00:00'), new DateTime('2012-03-10 01:00:00'), false]; + + // Relation to end time of event + $valarm3 = $calendar->createComponent('VALARM'); + $valarm3->add($calendar->createProperty('TRIGGER', '-P1D', ['VALUE' => 'DURATION', 'RELATED' => 'END'])); + + $vevent3 = $calendar->createComponent('VEVENT'); + $vevent3->DTSTART = '20120301T130000Z'; + $vevent3->DTEND = '20120401T130000Z'; + $vevent3->add($valarm3); + + $tests[] = [$valarm3, new DateTime('2012-02-25 01:00:00'), new DateTime('2012-03-05 01:00:00'), false]; + $tests[] = [$valarm3, new DateTime('2012-03-25 01:00:00'), new DateTime('2012-04-05 01:00:00'), true]; + + // Relation to end time of todo + $valarm4 = $calendar->createComponent('VALARM'); + $valarm4->TRIGGER = '-P1D'; + $valarm4->TRIGGER['VALUE'] = 'DURATION'; + $valarm4->TRIGGER['RELATED'] = 'END'; + + $vtodo4 = $calendar->createComponent('VTODO'); + $vtodo4->DTSTART = '20120301T130000Z'; + $vtodo4->DUE = '20120401T130000Z'; + $vtodo4->add($valarm4); + + $tests[] = [$valarm4, new DateTime('2012-02-25 01:00:00'), new DateTime('2012-03-05 01:00:00'), false]; + $tests[] = [$valarm4, new DateTime('2012-03-25 01:00:00'), new DateTime('2012-04-05 01:00:00'), true]; + + // Relation to start time of event + repeat + $valarm5 = $calendar->createComponent('VALARM'); + $valarm5->TRIGGER = '-P1D'; + $valarm5->TRIGGER['VALUE'] = 'DURATION'; + $valarm5->REPEAT = 10; + $valarm5->DURATION = 'P1D'; + + $vevent5 = $calendar->createComponent('VEVENT'); + $vevent5->DTSTART = '20120301T130000Z'; + $vevent5->add($valarm5); + + $tests[] = [$valarm5, new DateTime('2012-03-09 01:00:00'), new DateTime('2012-03-10 01:00:00'), true]; + + // Relation to start time of event + duration, but no repeat + $valarm6 = $calendar->createComponent('VALARM'); + $valarm6->TRIGGER = '-P1D'; + $valarm6->TRIGGER['VALUE'] = 'DURATION'; + $valarm6->DURATION = 'P1D'; + + $vevent6 = $calendar->createComponent('VEVENT'); + $vevent6->DTSTART = '20120313T130000Z'; + $vevent6->add($valarm6); + + $tests[] = [$valarm6, new DateTime('2012-03-01 01:00:00'), new DateTime('2012-04-01 01:00:00'), true]; + $tests[] = [$valarm6, new DateTime('2012-03-01 01:00:00'), new DateTime('2012-03-10 01:00:00'), false]; + + + // Relation to end time of event (DURATION instead of DTEND) + $valarm7 = $calendar->createComponent('VALARM'); + $valarm7->TRIGGER = '-P1D'; + $valarm7->TRIGGER['VALUE'] = 'DURATION'; + $valarm7->TRIGGER['RELATED'] = 'END'; + + $vevent7 = $calendar->createComponent('VEVENT'); + $vevent7->DTSTART = '20120301T130000Z'; + $vevent7->DURATION = 'P30D'; + $vevent7->add($valarm7); + + $tests[] = [$valarm7, new DateTime('2012-02-25 01:00:00'), new DateTime('2012-03-05 01:00:00'), false]; + $tests[] = [$valarm7, new DateTime('2012-03-25 01:00:00'), new DateTime('2012-04-05 01:00:00'), true]; + + // Relation to end time of event (No DTEND or DURATION) + $valarm7 = $calendar->createComponent('VALARM'); + $valarm7->TRIGGER = '-P1D'; + $valarm7->TRIGGER['VALUE'] = 'DURATION'; + $valarm7->TRIGGER['RELATED'] = 'END'; + + $vevent7 = $calendar->createComponent('VEVENT'); + $vevent7->DTSTART = '20120301T130000Z'; + $vevent7->add($valarm7); + + $tests[] = [$valarm7, new DateTime('2012-02-25 01:00:00'), new DateTime('2012-03-05 01:00:00'), true]; + $tests[] = [$valarm7, new DateTime('2012-03-25 01:00:00'), new DateTime('2012-04-05 01:00:00'), false]; + + + return $tests; + } + + /** + * @expectedException \Sabre\VObject\InvalidDataException + */ + function testInTimeRangeInvalidComponent() { + + $calendar = new VCalendar(); + $valarm = $calendar->createComponent('VALARM'); + $valarm->TRIGGER = '-P1D'; + $valarm->TRIGGER['RELATED'] = 'END'; + + $vjournal = $calendar->createComponent('VJOURNAL'); + $vjournal->add($valarm); + + $valarm->isInTimeRange(new DateTime('2012-02-25 01:00:00'), new DateTime('2012-03-05 01:00:00')); + + } + + /** + * This bug was found and reported on the mailing list. + */ + function testInTimeRangeBuggy() { + +$input = <<<BLA +BEGIN:VCALENDAR +BEGIN:VTODO +DTSTAMP:20121003T064931Z +UID:b848cb9a7bb16e464a06c222ca1f8102@examle.com +STATUS:NEEDS-ACTION +DUE:20121005T000000Z +SUMMARY:Task 1 +CATEGORIES:AlarmCategory +BEGIN:VALARM +TRIGGER:-PT10M +ACTION:DISPLAY +DESCRIPTION:Task 1 +END:VALARM +END:VTODO +END:VCALENDAR +BLA; + + $vobj = Reader::read($input); + + $this->assertTrue($vobj->VTODO->VALARM->isInTimeRange(new \DateTime('2012-10-01 00:00:00'), new \DateTime('2012-11-01 00:00:00'))); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VAvailabilityTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VAvailabilityTest.php new file mode 100644 index 00000000000..021100ecb88 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VAvailabilityTest.php @@ -0,0 +1,490 @@ +<?php + +namespace Sabre\VObject\Component; + +use DateTimeImmutable; +use DateTimeZone; +use Sabre\VObject; +use Sabre\VObject\Reader; + +/** + * We use `RFCxxx` has a placeholder for the + * https://tools.ietf.org/html/draft-daboo-calendar-availability-05 name. + */ +class VAvailabilityTest extends \PHPUnit_Framework_TestCase { + + function testVAvailabilityComponent() { + + $vcal = <<<VCAL +BEGIN:VCALENDAR +BEGIN:VAVAILABILITY +END:VAVAILABILITY +END:VCALENDAR +VCAL; + $document = Reader::read($vcal); + + $this->assertInstanceOf(__NAMESPACE__ . '\VAvailability', $document->VAVAILABILITY); + + } + + function testGetEffectiveStartEnd() { + + $vcal = <<<VCAL +BEGIN:VCALENDAR +BEGIN:VAVAILABILITY +DTSTART:20150717T162200Z +DTEND:20150717T172200Z +END:VAVAILABILITY +END:VCALENDAR +VCAL; + + $document = Reader::read($vcal); + $tz = new DateTimeZone('UTC'); + $this->assertEquals( + [ + new DateTimeImmutable('2015-07-17 16:22:00', $tz), + new DateTimeImmutable('2015-07-17 17:22:00', $tz), + ], + $document->VAVAILABILITY->getEffectiveStartEnd() + ); + + } + + function testGetEffectiveStartDuration() { + + $vcal = <<<VCAL +BEGIN:VCALENDAR +BEGIN:VAVAILABILITY +DTSTART:20150717T162200Z +DURATION:PT1H +END:VAVAILABILITY +END:VCALENDAR +VCAL; + + $document = Reader::read($vcal); + $tz = new DateTimeZone('UTC'); + $this->assertEquals( + [ + new DateTimeImmutable('2015-07-17 16:22:00', $tz), + new DateTimeImmutable('2015-07-17 17:22:00', $tz), + ], + $document->VAVAILABILITY->getEffectiveStartEnd() + ); + + } + + function testGetEffectiveStartEndUnbound() { + + $vcal = <<<VCAL +BEGIN:VCALENDAR +BEGIN:VAVAILABILITY +END:VAVAILABILITY +END:VCALENDAR +VCAL; + + $document = Reader::read($vcal); + $this->assertEquals( + [ + null, + null, + ], + $document->VAVAILABILITY->getEffectiveStartEnd() + ); + + } + + function testIsInTimeRangeUnbound() { + + $vcal = <<<VCAL +BEGIN:VCALENDAR +BEGIN:VAVAILABILITY +END:VAVAILABILITY +END:VCALENDAR +VCAL; + + $document = Reader::read($vcal); + $this->assertTrue( + $document->VAVAILABILITY->isInTimeRange(new DateTimeImmutable('2015-07-17'), new DateTimeImmutable('2015-07-18')) + ); + + } + + function testIsInTimeRangeOutside() { + + $vcal = <<<VCAL +BEGIN:VCALENDAR +BEGIN:VAVAILABILITY +DTSTART:20140101T000000Z +DTEND:20140102T000000Z +END:VAVAILABILITY +END:VCALENDAR +VCAL; + + $document = Reader::read($vcal); + $this->assertFalse( + $document->VAVAILABILITY->isInTimeRange(new DateTimeImmutable('2015-07-17'), new DateTimeImmutable('2015-07-18')) + ); + + } + + function testRFCxxxSection3_1_availabilityprop_required() { + + // UID and DTSTAMP are present. + $this->assertIsValid(Reader::read( +<<<VCAL +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//id +BEGIN:VAVAILABILITY +UID:foo@test +DTSTAMP:20111005T133225Z +END:VAVAILABILITY +END:VCALENDAR +VCAL + )); + + // UID and DTSTAMP are missing. + $this->assertIsNotValid(Reader::read( +<<<VCAL +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//id +BEGIN:VAVAILABILITY +END:VAVAILABILITY +END:VCALENDAR +VCAL + )); + + // DTSTAMP is missing. + $this->assertIsNotValid(Reader::read( +<<<VCAL +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//id +BEGIN:VAVAILABILITY +UID:foo@test +END:VAVAILABILITY +END:VCALENDAR +VCAL + )); + + // UID is missing. + $this->assertIsNotValid(Reader::read( +<<<VCAL +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//id +BEGIN:VAVAILABILITY +DTSTAMP:20111005T133225Z +END:VAVAILABILITY +END:VCALENDAR +VCAL + )); + + } + + function testRFCxxxSection3_1_availabilityprop_optional_once() { + + $properties = [ + 'BUSYTYPE:BUSY', + 'CLASS:PUBLIC', + 'CREATED:20111005T135125Z', + 'DESCRIPTION:Long bla bla', + 'DTSTART:20111005T020000', + 'LAST-MODIFIED:20111005T135325Z', + 'ORGANIZER:mailto:foo@example.com', + 'PRIORITY:1', + 'SEQUENCE:0', + 'SUMMARY:Bla bla', + 'URL:http://example.org/' + ]; + + // They are all present, only once. + $this->assertIsValid(Reader::read($this->template($properties))); + + // We duplicate each one to see if it fails. + foreach ($properties as $property) { + $this->assertIsNotValid(Reader::read($this->template([ + $property, + $property + ]))); + } + + } + + function testRFCxxxSection3_1_availabilityprop_dtend_duration() { + + // Only DTEND. + $this->assertIsValid(Reader::read($this->template([ + 'DTEND:21111005T133225Z' + ]))); + + // Only DURATION. + $this->assertIsValid(Reader::read($this->template([ + 'DURATION:PT1H' + ]))); + + // Both (not allowed). + $this->assertIsNotValid(Reader::read($this->template([ + 'DTEND:21111005T133225Z', + 'DURATION:PT1H' + ]))); + } + + function testAvailableSubComponent() { + + $vcal = <<<VCAL +BEGIN:VCALENDAR +BEGIN:VAVAILABILITY +BEGIN:AVAILABLE +END:AVAILABLE +END:VAVAILABILITY +END:VCALENDAR +VCAL; + $document = Reader::read($vcal); + + $this->assertInstanceOf(__NAMESPACE__, $document->VAVAILABILITY->AVAILABLE); + + } + + function testRFCxxxSection3_1_availableprop_required() { + + // UID, DTSTAMP and DTSTART are present. + $this->assertIsValid(Reader::read( +<<<VCAL +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//id +BEGIN:VAVAILABILITY +UID:foo@test +DTSTAMP:20111005T133225Z +BEGIN:AVAILABLE +UID:foo@test +DTSTAMP:20111005T133225Z +DTSTART:20111005T133225Z +END:AVAILABLE +END:VAVAILABILITY +END:VCALENDAR +VCAL + )); + + // UID, DTSTAMP and DTSTART are missing. + $this->assertIsNotValid(Reader::read( +<<<VCAL +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//id +BEGIN:VAVAILABILITY +UID:foo@test +DTSTAMP:20111005T133225Z +BEGIN:AVAILABLE +END:AVAILABLE +END:VAVAILABILITY +END:VCALENDAR +VCAL + )); + + // UID is missing. + $this->assertIsNotValid(Reader::read( +<<<VCAL +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//id +BEGIN:VAVAILABILITY +UID:foo@test +DTSTAMP:20111005T133225Z +BEGIN:AVAILABLE +DTSTAMP:20111005T133225Z +DTSTART:20111005T133225Z +END:AVAILABLE +END:VAVAILABILITY +END:VCALENDAR +VCAL + )); + + // DTSTAMP is missing. + $this->assertIsNotValid(Reader::read( +<<<VCAL +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//id +BEGIN:VAVAILABILITY +UID:foo@test +DTSTAMP:20111005T133225Z +BEGIN:AVAILABLE +UID:foo@test +DTSTART:20111005T133225Z +END:AVAILABLE +END:VAVAILABILITY +END:VCALENDAR +VCAL + )); + + // DTSTART is missing. + $this->assertIsNotValid(Reader::read( +<<<VCAL +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//id +BEGIN:VAVAILABILITY +UID:foo@test +DTSTAMP:20111005T133225Z +BEGIN:AVAILABLE +UID:foo@test +DTSTAMP:20111005T133225Z +END:AVAILABLE +END:VAVAILABILITY +END:VCALENDAR +VCAL + )); + + } + + function testRFCxxxSection3_1_available_dtend_duration() { + + // Only DTEND. + $this->assertIsValid(Reader::read($this->templateAvailable([ + 'DTEND:21111005T133225Z' + ]))); + + // Only DURATION. + $this->assertIsValid(Reader::read($this->templateAvailable([ + 'DURATION:PT1H' + ]))); + + // Both (not allowed). + $this->assertIsNotValid(Reader::read($this->templateAvailable([ + 'DTEND:21111005T133225Z', + 'DURATION:PT1H' + ]))); + } + + function testRFCxxxSection3_1_available_optional_once() { + + $properties = [ + 'CREATED:20111005T135125Z', + 'DESCRIPTION:Long bla bla', + 'LAST-MODIFIED:20111005T135325Z', + 'RECURRENCE-ID;RANGE=THISANDFUTURE:19980401T133000Z', + 'RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', + 'SUMMARY:Bla bla' + ]; + + // They are all present, only once. + $this->assertIsValid(Reader::read($this->templateAvailable($properties))); + + // We duplicate each one to see if it fails. + foreach ($properties as $property) { + $this->assertIsNotValid(Reader::read($this->templateAvailable([ + $property, + $property + ]))); + } + + } + function testRFCxxxSection3_2() { + + $this->assertEquals( + 'BUSY', + Reader::read($this->templateAvailable([ + 'BUSYTYPE:BUSY' + ])) + ->VAVAILABILITY + ->AVAILABLE + ->BUSYTYPE + ->getValue() + ); + + $this->assertEquals( + 'BUSY-UNAVAILABLE', + Reader::read($this->templateAvailable([ + 'BUSYTYPE:BUSY-UNAVAILABLE' + ])) + ->VAVAILABILITY + ->AVAILABLE + ->BUSYTYPE + ->getValue() + ); + + $this->assertEquals( + 'BUSY-TENTATIVE', + Reader::read($this->templateAvailable([ + 'BUSYTYPE:BUSY-TENTATIVE' + ])) + ->VAVAILABILITY + ->AVAILABLE + ->BUSYTYPE + ->getValue() + ); + + } + + protected function assertIsValid(VObject\Document $document) { + + $validationResult = $document->validate(); + if ($validationResult) { + $messages = array_map(function($item) { return $item['message']; }, $validationResult); + $this->fail('Failed to assert that the supplied document is a valid document. Validation messages: ' . implode(', ', $messages)); + } + $this->assertEmpty($document->validate()); + + } + + protected function assertIsNotValid(VObject\Document $document) { + + $this->assertNotEmpty($document->validate()); + + } + + protected function template(array $properties) { + + return $this->_template( + <<<VCAL +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//id +BEGIN:VAVAILABILITY +UID:foo@test +DTSTAMP:20111005T133225Z +… +END:VAVAILABILITY +END:VCALENDAR +VCAL +, + $properties + ); + + } + + protected function templateAvailable(array $properties) { + + return $this->_template( + <<<VCAL +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//id +BEGIN:VAVAILABILITY +UID:foo@test +DTSTAMP:20111005T133225Z +BEGIN:AVAILABLE +UID:foo@test +DTSTAMP:20111005T133225Z +DTSTART:20111005T133225Z +… +END:AVAILABLE +END:VAVAILABILITY +END:VCALENDAR +VCAL +, + $properties + ); + + } + + protected function _template($template, array $properties) { + + return str_replace('…', implode("\r\n", $properties), $template); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VCalendarTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VCalendarTest.php new file mode 100644 index 00000000000..abb387d9572 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VCalendarTest.php @@ -0,0 +1,782 @@ +<?php + +namespace Sabre\VObject\Component; + +use DateTimeZone; +use Sabre\VObject; + +class VCalendarTest extends \PHPUnit_Framework_TestCase { + + use VObject\PHPUnitAssertions; + + /** + * @dataProvider expandData + */ + function testExpand($input, $output, $timeZone = 'UTC', $start = '2011-12-01', $end = '2011-12-31') { + + $vcal = VObject\Reader::read($input); + + $timeZone = new DateTimeZone($timeZone); + + $vcal = $vcal->expand( + new \DateTime($start), + new \DateTime($end), + $timeZone + ); + + // This will normalize the output + $output = VObject\Reader::read($output)->serialize(); + + $this->assertVObjectEqualsVObject($output, $vcal->serialize()); + + } + + function expandData() { + + $tests = []; + + // No data + $input = 'BEGIN:VCALENDAR +CALSCALE:GREGORIAN +VERSION:2.0 +END:VCALENDAR +'; + + $output = $input; + $tests[] = [$input,$output]; + + + // Simple events + $input = 'BEGIN:VCALENDAR +CALSCALE:GREGORIAN +VERSION:2.0 +BEGIN:VEVENT +UID:bla +SUMMARY:InExpand +DTSTART;VALUE=DATE:20111202 +END:VEVENT +BEGIN:VEVENT +UID:bla2 +SUMMARY:NotInExpand +DTSTART;VALUE=DATE:20120101 +END:VEVENT +END:VCALENDAR +'; + + $output = 'BEGIN:VCALENDAR +CALSCALE:GREGORIAN +VERSION:2.0 +BEGIN:VEVENT +UID:bla +SUMMARY:InExpand +DTSTART;VALUE=DATE:20111202 +END:VEVENT +END:VCALENDAR +'; + + $tests[] = [$input, $output]; + + // Removing timezone info + $input = 'BEGIN:VCALENDAR +CALSCALE:GREGORIAN +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:Europe/Paris +END:VTIMEZONE +BEGIN:VEVENT +UID:bla4 +SUMMARY:RemoveTZ info +DTSTART;TZID=Europe/Paris:20111203T130102 +END:VEVENT +END:VCALENDAR +'; + + $output = 'BEGIN:VCALENDAR +CALSCALE:GREGORIAN +VERSION:2.0 +BEGIN:VEVENT +UID:bla4 +SUMMARY:RemoveTZ info +DTSTART:20111203T120102Z +END:VEVENT +END:VCALENDAR +'; + + $tests[] = [$input, $output]; + + // Removing timezone info from sub-components. See Issue #278 + $input = 'BEGIN:VCALENDAR +CALSCALE:GREGORIAN +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:Europe/Paris +END:VTIMEZONE +BEGIN:VEVENT +UID:bla4 +SUMMARY:RemoveTZ info +DTSTART;TZID=Europe/Paris:20111203T130102 +BEGIN:VALARM +TRIGGER;VALUE=DATE-TIME;TZID=America/New_York:20151209T133200 +END:VALARM +END:VEVENT +END:VCALENDAR +'; + + $output = 'BEGIN:VCALENDAR +CALSCALE:GREGORIAN +VERSION:2.0 +BEGIN:VEVENT +UID:bla4 +SUMMARY:RemoveTZ info +DTSTART:20111203T120102Z +BEGIN:VALARM +TRIGGER;VALUE=DATE-TIME:20151209T183200Z +END:VALARM +END:VEVENT +END:VCALENDAR +'; + + $tests[] = [$input, $output]; + + // Recurrence rule + $input = 'BEGIN:VCALENDAR +CALSCALE:GREGORIAN +VERSION:2.0 +BEGIN:VEVENT +UID:bla6 +SUMMARY:Testing RRule +DTSTART:20111125T120000Z +DTEND:20111125T130000Z +RRULE:FREQ=WEEKLY +END:VEVENT +END:VCALENDAR +'; + + $output = 'BEGIN:VCALENDAR +CALSCALE:GREGORIAN +VERSION:2.0 +BEGIN:VEVENT +UID:bla6 +SUMMARY:Testing RRule +DTSTART:20111202T120000Z +DTEND:20111202T130000Z +RECURRENCE-ID:20111202T120000Z +END:VEVENT +BEGIN:VEVENT +UID:bla6 +SUMMARY:Testing RRule +DTSTART:20111209T120000Z +DTEND:20111209T130000Z +RECURRENCE-ID:20111209T120000Z +END:VEVENT +BEGIN:VEVENT +UID:bla6 +SUMMARY:Testing RRule +DTSTART:20111216T120000Z +DTEND:20111216T130000Z +RECURRENCE-ID:20111216T120000Z +END:VEVENT +BEGIN:VEVENT +UID:bla6 +SUMMARY:Testing RRule +DTSTART:20111223T120000Z +DTEND:20111223T130000Z +RECURRENCE-ID:20111223T120000Z +END:VEVENT +BEGIN:VEVENT +UID:bla6 +SUMMARY:Testing RRule +DTSTART:20111230T120000Z +DTEND:20111230T130000Z +RECURRENCE-ID:20111230T120000Z +END:VEVENT +END:VCALENDAR +'; + + $tests[] = [$input, $output]; + + // Recurrence rule + override + $input = 'BEGIN:VCALENDAR +CALSCALE:GREGORIAN +VERSION:2.0 +BEGIN:VEVENT +UID:bla6 +SUMMARY:Testing RRule2 +DTSTART:20111125T120000Z +DTEND:20111125T130000Z +RRULE:FREQ=WEEKLY +END:VEVENT +BEGIN:VEVENT +UID:bla6 +RECURRENCE-ID:20111209T120000Z +DTSTART:20111209T140000Z +DTEND:20111209T150000Z +SUMMARY:Override! +END:VEVENT +END:VCALENDAR +'; + + $output = 'BEGIN:VCALENDAR +CALSCALE:GREGORIAN +VERSION:2.0 +BEGIN:VEVENT +UID:bla6 +SUMMARY:Testing RRule2 +DTSTART:20111202T120000Z +DTEND:20111202T130000Z +RECURRENCE-ID:20111202T120000Z +END:VEVENT +BEGIN:VEVENT +UID:bla6 +RECURRENCE-ID:20111209T120000Z +DTSTART:20111209T140000Z +DTEND:20111209T150000Z +SUMMARY:Override! +END:VEVENT +BEGIN:VEVENT +UID:bla6 +SUMMARY:Testing RRule2 +DTSTART:20111216T120000Z +DTEND:20111216T130000Z +RECURRENCE-ID:20111216T120000Z +END:VEVENT +BEGIN:VEVENT +UID:bla6 +SUMMARY:Testing RRule2 +DTSTART:20111223T120000Z +DTEND:20111223T130000Z +RECURRENCE-ID:20111223T120000Z +END:VEVENT +BEGIN:VEVENT +UID:bla6 +SUMMARY:Testing RRule2 +DTSTART:20111230T120000Z +DTEND:20111230T130000Z +RECURRENCE-ID:20111230T120000Z +END:VEVENT +END:VCALENDAR +'; + + $tests[] = [$input, $output]; + + // Floating dates and times. + $input = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:bla1 +DTSTART:20141112T195000 +END:VEVENT +BEGIN:VEVENT +UID:bla2 +DTSTART;VALUE=DATE:20141112 +END:VEVENT +BEGIN:VEVENT +UID:bla3 +DTSTART;VALUE=DATE:20141112 +RRULE:FREQ=DAILY;COUNT=2 +END:VEVENT +END:VCALENDAR +ICS; + + $output = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:bla1 +DTSTART:20141112T225000Z +END:VEVENT +BEGIN:VEVENT +UID:bla2 +DTSTART;VALUE=DATE:20141112 +END:VEVENT +BEGIN:VEVENT +UID:bla3 +DTSTART;VALUE=DATE:20141112 +RECURRENCE-ID;VALUE=DATE:20141112 +END:VEVENT +BEGIN:VEVENT +UID:bla3 +DTSTART;VALUE=DATE:20141113 +RECURRENCE-ID;VALUE=DATE:20141113 +END:VEVENT +END:VCALENDAR +ICS; + + $tests[] = [$input, $output, 'America/Argentina/Buenos_Aires', '2014-01-01', '2015-01-01']; + + // Recurrence rule with no valid instances + $input = 'BEGIN:VCALENDAR +CALSCALE:GREGORIAN +VERSION:2.0 +BEGIN:VEVENT +UID:bla6 +SUMMARY:Testing RRule3 +DTSTART:20111125T120000Z +DTEND:20111125T130000Z +RRULE:FREQ=WEEKLY;COUNT=1 +EXDATE:20111125T120000Z +END:VEVENT +END:VCALENDAR +'; + + $output = 'BEGIN:VCALENDAR +CALSCALE:GREGORIAN +VERSION:2.0 +END:VCALENDAR +'; + + $tests[] = [$input, $output]; + return $tests; + + } + + /** + * @expectedException \Sabre\VObject\InvalidDataException + */ + function testBrokenEventExpand() { + + $input = 'BEGIN:VCALENDAR +CALSCALE:GREGORIAN +VERSION:2.0 +BEGIN:VEVENT +RRULE:FREQ=WEEKLY +DTSTART;VALUE=DATE:20111202 +END:VEVENT +END:VCALENDAR +'; + $vcal = VObject\Reader::read($input); + $vcal->expand( + new \DateTime('2011-12-01'), + new \DateTime('2011-12-31') + ); + + } + + function testGetDocumentType() { + + $vcard = new VCalendar(); + $vcard->VERSION = '2.0'; + $this->assertEquals(VCalendar::ICALENDAR20, $vcard->getDocumentType()); + + } + + function testValidateCorrect() { + + $input = 'BEGIN:VCALENDAR +CALSCALE:GREGORIAN +VERSION:2.0 +PRODID:foo +BEGIN:VEVENT +DTSTART;VALUE=DATE:20111202 +DTSTAMP:20140122T233226Z +UID:foo +END:VEVENT +END:VCALENDAR +'; + + $vcal = VObject\Reader::read($input); + $this->assertEquals([], $vcal->validate(), 'Got an error'); + + } + + function testValidateNoVersion() { + + $input = 'BEGIN:VCALENDAR +CALSCALE:GREGORIAN +PRODID:foo +BEGIN:VEVENT +DTSTART;VALUE=DATE:20111202 +UID:foo +DTSTAMP:20140122T234434Z +END:VEVENT +END:VCALENDAR +'; + + $vcal = VObject\Reader::read($input); + $this->assertEquals(1, count($vcal->validate())); + + } + + function testValidateWrongVersion() { + + $input = 'BEGIN:VCALENDAR +CALSCALE:GREGORIAN +VERSION:3.0 +PRODID:foo +BEGIN:VEVENT +DTSTART;VALUE=DATE:20111202 +UID:foo +DTSTAMP:20140122T234434Z +END:VEVENT +END:VCALENDAR +'; + + $vcal = VObject\Reader::read($input); + $this->assertEquals(1, count($vcal->validate())); + + } + + function testValidateNoProdId() { + + $input = 'BEGIN:VCALENDAR +CALSCALE:GREGORIAN +VERSION:2.0 +BEGIN:VEVENT +DTSTART;VALUE=DATE:20111202 +UID:foo +DTSTAMP:20140122T234434Z +END:VEVENT +END:VCALENDAR +'; + + $vcal = VObject\Reader::read($input); + $this->assertEquals(1, count($vcal->validate())); + + } + + function testValidateDoubleCalScale() { + + $input = 'BEGIN:VCALENDAR +VERSION:2.0 +PRODID:foo +CALSCALE:GREGORIAN +CALSCALE:GREGORIAN +BEGIN:VEVENT +DTSTART;VALUE=DATE:20111202 +UID:foo +DTSTAMP:20140122T234434Z +END:VEVENT +END:VCALENDAR +'; + + $vcal = VObject\Reader::read($input); + $this->assertEquals(1, count($vcal->validate())); + + } + + function testValidateDoubleMethod() { + + $input = 'BEGIN:VCALENDAR +VERSION:2.0 +PRODID:foo +METHOD:REQUEST +METHOD:REQUEST +BEGIN:VEVENT +DTSTART;VALUE=DATE:20111202 +UID:foo +DTSTAMP:20140122T234434Z +END:VEVENT +END:VCALENDAR +'; + + $vcal = VObject\Reader::read($input); + $this->assertEquals(1, count($vcal->validate())); + + } + + function testValidateTwoMasterEvents() { + + $input = 'BEGIN:VCALENDAR +VERSION:2.0 +PRODID:foo +METHOD:REQUEST +BEGIN:VEVENT +DTSTART;VALUE=DATE:20111202 +UID:foo +DTSTAMP:20140122T234434Z +END:VEVENT +BEGIN:VEVENT +DTSTART;VALUE=DATE:20111202 +UID:foo +DTSTAMP:20140122T234434Z +END:VEVENT +END:VCALENDAR +'; + + $vcal = VObject\Reader::read($input); + $this->assertEquals(1, count($vcal->validate())); + + } + + function testValidateOneMasterEvent() { + + $input = 'BEGIN:VCALENDAR +VERSION:2.0 +PRODID:foo +METHOD:REQUEST +BEGIN:VEVENT +DTSTART;VALUE=DATE:20111202 +UID:foo +DTSTAMP:20140122T234434Z +END:VEVENT +BEGIN:VEVENT +DTSTART;VALUE=DATE:20111202 +UID:foo +DTSTAMP:20140122T234434Z +RECURRENCE-ID;VALUE=DATE:20111202 +END:VEVENT +END:VCALENDAR +'; + + $vcal = VObject\Reader::read($input); + $this->assertEquals(0, count($vcal->validate())); + + } + + function testGetBaseComponent() { + + $input = 'BEGIN:VCALENDAR +VERSION:2.0 +PRODID:foo +METHOD:REQUEST +BEGIN:VEVENT +SUMMARY:test +DTSTART;VALUE=DATE:20111202 +UID:foo +DTSTAMP:20140122T234434Z +END:VEVENT +BEGIN:VEVENT +DTSTART;VALUE=DATE:20111202 +UID:foo +DTSTAMP:20140122T234434Z +RECURRENCE-ID;VALUE=DATE:20111202 +END:VEVENT +END:VCALENDAR +'; + + $vcal = VObject\Reader::read($input); + + $result = $vcal->getBaseComponent(); + $this->assertEquals('test', $result->SUMMARY->getValue()); + + } + + function testGetBaseComponentNoResult() { + + $input = 'BEGIN:VCALENDAR +VERSION:2.0 +PRODID:foo +METHOD:REQUEST +BEGIN:VEVENT +SUMMARY:test +RECURRENCE-ID;VALUE=DATE:20111202 +DTSTART;VALUE=DATE:20111202 +UID:foo +DTSTAMP:20140122T234434Z +END:VEVENT +BEGIN:VEVENT +DTSTART;VALUE=DATE:20111202 +UID:foo +DTSTAMP:20140122T234434Z +RECURRENCE-ID;VALUE=DATE:20111202 +END:VEVENT +END:VCALENDAR +'; + + $vcal = VObject\Reader::read($input); + + $result = $vcal->getBaseComponent(); + $this->assertNull($result); + + } + + function testGetBaseComponentWithFilter() { + + $input = 'BEGIN:VCALENDAR +VERSION:2.0 +PRODID:foo +METHOD:REQUEST +BEGIN:VEVENT +SUMMARY:test +DTSTART;VALUE=DATE:20111202 +UID:foo +DTSTAMP:20140122T234434Z +END:VEVENT +BEGIN:VEVENT +DTSTART;VALUE=DATE:20111202 +UID:foo +DTSTAMP:20140122T234434Z +RECURRENCE-ID;VALUE=DATE:20111202 +END:VEVENT +END:VCALENDAR +'; + + $vcal = VObject\Reader::read($input); + + $result = $vcal->getBaseComponent('VEVENT'); + $this->assertEquals('test', $result->SUMMARY->getValue()); + + } + + function testGetBaseComponentWithFilterNoResult() { + + $input = 'BEGIN:VCALENDAR +VERSION:2.0 +PRODID:foo +METHOD:REQUEST +BEGIN:VTODO +SUMMARY:test +UID:foo +DTSTAMP:20140122T234434Z +END:VTODO +END:VCALENDAR +'; + + $vcal = VObject\Reader::read($input); + + $result = $vcal->getBaseComponent('VEVENT'); + $this->assertNull($result); + + } + + function testNoComponents() { + + $input = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:vobject +END:VCALENDAR +ICS; + + $this->assertValidate( + $input, + 0, + 3, + "An iCalendar object must have at least 1 component." + ); + + } + + function testCalDAVNoComponents() { + + $input = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:vobject +BEGIN:VTIMEZONE +TZID:America/Toronto +END:VTIMEZONE +END:VCALENDAR +ICS; + + $this->assertValidate( + $input, + VCalendar::PROFILE_CALDAV, + 3, + "A calendar object on a CalDAV server must have at least 1 component (VTODO, VEVENT, VJOURNAL)." + ); + + } + + function testCalDAVMultiUID() { + + $input = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:vobject +BEGIN:VEVENT +UID:foo +DTSTAMP:20150109T184500Z +DTSTART:20150109T184500Z +END:VEVENT +BEGIN:VEVENT +UID:bar +DTSTAMP:20150109T184500Z +DTSTART:20150109T184500Z +END:VEVENT +END:VCALENDAR +ICS; + + $this->assertValidate( + $input, + VCalendar::PROFILE_CALDAV, + 3, + "A calendar object on a CalDAV server may only have components with the same UID." + ); + + } + + function testCalDAVMultiComponent() { + + $input = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:vobject +BEGIN:VEVENT +UID:foo +RECURRENCE-ID:20150109T185200Z +DTSTAMP:20150109T184500Z +DTSTART:20150109T184500Z +END:VEVENT +BEGIN:VTODO +UID:foo +DTSTAMP:20150109T184500Z +DTSTART:20150109T184500Z +END:VTODO +END:VCALENDAR +ICS; + + $this->assertValidate( + $input, + VCalendar::PROFILE_CALDAV, + 3, + "A calendar object on a CalDAV server may only have 1 type of component (VEVENT, VTODO or VJOURNAL)." + ); + + } + + function testCalDAVMETHOD() { + + $input = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +METHOD:PUBLISH +PRODID:vobject +BEGIN:VEVENT +UID:foo +RECURRENCE-ID:20150109T185200Z +DTSTAMP:20150109T184500Z +DTSTART:20150109T184500Z +END:VEVENT +END:VCALENDAR +ICS; + + $this->assertValidate( + $input, + VCalendar::PROFILE_CALDAV, + 3, + "A calendar object on a CalDAV server MUST NOT have a METHOD property." + ); + + } + + function assertValidate($ics, $options, $expectedLevel, $expectedMessage = null) { + + $vcal = VObject\Reader::read($ics); + $result = $vcal->validate($options); + + $this->assertValidateResult($result, $expectedLevel, $expectedMessage); + + } + + function assertValidateResult($input, $expectedLevel, $expectedMessage = null) { + + $messages = []; + foreach ($input as $warning) { + $messages[] = $warning['message']; + } + + if ($expectedLevel === 0) { + $this->assertEquals(0, count($input), 'No validation messages were expected. We got: ' . implode(', ', $messages)); + } else { + $this->assertEquals(1, count($input), 'We expected exactly 1 validation message, We got: ' . implode(', ', $messages)); + + $this->assertEquals($expectedMessage, $input[0]['message']); + $this->assertEquals($expectedLevel, $input[0]['level']); + } + + } + + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VCardTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VCardTest.php new file mode 100644 index 00000000000..baa7f490d44 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VCardTest.php @@ -0,0 +1,304 @@ +<?php + +namespace Sabre\VObject\Component; + +use Sabre\VObject; + +class VCardTest extends \PHPUnit_Framework_TestCase { + + /** + * @dataProvider validateData + */ + function testValidate($input, $expectedWarnings, $expectedRepairedOutput) { + + $vcard = VObject\Reader::read($input); + + $warnings = $vcard->validate(); + + $warnMsg = []; + foreach ($warnings as $warning) { + $warnMsg[] = $warning['message']; + } + + $this->assertEquals($expectedWarnings, $warnMsg); + + $vcard->validate(VObject\Component::REPAIR); + + $this->assertEquals( + $expectedRepairedOutput, + $vcard->serialize() + ); + + } + + function validateData() { + + $tests = []; + + // Correct + $tests[] = [ + "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:John Doe\r\nUID:foo\r\nEND:VCARD\r\n", + [], + "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:John Doe\r\nUID:foo\r\nEND:VCARD\r\n", + ]; + + // No VERSION + $tests[] = [ + "BEGIN:VCARD\r\nFN:John Doe\r\nUID:foo\r\nEND:VCARD\r\n", + [ + 'VERSION MUST appear exactly once in a VCARD component', + ], + "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:John Doe\r\nUID:foo\r\nEND:VCARD\r\n", + ]; + + // Unknown version + $tests[] = [ + "BEGIN:VCARD\r\nVERSION:2.2\r\nFN:John Doe\r\nUID:foo\r\nEND:VCARD\r\n", + [ + 'Only vcard version 4.0 (RFC6350), version 3.0 (RFC2426) or version 2.1 (icm-vcard-2.1) are supported.', + ], + "BEGIN:VCARD\r\nVERSION:2.1\r\nFN:John Doe\r\nUID:foo\r\nEND:VCARD\r\n", + ]; + + // No FN + $tests[] = [ + "BEGIN:VCARD\r\nVERSION:4.0\r\nUID:foo\r\nEND:VCARD\r\n", + [ + 'The FN property must appear in the VCARD component exactly 1 time', + ], + "BEGIN:VCARD\r\nVERSION:4.0\r\nUID:foo\r\nEND:VCARD\r\n", + ]; + // No FN, N fallback + $tests[] = [ + "BEGIN:VCARD\r\nVERSION:4.0\r\nUID:foo\r\nN:Doe;John;;;;;\r\nEND:VCARD\r\n", + [ + 'The FN property must appear in the VCARD component exactly 1 time', + ], + "BEGIN:VCARD\r\nVERSION:4.0\r\nUID:foo\r\nN:Doe;John;;;;;\r\nFN:John Doe\r\nEND:VCARD\r\n", + ]; + // No FN, N fallback, no first name + $tests[] = [ + "BEGIN:VCARD\r\nVERSION:4.0\r\nUID:foo\r\nN:Doe;;;;;;\r\nEND:VCARD\r\n", + [ + 'The FN property must appear in the VCARD component exactly 1 time', + ], + "BEGIN:VCARD\r\nVERSION:4.0\r\nUID:foo\r\nN:Doe;;;;;;\r\nFN:Doe\r\nEND:VCARD\r\n", + ]; + + // No FN, ORG fallback + $tests[] = [ + "BEGIN:VCARD\r\nVERSION:4.0\r\nUID:foo\r\nORG:Acme Co.\r\nEND:VCARD\r\n", + [ + 'The FN property must appear in the VCARD component exactly 1 time', + ], + "BEGIN:VCARD\r\nVERSION:4.0\r\nUID:foo\r\nORG:Acme Co.\r\nFN:Acme Co.\r\nEND:VCARD\r\n", + ]; + return $tests; + + } + + function testGetDocumentType() { + + $vcard = new VCard([], false); + $vcard->VERSION = '2.1'; + $this->assertEquals(VCard::VCARD21, $vcard->getDocumentType()); + + $vcard = new VCard([], false); + $vcard->VERSION = '3.0'; + $this->assertEquals(VCard::VCARD30, $vcard->getDocumentType()); + + $vcard = new VCard([], false); + $vcard->VERSION = '4.0'; + $this->assertEquals(VCard::VCARD40, $vcard->getDocumentType()); + + $vcard = new VCard([], false); + $this->assertEquals(VCard::UNKNOWN, $vcard->getDocumentType()); + } + + function testGetByType() { + $vcard = <<<VCF +BEGIN:VCARD +VERSION:3.0 +EMAIL;TYPE=home:1@example.org +EMAIL;TYPE=work:2@example.org +END:VCARD +VCF; + + $vcard = VObject\Reader::read($vcard); + $this->assertEquals('1@example.org', $vcard->getByType('EMAIL', 'home')->getValue()); + $this->assertEquals('2@example.org', $vcard->getByType('EMAIL', 'work')->getValue()); + $this->assertNull($vcard->getByType('EMAIL', 'non-existant')); + $this->assertNull($vcard->getByType('ADR', 'non-existant')); + } + + function testPreferredNoPref() { + + $vcard = <<<VCF +BEGIN:VCARD +VERSION:3.0 +EMAIL:1@example.org +EMAIL:2@example.org +END:VCARD +VCF; + + $vcard = VObject\Reader::read($vcard); + $this->assertEquals('1@example.org', $vcard->preferred('EMAIL')->getValue()); + + } + + function testPreferredWithPref() { + + $vcard = <<<VCF +BEGIN:VCARD +VERSION:3.0 +EMAIL:1@example.org +EMAIL;TYPE=PREF:2@example.org +END:VCARD +VCF; + + $vcard = VObject\Reader::read($vcard); + $this->assertEquals('2@example.org', $vcard->preferred('EMAIL')->getValue()); + + } + + function testPreferredWith40Pref() { + + $vcard = <<<VCF +BEGIN:VCARD +VERSION:4.0 +EMAIL:1@example.org +EMAIL;PREF=3:2@example.org +EMAIL;PREF=2:3@example.org +END:VCARD +VCF; + + $vcard = VObject\Reader::read($vcard); + $this->assertEquals('3@example.org', $vcard->preferred('EMAIL')->getValue()); + + } + + function testPreferredNotFound() { + + $vcard = <<<VCF +BEGIN:VCARD +VERSION:4.0 +END:VCARD +VCF; + + $vcard = VObject\Reader::read($vcard); + $this->assertNull($vcard->preferred('EMAIL')); + + } + + function testNoUIDCardDAV() { + + $vcard = <<<VCF +BEGIN:VCARD +VERSION:4.0 +FN:John Doe +END:VCARD +VCF; + $this->assertValidate( + $vcard, + VCARD::PROFILE_CARDDAV, + 3, + 'vCards on CardDAV servers MUST have a UID property.' + ); + + } + + function testNoUIDNoCardDAV() { + + $vcard = <<<VCF +BEGIN:VCARD +VERSION:4.0 +FN:John Doe +END:VCARD +VCF; + $this->assertValidate( + $vcard, + 0, + 2, + 'Adding a UID to a vCard property is recommended.' + ); + + } + function testNoUIDNoCardDAVRepair() { + + $vcard = <<<VCF +BEGIN:VCARD +VERSION:4.0 +FN:John Doe +END:VCARD +VCF; + $this->assertValidate( + $vcard, + VCARD::REPAIR, + 1, + 'Adding a UID to a vCard property is recommended.' + ); + + } + + function testVCard21CardDAV() { + + $vcard = <<<VCF +BEGIN:VCARD +VERSION:2.1 +FN:John Doe +UID:foo +END:VCARD +VCF; + $this->assertValidate( + $vcard, + VCARD::PROFILE_CARDDAV, + 3, + 'CardDAV servers are not allowed to accept vCard 2.1.' + ); + + } + + function testVCard21NoCardDAV() { + + $vcard = <<<VCF +BEGIN:VCARD +VERSION:2.1 +FN:John Doe +UID:foo +END:VCARD +VCF; + $this->assertValidate( + $vcard, + 0, + 0 + ); + + } + + function assertValidate($vcf, $options, $expectedLevel, $expectedMessage = null) { + + $vcal = VObject\Reader::read($vcf); + $result = $vcal->validate($options); + + $this->assertValidateResult($result, $expectedLevel, $expectedMessage); + + } + + function assertValidateResult($input, $expectedLevel, $expectedMessage = null) { + + $messages = []; + foreach ($input as $warning) { + $messages[] = $warning['message']; + } + + if ($expectedLevel === 0) { + $this->assertEquals(0, count($input), 'No validation messages were expected. We got: ' . implode(', ', $messages)); + } else { + $this->assertEquals(1, count($input), 'We expected exactly 1 validation message, We got: ' . implode(', ', $messages)); + + $this->assertEquals($expectedMessage, $input[0]['message']); + $this->assertEquals($expectedLevel, $input[0]['level']); + } + + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VEventTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VEventTest.php new file mode 100644 index 00000000000..11acd3f6b72 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VEventTest.php @@ -0,0 +1,95 @@ +<?php + +namespace Sabre\VObject\Component; + +class VEventTest extends \PHPUnit_Framework_TestCase { + + /** + * @dataProvider timeRangeTestData + */ + function testInTimeRange(VEvent $vevent, $start, $end, $outcome) { + + $this->assertEquals($outcome, $vevent->isInTimeRange($start, $end)); + + } + + function timeRangeTestData() { + + $tests = []; + + $calendar = new VCalendar(); + + $vevent = $calendar->createComponent('VEVENT'); + $vevent->DTSTART = '20111223T120000Z'; + $tests[] = [$vevent, new \DateTime('2011-01-01'), new \DateTime('2012-01-01'), true]; + $tests[] = [$vevent, new \DateTime('2011-01-01'), new \DateTime('2011-11-01'), false]; + + $vevent2 = clone $vevent; + $vevent2->DTEND = '20111225T120000Z'; + $tests[] = [$vevent2, new \DateTime('2011-01-01'), new \DateTime('2012-01-01'), true]; + $tests[] = [$vevent2, new \DateTime('2011-01-01'), new \DateTime('2011-11-01'), false]; + + $vevent3 = clone $vevent; + $vevent3->DURATION = 'P1D'; + $tests[] = [$vevent3, new \DateTime('2011-01-01'), new \DateTime('2012-01-01'), true]; + $tests[] = [$vevent3, new \DateTime('2011-01-01'), new \DateTime('2011-11-01'), false]; + + $vevent4 = clone $vevent; + $vevent4->DTSTART = '20111225'; + $vevent4->DTSTART['VALUE'] = 'DATE'; + $tests[] = [$vevent4, new \DateTime('2011-01-01'), new \DateTime('2012-01-01'), true]; + $tests[] = [$vevent4, new \DateTime('2011-01-01'), new \DateTime('2011-11-01'), false]; + // Event with no end date should be treated as lasting the entire day. + $tests[] = [$vevent4, new \DateTime('2011-12-25 16:00:00'), new \DateTime('2011-12-25 17:00:00'), true]; + // DTEND is non inclusive so all day events should not be returned on the next day. + $tests[] = [$vevent4, new \DateTime('2011-12-26 00:00:00'), new \DateTime('2011-12-26 17:00:00'), false]; + // The timezone of timerange in question also needs to be considered. + $tests[] = [$vevent4, new \DateTime('2011-12-26 00:00:00', new \DateTimeZone('Europe/Berlin')), new \DateTime('2011-12-26 17:00:00', new \DateTimeZone('Europe/Berlin')), false]; + + $vevent5 = clone $vevent; + $vevent5->DURATION = 'P1D'; + $vevent5->RRULE = 'FREQ=YEARLY'; + $tests[] = [$vevent5, new \DateTime('2011-01-01'), new \DateTime('2012-01-01'), true]; + $tests[] = [$vevent5, new \DateTime('2011-01-01'), new \DateTime('2011-11-01'), false]; + $tests[] = [$vevent5, new \DateTime('2013-12-01'), new \DateTime('2013-12-31'), true]; + + $vevent6 = clone $vevent; + $vevent6->DTSTART = '20111225'; + $vevent6->DTSTART['VALUE'] = 'DATE'; + $vevent6->DTEND = '20111225'; + $vevent6->DTEND['VALUE'] = 'DATE'; + + $tests[] = [$vevent6, new \DateTime('2011-01-01'), new \DateTime('2012-01-01'), true]; + $tests[] = [$vevent6, new \DateTime('2011-01-01'), new \DateTime('2011-11-01'), false]; + + // Added this test to ensure that recurrence rules with no DTEND also + // get checked for the entire day. + $vevent7 = clone $vevent; + $vevent7->DTSTART = '20120101'; + $vevent7->DTSTART['VALUE'] = 'DATE'; + $vevent7->RRULE = 'FREQ=MONTHLY'; + $tests[] = [$vevent7, new \DateTime('2012-02-01 15:00:00'), new \DateTime('2012-02-02'), true]; + // The timezone of timerange in question should also be considered. + $tests[] = [$vevent7, new \DateTime('2012-02-02 00:00:00', new \DateTimeZone('Europe/Berlin')), new \DateTime('2012-02-03 00:00:00', new \DateTimeZone('Europe/Berlin')), false]; + + // Added this test to check recurring events that have no instances. + $vevent8 = clone $vevent; + $vevent8->DTSTART = '20130329T140000'; + $vevent8->DTEND = '20130329T153000'; + $vevent8->RRULE = ['FREQ' => 'WEEKLY', 'BYDAY' => ['FR'], 'UNTIL' => '20130412T115959Z']; + $vevent8->add('EXDATE', '20130405T140000'); + $vevent8->add('EXDATE', '20130329T140000'); + $tests[] = [$vevent8, new \DateTime('2013-03-01'), new \DateTime('2013-04-01'), false]; + + // Added this test to check recurring all day event that repeat every day + $vevent9 = clone $vevent; + $vevent9->DTSTART = '20161027'; + $vevent9->DTEND = '20161028'; + $vevent9->RRULE = 'FREQ=DAILY'; + $tests[] = [$vevent9, new \DateTime('2016-10-31'), new \DateTime('2016-12-12'), true]; + + return $tests; + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VFreeBusyTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VFreeBusyTest.php new file mode 100644 index 00000000000..2d463fd09ad --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VFreeBusyTest.php @@ -0,0 +1,66 @@ +<?php + +namespace Sabre\VObject\Component; + +use Sabre\VObject; +use Sabre\VObject\Reader; + +class VFreeBusyTest extends \PHPUnit_Framework_TestCase { + + function testIsFree() { + + $input = <<<BLA +BEGIN:VCALENDAR +BEGIN:VFREEBUSY +FREEBUSY;FBTYPE=FREE:20120912T000500Z/PT1H +FREEBUSY;FBTYPE=BUSY:20120912T010000Z/20120912T020000Z +FREEBUSY;FBTYPE=BUSY-TENTATIVE:20120912T020000Z/20120912T030000Z +FREEBUSY;FBTYPE=BUSY-UNAVAILABLE:20120912T030000Z/20120912T040000Z +FREEBUSY;FBTYPE=BUSY:20120912T050000Z/20120912T060000Z,20120912T080000Z/20120912T090000Z +FREEBUSY;FBTYPE=BUSY:20120912T100000Z/PT1H +END:VFREEBUSY +END:VCALENDAR +BLA; + + $obj = VObject\Reader::read($input); + $vfb = $obj->VFREEBUSY; + + $tz = new \DateTimeZone('UTC'); + + $this->assertFalse($vfb->isFree(new \DateTime('2012-09-12 01:15:00', $tz), new \DateTime('2012-09-12 01:45:00', $tz))); + $this->assertFalse($vfb->isFree(new \DateTime('2012-09-12 08:05:00', $tz), new \DateTime('2012-09-12 08:10:00', $tz))); + $this->assertFalse($vfb->isFree(new \DateTime('2012-09-12 10:15:00', $tz), new \DateTime('2012-09-12 10:45:00', $tz))); + + // Checking whether the end time is treated as non-inclusive + $this->assertTrue($vfb->isFree(new \DateTime('2012-09-12 09:00:00', $tz), new \DateTime('2012-09-12 09:15:00', $tz))); + $this->assertTrue($vfb->isFree(new \DateTime('2012-09-12 09:45:00', $tz), new \DateTime('2012-09-12 10:00:00', $tz))); + $this->assertTrue($vfb->isFree(new \DateTime('2012-09-12 11:00:00', $tz), new \DateTime('2012-09-12 12:00:00', $tz))); + + } + + function testValidate() { + + $input = <<<HI +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:YoYo +BEGIN:VFREEBUSY +UID:some-random-id +DTSTAMP:20140402T180200Z +END:VFREEBUSY +END:VCALENDAR +HI; + + $obj = Reader::read($input); + + $warnings = $obj->validate(); + $messages = []; + foreach ($warnings as $warning) { + $messages[] = $warning['message']; + } + + $this->assertEquals([], $messages); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VJournalTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VJournalTest.php new file mode 100644 index 00000000000..1a2362d4a73 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VJournalTest.php @@ -0,0 +1,100 @@ +<?php + +namespace Sabre\VObject\Component; + +use Sabre\VObject\Component; +use Sabre\VObject\Reader; + +class VJournalTest extends \PHPUnit_Framework_TestCase { + + /** + * @dataProvider timeRangeTestData + */ + function testInTimeRange(VJournal $vtodo, $start, $end, $outcome) { + + $this->assertEquals($outcome, $vtodo->isInTimeRange($start, $end)); + + } + + function testValidate() { + + $input = <<<HI +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:YoYo +BEGIN:VJOURNAL +UID:12345678 +DTSTAMP:20140402T174100Z +END:VJOURNAL +END:VCALENDAR +HI; + + $obj = Reader::read($input); + + $warnings = $obj->validate(); + $messages = []; + foreach ($warnings as $warning) { + $messages[] = $warning['message']; + } + + $this->assertEquals([], $messages); + + } + + function testValidateBroken() { + + $input = <<<HI +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:YoYo +BEGIN:VJOURNAL +UID:12345678 +DTSTAMP:20140402T174100Z +URL:http://example.org/ +URL:http://example.com/ +END:VJOURNAL +END:VCALENDAR +HI; + + $obj = Reader::read($input); + + $warnings = $obj->validate(); + $messages = []; + foreach ($warnings as $warning) { + $messages[] = $warning['message']; + } + + $this->assertEquals( + ["URL MUST NOT appear more than once in a VJOURNAL component"], + $messages + ); + + } + + function timeRangeTestData() { + + $calendar = new VCalendar(); + + $tests = []; + + $vjournal = $calendar->createComponent('VJOURNAL'); + $vjournal->DTSTART = '20111223T120000Z'; + $tests[] = [$vjournal, new \DateTime('2011-01-01'), new \DateTime('2012-01-01'), true]; + $tests[] = [$vjournal, new \DateTime('2011-01-01'), new \DateTime('2011-11-01'), false]; + + $vjournal2 = $calendar->createComponent('VJOURNAL'); + $vjournal2->DTSTART = '20111223'; + $vjournal2->DTSTART['VALUE'] = 'DATE'; + $tests[] = [$vjournal2, new \DateTime('2011-01-01'), new \DateTime('2012-01-01'), true]; + $tests[] = [$vjournal2, new \DateTime('2011-01-01'), new \DateTime('2011-11-01'), false]; + + $vjournal3 = $calendar->createComponent('VJOURNAL'); + $tests[] = [$vjournal3, new \DateTime('2011-01-01'), new \DateTime('2012-01-01'), false]; + $tests[] = [$vjournal3, new \DateTime('2011-01-01'), new \DateTime('2011-11-01'), false]; + + return $tests; + } + + + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VTimeZoneTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VTimeZoneTest.php new file mode 100644 index 00000000000..f320fd3d6e8 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VTimeZoneTest.php @@ -0,0 +1,56 @@ +<?php + +namespace Sabre\VObject\Component; + +use Sabre\VObject\Reader; + +class VTimeZoneTest extends \PHPUnit_Framework_TestCase { + + function testValidate() { + + $input = <<<HI +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:YoYo +BEGIN:VTIMEZONE +TZID:America/Toronto +END:VTIMEZONE +END:VCALENDAR +HI; + + $obj = Reader::read($input); + + $warnings = $obj->validate(); + $messages = []; + foreach ($warnings as $warning) { + $messages[] = $warning['message']; + } + + $this->assertEquals([], $messages); + + } + + function testGetTimeZone() { + + $input = <<<HI +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:YoYo +BEGIN:VTIMEZONE +TZID:America/Toronto +END:VTIMEZONE +END:VCALENDAR +HI; + + $obj = Reader::read($input); + + $tz = new \DateTimeZone('America/Toronto'); + + $this->assertEquals( + $tz, + $obj->VTIMEZONE->getTimeZone() + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VTodoTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VTodoTest.php new file mode 100644 index 00000000000..85c6e07842f --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Component/VTodoTest.php @@ -0,0 +1,178 @@ +<?php + +namespace Sabre\VObject\Component; + +use Sabre\VObject\Component; +use Sabre\VObject\Reader; + +class VTodoTest extends \PHPUnit_Framework_TestCase { + + /** + * @dataProvider timeRangeTestData + */ + function testInTimeRange(VTodo $vtodo, $start, $end, $outcome) { + + $this->assertEquals($outcome, $vtodo->isInTimeRange($start, $end)); + + } + + function timeRangeTestData() { + + $tests = []; + + $calendar = new VCalendar(); + + $vtodo = $calendar->createComponent('VTODO'); + $vtodo->DTSTART = '20111223T120000Z'; + $tests[] = [$vtodo, new \DateTime('2011-01-01'), new \DateTime('2012-01-01'), true]; + $tests[] = [$vtodo, new \DateTime('2011-01-01'), new \DateTime('2011-11-01'), false]; + + $vtodo2 = clone $vtodo; + $vtodo2->DURATION = 'P1D'; + $tests[] = [$vtodo2, new \DateTime('2011-01-01'), new \DateTime('2012-01-01'), true]; + $tests[] = [$vtodo2, new \DateTime('2011-01-01'), new \DateTime('2011-11-01'), false]; + + $vtodo3 = clone $vtodo; + $vtodo3->DUE = '20111225'; + $tests[] = [$vtodo3, new \DateTime('2011-01-01'), new \DateTime('2012-01-01'), true]; + $tests[] = [$vtodo3, new \DateTime('2011-01-01'), new \DateTime('2011-11-01'), false]; + + $vtodo4 = $calendar->createComponent('VTODO'); + $vtodo4->DUE = '20111225'; + $tests[] = [$vtodo4, new \DateTime('2011-01-01'), new \DateTime('2012-01-01'), true]; + $tests[] = [$vtodo4, new \DateTime('2011-01-01'), new \DateTime('2011-11-01'), false]; + + $vtodo5 = $calendar->createComponent('VTODO'); + $vtodo5->COMPLETED = '20111225'; + $tests[] = [$vtodo5, new \DateTime('2011-01-01'), new \DateTime('2012-01-01'), true]; + $tests[] = [$vtodo5, new \DateTime('2011-01-01'), new \DateTime('2011-11-01'), false]; + + $vtodo6 = $calendar->createComponent('VTODO'); + $vtodo6->CREATED = '20111225'; + $tests[] = [$vtodo6, new \DateTime('2011-01-01'), new \DateTime('2012-01-01'), true]; + $tests[] = [$vtodo6, new \DateTime('2011-01-01'), new \DateTime('2011-11-01'), false]; + + $vtodo7 = $calendar->createComponent('VTODO'); + $vtodo7->CREATED = '20111225'; + $vtodo7->COMPLETED = '20111226'; + $tests[] = [$vtodo7, new \DateTime('2011-01-01'), new \DateTime('2012-01-01'), true]; + $tests[] = [$vtodo7, new \DateTime('2011-01-01'), new \DateTime('2011-11-01'), false]; + + $vtodo7 = $calendar->createComponent('VTODO'); + $tests[] = [$vtodo7, new \DateTime('2011-01-01'), new \DateTime('2012-01-01'), true]; + $tests[] = [$vtodo7, new \DateTime('2011-01-01'), new \DateTime('2011-11-01'), true]; + + return $tests; + + } + + function testValidate() { + + $input = <<<HI +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:YoYo +BEGIN:VTODO +UID:1234-21355-123156 +DTSTAMP:20140402T183400Z +END:VTODO +END:VCALENDAR +HI; + + $obj = Reader::read($input); + + $warnings = $obj->validate(); + $messages = []; + foreach ($warnings as $warning) { + $messages[] = $warning['message']; + } + + $this->assertEquals([], $messages); + + } + + function testValidateInvalid() { + + $input = <<<HI +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:YoYo +BEGIN:VTODO +END:VTODO +END:VCALENDAR +HI; + + $obj = Reader::read($input); + + $warnings = $obj->validate(); + $messages = []; + foreach ($warnings as $warning) { + $messages[] = $warning['message']; + } + + $this->assertEquals([ + "UID MUST appear exactly once in a VTODO component", + "DTSTAMP MUST appear exactly once in a VTODO component", + ], $messages); + + } + + function testValidateDUEDTSTARTMisMatch() { + + $input = <<<HI +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:YoYo +BEGIN:VTODO +UID:FOO +DTSTART;VALUE=DATE-TIME:20140520T131600Z +DUE;VALUE=DATE:20140520 +DTSTAMP;VALUE=DATE-TIME:20140520T131600Z +END:VTODO +END:VCALENDAR +HI; + + $obj = Reader::read($input); + + $warnings = $obj->validate(); + $messages = []; + foreach ($warnings as $warning) { + $messages[] = $warning['message']; + } + + $this->assertEquals([ + "The value type (DATE or DATE-TIME) must be identical for DUE and DTSTART", + ], $messages); + + } + + function testValidateDUEbeforeDTSTART() { + + $input = <<<HI +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:YoYo +BEGIN:VTODO +UID:FOO +DTSTART;VALUE=DATE:20140520 +DUE;VALUE=DATE:20140518 +DTSTAMP;VALUE=DATE-TIME:20140520T131600Z +END:VTODO +END:VCALENDAR +HI; + + $obj = Reader::read($input); + + $warnings = $obj->validate(); + $messages = []; + foreach ($warnings as $warning) { + $messages[] = $warning['message']; + } + + $this->assertEquals([ + "DUE must occur after DTSTART", + ], $messages); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/ComponentTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ComponentTest.php new file mode 100644 index 00000000000..9323a43d1c1 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ComponentTest.php @@ -0,0 +1,527 @@ +<?php + +namespace Sabre\VObject; + +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Component\VCard; + +class ComponentTest extends \PHPUnit_Framework_TestCase { + + function testIterate() { + + $comp = new VCalendar([], false); + + $sub = $comp->createComponent('VEVENT'); + $comp->add($sub); + + $sub = $comp->createComponent('VTODO'); + $comp->add($sub); + + $count = 0; + foreach ($comp->children() as $key => $subcomponent) { + + $count++; + $this->assertInstanceOf('Sabre\\VObject\\Component', $subcomponent); + + } + $this->assertEquals(2, $count); + $this->assertEquals(1, $key); + + } + + function testMagicGet() { + + $comp = new VCalendar([], false); + + $sub = $comp->createComponent('VEVENT'); + $comp->add($sub); + + $sub = $comp->createComponent('VTODO'); + $comp->add($sub); + + $event = $comp->vevent; + $this->assertInstanceOf('Sabre\\VObject\\Component', $event); + $this->assertEquals('VEVENT', $event->name); + + $this->assertInternalType('null', $comp->vjournal); + + } + + function testMagicGetGroups() { + + $comp = new VCard(); + + $sub = $comp->createProperty('GROUP1.EMAIL', '1@1.com'); + $comp->add($sub); + + $sub = $comp->createProperty('GROUP2.EMAIL', '2@2.com'); + $comp->add($sub); + + $sub = $comp->createProperty('EMAIL', '3@3.com'); + $comp->add($sub); + + $emails = $comp->email; + $this->assertEquals(3, count($emails)); + + $email1 = $comp->{"group1.email"}; + $this->assertEquals('EMAIL', $email1[0]->name); + $this->assertEquals('GROUP1', $email1[0]->group); + + $email3 = $comp->{".email"}; + $this->assertEquals('EMAIL', $email3[0]->name); + $this->assertEquals(null, $email3[0]->group); + + } + + function testMagicIsset() { + + $comp = new VCalendar(); + + $sub = $comp->createComponent('VEVENT'); + $comp->add($sub); + + $sub = $comp->createComponent('VTODO'); + $comp->add($sub); + + $this->assertTrue(isset($comp->vevent)); + $this->assertTrue(isset($comp->vtodo)); + $this->assertFalse(isset($comp->vjournal)); + + } + + function testMagicSetScalar() { + + $comp = new VCalendar(); + $comp->myProp = 'myValue'; + + $this->assertInstanceOf('Sabre\\VObject\\Property', $comp->MYPROP); + $this->assertEquals('myValue', (string)$comp->MYPROP); + + + } + + function testMagicSetScalarTwice() { + + $comp = new VCalendar([], false); + $comp->myProp = 'myValue'; + $comp->myProp = 'myValue'; + + $this->assertEquals(1, count($comp->children())); + $this->assertInstanceOf('Sabre\\VObject\\Property', $comp->MYPROP); + $this->assertEquals('myValue', (string)$comp->MYPROP); + + } + + function testMagicSetArray() { + + $comp = new VCalendar(); + $comp->ORG = ['Acme Inc', 'Section 9']; + + $this->assertInstanceOf('Sabre\\VObject\\Property', $comp->ORG); + $this->assertEquals(['Acme Inc', 'Section 9'], $comp->ORG->getParts()); + + } + + function testMagicSetComponent() { + + $comp = new VCalendar(); + + // Note that 'myProp' is ignored here. + $comp->myProp = $comp->createComponent('VEVENT'); + + $this->assertEquals(1, count($comp)); + + $this->assertEquals('VEVENT', $comp->VEVENT->name); + + } + + function testMagicSetTwice() { + + $comp = new VCalendar([], false); + + $comp->VEVENT = $comp->createComponent('VEVENT'); + $comp->VEVENT = $comp->createComponent('VEVENT'); + + $this->assertEquals(1, count($comp->children())); + + $this->assertEquals('VEVENT', $comp->VEVENT->name); + + } + + function testArrayAccessGet() { + + $comp = new VCalendar([], false); + + $event = $comp->createComponent('VEVENT'); + $event->summary = 'Event 1'; + + $comp->add($event); + + $event2 = clone $event; + $event2->summary = 'Event 2'; + + $comp->add($event2); + + $this->assertEquals(2, count($comp->children())); + $this->assertTrue($comp->vevent[1] instanceof Component); + $this->assertEquals('Event 2', (string)$comp->vevent[1]->summary); + + } + + function testArrayAccessExists() { + + $comp = new VCalendar(); + + $event = $comp->createComponent('VEVENT'); + $event->summary = 'Event 1'; + + $comp->add($event); + + $event2 = clone $event; + $event2->summary = 'Event 2'; + + $comp->add($event2); + + $this->assertTrue(isset($comp->vevent[0])); + $this->assertTrue(isset($comp->vevent[1])); + + } + + /** + * @expectedException LogicException + */ + function testArrayAccessSet() { + + $comp = new VCalendar(); + $comp['hey'] = 'hi there'; + + } + /** + * @expectedException LogicException + */ + function testArrayAccessUnset() { + + $comp = new VCalendar(); + unset($comp[0]); + + } + + function testAddScalar() { + + $comp = new VCalendar([], false); + + $comp->add('myprop', 'value'); + + $this->assertEquals(1, count($comp->children())); + + $bla = $comp->children()[0]; + + $this->assertTrue($bla instanceof Property); + $this->assertEquals('MYPROP', $bla->name); + $this->assertEquals('value', (string)$bla); + + } + + function testAddScalarParams() { + + $comp = new VCalendar([], false); + + $comp->add('myprop', 'value', ['param1' => 'value1']); + + $this->assertEquals(1, count($comp->children())); + + $bla = $comp->children()[0]; + + $this->assertInstanceOf('Sabre\\VObject\\Property', $bla); + $this->assertEquals('MYPROP', $bla->name); + $this->assertEquals('value', (string)$bla); + + $this->assertEquals(1, count($bla->parameters())); + + $this->assertEquals('PARAM1', $bla->parameters['PARAM1']->name); + $this->assertEquals('value1', $bla->parameters['PARAM1']->getValue()); + + } + + + function testAddComponent() { + + $comp = new VCalendar([], false); + + $comp->add($comp->createComponent('VEVENT')); + + $this->assertEquals(1, count($comp->children())); + + $this->assertEquals('VEVENT', $comp->VEVENT->name); + + } + + function testAddComponentTwice() { + + $comp = new VCalendar([], false); + + $comp->add($comp->createComponent('VEVENT')); + $comp->add($comp->createComponent('VEVENT')); + + $this->assertEquals(2, count($comp->children())); + + $this->assertEquals('VEVENT', $comp->VEVENT->name); + + } + + /** + * @expectedException InvalidArgumentException + */ + function testAddArgFail() { + + $comp = new VCalendar(); + $comp->add($comp->createComponent('VEVENT'), 'hello'); + + } + + /** + * @expectedException InvalidArgumentException + */ + function testAddArgFail2() { + + $comp = new VCalendar(); + $comp->add([]); + + } + + function testMagicUnset() { + + $comp = new VCalendar([], false); + $comp->add($comp->createComponent('VEVENT')); + + unset($comp->vevent); + + $this->assertEquals(0, count($comp->children())); + + } + + + function testCount() { + + $comp = new VCalendar(); + $this->assertEquals(1, $comp->count()); + + } + + function testChildren() { + + $comp = new VCalendar([], false); + + // Note that 'myProp' is ignored here. + $comp->add($comp->createComponent('VEVENT')); + $comp->add($comp->createComponent('VTODO')); + + $r = $comp->children(); + $this->assertInternalType('array', $r); + $this->assertEquals(2, count($r)); + } + + function testGetComponents() { + + $comp = new VCalendar(); + + $comp->add($comp->createProperty('FOO', 'BAR')); + $comp->add($comp->createComponent('VTODO')); + + $r = $comp->getComponents(); + $this->assertInternalType('array', $r); + $this->assertEquals(1, count($r)); + $this->assertEquals('VTODO', $r[0]->name); + } + + function testSerialize() { + + $comp = new VCalendar([], false); + $this->assertEquals("BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n", $comp->serialize()); + + } + + function testSerializeChildren() { + + $comp = new VCalendar([], false); + $event = $comp->add($comp->createComponent('VEVENT')); + unset($event->DTSTAMP, $event->UID); + $todo = $comp->add($comp->createComponent('VTODO')); + unset($todo->DTSTAMP, $todo->UID); + + $str = $comp->serialize(); + + $this->assertEquals("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nEND:VEVENT\r\nBEGIN:VTODO\r\nEND:VTODO\r\nEND:VCALENDAR\r\n", $str); + + } + + function testSerializeOrderCompAndProp() { + + $comp = new VCalendar([], false); + $comp->add($event = $comp->createComponent('VEVENT')); + $comp->add('PROP1', 'BLABLA'); + $comp->add('VERSION', '2.0'); + $comp->add($comp->createComponent('VTIMEZONE')); + + unset($event->DTSTAMP, $event->UID); + $str = $comp->serialize(); + + $this->assertEquals("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPROP1:BLABLA\r\nBEGIN:VTIMEZONE\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", $str); + + } + + function testAnotherSerializeOrderProp() { + + $prop4s = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']; + + $comp = new VCard([], false); + + $comp->__set('SOMEPROP', 'FOO'); + $comp->__set('ANOTHERPROP', 'FOO'); + $comp->__set('THIRDPROP', 'FOO'); + foreach ($prop4s as $prop4) { + $comp->add('PROP4', 'FOO ' . $prop4); + } + $comp->__set('PROPNUMBERFIVE', 'FOO'); + $comp->__set('PROPNUMBERSIX', 'FOO'); + $comp->__set('PROPNUMBERSEVEN', 'FOO'); + $comp->__set('PROPNUMBEREIGHT', 'FOO'); + $comp->__set('PROPNUMBERNINE', 'FOO'); + $comp->__set('PROPNUMBERTEN', 'FOO'); + $comp->__set('VERSION', '2.0'); + $comp->__set('UID', 'FOO'); + + $str = $comp->serialize(); + + $this->assertEquals("BEGIN:VCARD\r\nVERSION:2.0\r\nSOMEPROP:FOO\r\nANOTHERPROP:FOO\r\nTHIRDPROP:FOO\r\nPROP4:FOO 1\r\nPROP4:FOO 2\r\nPROP4:FOO 3\r\nPROP4:FOO 4\r\nPROP4:FOO 5\r\nPROP4:FOO 6\r\nPROP4:FOO 7\r\nPROP4:FOO 8\r\nPROP4:FOO 9\r\nPROP4:FOO 10\r\nPROPNUMBERFIVE:FOO\r\nPROPNUMBERSIX:FOO\r\nPROPNUMBERSEVEN:FOO\r\nPROPNUMBEREIGHT:FOO\r\nPROPNUMBERNINE:FOO\r\nPROPNUMBERTEN:FOO\r\nUID:FOO\r\nEND:VCARD\r\n", $str); + + } + + function testInstantiateWithChildren() { + + $comp = new VCard([ + 'ORG' => ['Acme Inc.', 'Section 9'], + 'FN' => 'Finn The Human', + ]); + + $this->assertEquals(['Acme Inc.', 'Section 9'], $comp->ORG->getParts()); + $this->assertEquals('Finn The Human', $comp->FN->getValue()); + + } + + function testInstantiateSubComponent() { + + $comp = new VCalendar(); + $event = $comp->createComponent('VEVENT', [ + $comp->createProperty('UID', '12345'), + ]); + $comp->add($event); + + $this->assertEquals('12345', $comp->VEVENT->UID->getValue()); + + } + + function testRemoveByName() { + + $comp = new VCalendar([], false); + $comp->add('prop1', 'val1'); + $comp->add('prop2', 'val2'); + $comp->add('prop2', 'val2'); + + $comp->remove('prop2'); + $this->assertFalse(isset($comp->prop2)); + $this->assertTrue(isset($comp->prop1)); + + } + + function testRemoveByObj() { + + $comp = new VCalendar([], false); + $comp->add('prop1', 'val1'); + $prop = $comp->add('prop2', 'val2'); + + $comp->remove($prop); + $this->assertFalse(isset($comp->prop2)); + $this->assertTrue(isset($comp->prop1)); + + } + + /** + * @expectedException InvalidArgumentException + */ + function testRemoveNotFound() { + + $comp = new VCalendar([], false); + $prop = $comp->createProperty('A', 'B'); + $comp->remove($prop); + + } + + /** + * @dataProvider ruleData + */ + function testValidateRules($componentList, $errorCount) { + + $vcard = new Component\VCard(); + + $component = new FakeComponent($vcard, 'Hi', [], $defaults = false); + foreach ($componentList as $v) { + $component->add($v, 'Hello.'); + } + + $this->assertEquals($errorCount, count($component->validate())); + + } + + function testValidateRepair() { + + $vcard = new Component\VCard(); + + $component = new FakeComponent($vcard, 'Hi', [], $defaults = false); + $component->validate(Component::REPAIR); + $this->assertEquals('yow', $component->BAR->getValue()); + + } + + function ruleData() { + + return [ + + [[], 2], + [['FOO'], 3], + [['BAR'], 1], + [['BAZ'], 1], + [['BAR','BAZ'], 0], + [['BAR','BAZ','ZIM',], 0], + [['BAR','BAZ','ZIM','GIR'], 0], + [['BAR','BAZ','ZIM','GIR','GIR'], 1], + + ]; + + } + +} + +class FakeComponent extends Component { + + function getValidationRules() { + + return [ + 'FOO' => '0', + 'BAR' => '1', + 'BAZ' => '+', + 'ZIM' => '*', + 'GIR' => '?', + ]; + + } + + function getDefaults() { + + return [ + 'BAR' => 'yow', + ]; + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/DateTimeParserTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/DateTimeParserTest.php new file mode 100644 index 00000000000..677c2893677 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/DateTimeParserTest.php @@ -0,0 +1,699 @@ +<?php + +namespace Sabre\VObject; + +use DateInterval; +use DateTimeImmutable; +use DateTimeZone; + +class DateTimeParserTest extends \PHPUnit_Framework_TestCase { + + function testParseICalendarDuration() { + + $this->assertEquals('+1 weeks', DateTimeParser::parseDuration('P1W', true)); + $this->assertEquals('+5 days', DateTimeParser::parseDuration('P5D', true)); + $this->assertEquals('+5 days 3 hours 50 minutes 12 seconds', DateTimeParser::parseDuration('P5DT3H50M12S', true)); + $this->assertEquals('-1 weeks 50 minutes', DateTimeParser::parseDuration('-P1WT50M', true)); + $this->assertEquals('+50 days 3 hours 2 seconds', DateTimeParser::parseDuration('+P50DT3H2S', true)); + $this->assertEquals('+0 seconds', DateTimeParser::parseDuration('+PT0S', true)); + $this->assertEquals(new DateInterval('PT0S'), DateTimeParser::parseDuration('PT0S')); + + } + + function testParseICalendarDurationDateInterval() { + + $expected = new DateInterval('P7D'); + $this->assertEquals($expected, DateTimeParser::parseDuration('P1W')); + $this->assertEquals($expected, DateTimeParser::parse('P1W')); + + $expected = new DateInterval('PT3M'); + $expected->invert = true; + $this->assertEquals($expected, DateTimeParser::parseDuration('-PT3M')); + + } + + /** + * @expectedException \Sabre\VObject\InvalidDataException + */ + function testParseICalendarDurationFail() { + + DateTimeParser::parseDuration('P1X', true); + + } + + function testParseICalendarDateTime() { + + $dateTime = DateTimeParser::parseDateTime('20100316T141405'); + + $compare = new DateTimeImmutable('2010-03-16 14:14:05', new DateTimeZone('UTC')); + + $this->assertEquals($compare, $dateTime); + + } + + /** + * @depends testParseICalendarDateTime + * @expectedException \Sabre\VObject\InvalidDataException + */ + function testParseICalendarDateTimeBadFormat() { + + $dateTime = DateTimeParser::parseDateTime('20100316T141405 '); + + } + + /** + * @depends testParseICalendarDateTime + * @expectedException \Sabre\VObject\InvalidDataException + */ + function testParseICalendarDateTimeInvalidTime() { + + $dateTime = DateTimeParser::parseDateTime('20100316T251405'); + + } + + /** + * @depends testParseICalendarDateTime + */ + function testParseICalendarDateTimeUTC() { + + $dateTime = DateTimeParser::parseDateTime('20100316T141405Z'); + + $compare = new DateTimeImmutable('2010-03-16 14:14:05', new DateTimeZone('UTC')); + $this->assertEquals($compare, $dateTime); + + } + + /** + * @depends testParseICalendarDateTime + */ + function testParseICalendarDateTimeUTC2() { + + $dateTime = DateTimeParser::parseDateTime('20101211T160000Z'); + + $compare = new DateTimeImmutable('2010-12-11 16:00:00', new DateTimeZone('UTC')); + $this->assertEquals($compare, $dateTime); + + } + + /** + * @depends testParseICalendarDateTime + */ + function testParseICalendarDateTimeCustomTimeZone() { + + $dateTime = DateTimeParser::parseDateTime('20100316T141405', new DateTimeZone('Europe/Amsterdam')); + + $compare = new DateTimeImmutable('2010-03-16 14:14:05', new DateTimeZone('Europe/Amsterdam')); + $this->assertEquals($compare, $dateTime); + + } + + function testParseICalendarDate() { + + $dateTime = DateTimeParser::parseDate('20100316'); + + $expected = new DateTimeImmutable('2010-03-16 00:00:00', new DateTimeZone('UTC')); + + $this->assertEquals($expected, $dateTime); + + $dateTime = DateTimeParser::parse('20100316'); + $this->assertEquals($expected, $dateTime); + + } + + /** + * TCheck if a date with year > 4000 will not throw an exception. iOS seems to use 45001231 in yearly recurring events + */ + function testParseICalendarDateGreaterThan4000() { + + $dateTime = DateTimeParser::parseDate('45001231'); + + $expected = new DateTimeImmutable('4500-12-31 00:00:00', new DateTimeZone('UTC')); + + $this->assertEquals($expected, $dateTime); + + $dateTime = DateTimeParser::parse('45001231'); + $this->assertEquals($expected, $dateTime); + + } + + /** + * Check if a datetime with year > 4000 will not throw an exception. iOS seems to use 45001231T235959 in yearly recurring events + */ + function testParseICalendarDateTimeGreaterThan4000() { + + $dateTime = DateTimeParser::parseDateTime('45001231T235959'); + + $expected = new DateTimeImmutable('4500-12-31 23:59:59', new DateTimeZone('UTC')); + + $this->assertEquals($expected, $dateTime); + + $dateTime = DateTimeParser::parse('45001231T235959'); + $this->assertEquals($expected, $dateTime); + + } + + /** + * @depends testParseICalendarDate + * @expectedException \Sabre\VObject\InvalidDataException + */ + function testParseICalendarDateBadFormat() { + + $dateTime = DateTimeParser::parseDate('20100316T141405'); + + } + + /** + * @depends testParseICalendarDate + * @expectedException \Sabre\VObject\InvalidDataException + */ + function testParseICalendarDateInvalidDate() { + + $dateTime = DateTimeParser::parseDate('20101331'); + + } + + /** + * @dataProvider vcardDates + */ + function testVCardDate($input, $output) { + + $this->assertEquals( + $output, + DateTimeParser::parseVCardDateTime($input) + ); + + } + + /** + * @expectedException \Sabre\VObject\InvalidDataException + */ + function testBadVCardDate() { + + DateTimeParser::parseVCardDateTime('1985---01'); + + } + + /** + * @expectedException \Sabre\VObject\InvalidDataException + */ + function testBadVCardTime() { + + DateTimeParser::parseVCardTime('23:12:166'); + + } + + function vcardDates() { + + return [ + [ + "19961022T140000", + [ + "year" => 1996, + "month" => 10, + "date" => 22, + "hour" => 14, + "minute" => 00, + "second" => 00, + "timezone" => null + ], + ], + [ + "--1022T1400", + [ + "year" => null, + "month" => 10, + "date" => 22, + "hour" => 14, + "minute" => 00, + "second" => null, + "timezone" => null + ], + ], + [ + "---22T14", + [ + "year" => null, + "month" => null, + "date" => 22, + "hour" => 14, + "minute" => null, + "second" => null, + "timezone" => null + ], + ], + [ + "19850412", + [ + "year" => 1985, + "month" => 4, + "date" => 12, + "hour" => null, + "minute" => null, + "second" => null, + "timezone" => null + ], + ], + [ + "1985-04", + [ + "year" => 1985, + "month" => 04, + "date" => null, + "hour" => null, + "minute" => null, + "second" => null, + "timezone" => null + ], + ], + [ + "1985", + [ + "year" => 1985, + "month" => null, + "date" => null, + "hour" => null, + "minute" => null, + "second" => null, + "timezone" => null + ], + ], + [ + "--0412", + [ + "year" => null, + "month" => 4, + "date" => 12, + "hour" => null, + "minute" => null, + "second" => null, + "timezone" => null + ], + ], + [ + "---12", + [ + "year" => null, + "month" => null, + "date" => 12, + "hour" => null, + "minute" => null, + "second" => null, + "timezone" => null + ], + ], + [ + "T102200", + [ + "year" => null, + "month" => null, + "date" => null, + "hour" => 10, + "minute" => 22, + "second" => 0, + "timezone" => null + ], + ], + [ + "T1022", + [ + "year" => null, + "month" => null, + "date" => null, + "hour" => 10, + "minute" => 22, + "second" => null, + "timezone" => null + ], + ], + [ + "T10", + [ + "year" => null, + "month" => null, + "date" => null, + "hour" => 10, + "minute" => null, + "second" => null, + "timezone" => null + ], + ], + [ + "T-2200", + [ + "year" => null, + "month" => null, + "date" => null, + "hour" => null, + "minute" => 22, + "second" => 00, + "timezone" => null + ], + ], + [ + "T--00", + [ + "year" => null, + "month" => null, + "date" => null, + "hour" => null, + "minute" => null, + "second" => 00, + "timezone" => null + ], + ], + [ + "T102200Z", + [ + "year" => null, + "month" => null, + "date" => null, + "hour" => 10, + "minute" => 22, + "second" => 00, + "timezone" => 'Z' + ], + ], + [ + "T102200-0800", + [ + "year" => null, + "month" => null, + "date" => null, + "hour" => 10, + "minute" => 22, + "second" => 00, + "timezone" => '-0800' + ], + ], + + // extended format + [ + "2012-11-29T15:10:53Z", + [ + "year" => 2012, + "month" => 11, + "date" => 29, + "hour" => 15, + "minute" => 10, + "second" => 53, + "timezone" => 'Z' + ], + ], + + // with milliseconds + [ + "20121129T151053.123Z", + [ + "year" => 2012, + "month" => 11, + "date" => 29, + "hour" => 15, + "minute" => 10, + "second" => 53, + "timezone" => 'Z' + ], + ], + + // extended format with milliseconds + [ + "2012-11-29T15:10:53.123Z", + [ + "year" => 2012, + "month" => 11, + "date" => 29, + "hour" => 15, + "minute" => 10, + "second" => 53, + "timezone" => 'Z' + ], + ], + ]; + + } + + function testDateAndOrTime_DateWithYearMonthDay() { + + $this->assertDateAndOrTimeEqualsTo( + '20150128', + [ + 'year' => '2015', + 'month' => '01', + 'date' => '28' + ] + ); + + } + + function testDateAndOrTime_DateWithYearMonth() { + + $this->assertDateAndOrTimeEqualsTo( + '2015-01', + [ + 'year' => '2015', + 'month' => '01' + ] + ); + + } + + function testDateAndOrTime_DateWithMonth() { + + $this->assertDateAndOrTimeEqualsTo( + '--01', + [ + 'month' => '01' + ] + ); + + } + + function testDateAndOrTime_DateWithMonthDay() { + + $this->assertDateAndOrTimeEqualsTo( + '--0128', + [ + 'month' => '01', + 'date' => '28' + ] + ); + + } + + function testDateAndOrTime_DateWithDay() { + + $this->assertDateAndOrTimeEqualsTo( + '---28', + [ + 'date' => '28' + ] + ); + + } + + function testDateAndOrTime_TimeWithHour() { + + $this->assertDateAndOrTimeEqualsTo( + '13', + [ + 'hour' => '13' + ] + ); + + } + + function testDateAndOrTime_TimeWithHourMinute() { + + $this->assertDateAndOrTimeEqualsTo( + '1353', + [ + 'hour' => '13', + 'minute' => '53' + ] + ); + + } + + function testDateAndOrTime_TimeWithHourSecond() { + + $this->assertDateAndOrTimeEqualsTo( + '135301', + [ + 'hour' => '13', + 'minute' => '53', + 'second' => '01' + ] + + ); + + } + + function testDateAndOrTime_TimeWithMinute() { + + $this->assertDateAndOrTimeEqualsTo( + '-53', + [ + 'minute' => '53' + ] + ); + + } + + function testDateAndOrTime_TimeWithMinuteSecond() { + + $this->assertDateAndOrTimeEqualsTo( + '-5301', + [ + 'minute' => '53', + 'second' => '01' + ] + ); + + } + + function testDateAndOrTime_TimeWithSecond() { + + $this->assertTrue(true); + + /** + * This is unreachable due to a conflict between date and time pattern. + * This is an error in the specification, not in our implementation. + */ + } + + function testDateAndOrTime_TimeWithSecondZ() { + + $this->assertDateAndOrTimeEqualsTo( + '--01Z', + [ + 'second' => '01', + 'timezone' => 'Z' + ] + ); + + } + + function testDateAndOrTime_TimeWithSecondTZ() { + + $this->assertDateAndOrTimeEqualsTo( + '--01+1234', + [ + 'second' => '01', + 'timezone' => '+1234' + ] + ); + + } + + function testDateAndOrTime_DateTimeWithYearMonthDayHour() { + + $this->assertDateAndOrTimeEqualsTo( + '20150128T13', + [ + 'year' => '2015', + 'month' => '01', + 'date' => '28', + 'hour' => '13' + ] + ); + + } + + function testDateAndOrTime_DateTimeWithMonthDayHour() { + + $this->assertDateAndOrTimeEqualsTo( + '--0128T13', + [ + 'month' => '01', + 'date' => '28', + 'hour' => '13' + ] + ); + + } + + function testDateAndOrTime_DateTimeWithDayHour() { + + $this->assertDateAndOrTimeEqualsTo( + '---28T13', + [ + 'date' => '28', + 'hour' => '13' + ] + ); + + } + + function testDateAndOrTime_DateTimeWithDayHourMinute() { + + $this->assertDateAndOrTimeEqualsTo( + '---28T1353', + [ + 'date' => '28', + 'hour' => '13', + 'minute' => '53' + ] + ); + + } + + function testDateAndOrTime_DateTimeWithDayHourMinuteSecond() { + + $this->assertDateAndOrTimeEqualsTo( + '---28T135301', + [ + 'date' => '28', + 'hour' => '13', + 'minute' => '53', + 'second' => '01' + ] + ); + + } + + function testDateAndOrTime_DateTimeWithDayHourZ() { + + $this->assertDateAndOrTimeEqualsTo( + '---28T13Z', + [ + 'date' => '28', + 'hour' => '13', + 'timezone' => 'Z' + ] + ); + + } + + function testDateAndOrTime_DateTimeWithDayHourTZ() { + + $this->assertDateAndOrTimeEqualsTo( + '---28T13+1234', + [ + 'date' => '28', + 'hour' => '13', + 'timezone' => '+1234' + ] + ); + + } + + protected function assertDateAndOrTimeEqualsTo($date, $parts) { + + $this->assertSame( + DateTimeParser::parseVCardDateAndOrTime($date), + array_merge( + [ + 'year' => null, + 'month' => null, + 'date' => null, + 'hour' => null, + 'minute' => null, + 'second' => null, + 'timezone' => null + ], + $parts + ) + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/DocumentTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/DocumentTest.php new file mode 100644 index 00000000000..f1730fdeab1 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/DocumentTest.php @@ -0,0 +1,91 @@ +<?php + +namespace Sabre\VObject; + +class DocumentTest extends \PHPUnit_Framework_TestCase { + + function testGetDocumentType() { + + $doc = new MockDocument(); + $this->assertEquals(Document::UNKNOWN, $doc->getDocumentType()); + + } + + function testConstruct() { + + $doc = new MockDocument('VLIST'); + $this->assertEquals('VLIST', $doc->name); + + } + + function testCreateComponent() { + + $vcal = new Component\VCalendar([], false); + + $event = $vcal->createComponent('VEVENT'); + + $this->assertInstanceOf('Sabre\VObject\Component\VEvent', $event); + $vcal->add($event); + + $prop = $vcal->createProperty('X-PROP', '1234256', ['X-PARAM' => '3']); + $this->assertInstanceOf('Sabre\VObject\Property', $prop); + + $event->add($prop); + + unset( + $event->DTSTAMP, + $event->UID + ); + + $out = $vcal->serialize(); + $this->assertEquals("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nX-PROP;X-PARAM=3:1234256\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", $out); + + } + + function testCreate() { + + $vcal = new Component\VCalendar([], false); + + $event = $vcal->create('VEVENT'); + $this->assertInstanceOf('Sabre\VObject\Component\VEvent', $event); + + $prop = $vcal->create('CALSCALE'); + $this->assertInstanceOf('Sabre\VObject\Property\Text', $prop); + + } + + function testGetClassNameForPropertyValue() { + + $vcal = new Component\VCalendar([], false); + $this->assertEquals('Sabre\\VObject\\Property\\Text', $vcal->getClassNameForPropertyValue('TEXT')); + $this->assertNull($vcal->getClassNameForPropertyValue('FOO')); + + } + + function testDestroy() { + + $vcal = new Component\VCalendar([], false); + $event = $vcal->createComponent('VEVENT'); + + $this->assertInstanceOf('Sabre\VObject\Component\VEvent', $event); + $vcal->add($event); + + $prop = $vcal->createProperty('X-PROP', '1234256', ['X-PARAM' => '3']); + + $event->add($prop); + + $this->assertEquals($event, $prop->parent); + + $vcal->destroy(); + + $this->assertNull($prop->parent); + + + } + +} + + +class MockDocument extends Document { + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/ElementListTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ElementListTest.php new file mode 100644 index 00000000000..e63231133ad --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ElementListTest.php @@ -0,0 +1,33 @@ +<?php + +namespace Sabre\VObject; + +class ElementListTest extends \PHPUnit_Framework_TestCase { + + function testIterate() { + + $cal = new Component\VCalendar(); + $sub = $cal->createComponent('VEVENT'); + + $elems = [ + $sub, + clone $sub, + clone $sub + ]; + + $elemList = new ElementList($elems); + + $count = 0; + foreach ($elemList as $key => $subcomponent) { + + $count++; + $this->assertInstanceOf('Sabre\\VObject\\Component', $subcomponent); + + } + $this->assertEquals(3, $count); + $this->assertEquals(2, $key); + + } + + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/EmClientTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/EmClientTest.php new file mode 100644 index 00000000000..5743d48d815 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/EmClientTest.php @@ -0,0 +1,56 @@ +<?php + +namespace Sabre\VObject; + +use DateTimeImmutable; + +class EmClientTest extends \PHPUnit_Framework_TestCase { + + function testParseTz() { + + $str = 'BEGIN:VCALENDAR +X-WR-CALNAME:Blackhawks Schedule 2011-12 +X-APPLE-CALENDAR-COLOR:#E51717 +X-WR-TIMEZONE:America/Chicago +CALSCALE:GREGORIAN +PRODID:-//eM Client/4.0.13961.0 +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:America/Chicago +BEGIN:DAYLIGHT +TZOFFSETFROM:-0600 +RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 +DTSTART:20070311T020000 +TZNAME:CDT +TZOFFSETTO:-0500 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:-0500 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 +DTSTART:20071104T020000 +TZNAME:CST +TZOFFSETTO:-0600 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20110624T181236Z +UID:be3bbfff-96e8-4c66-9908-ab791a62231d +DTEND;TZID="America/Chicago":20111008T223000 +TRANSP:OPAQUE +SUMMARY:Stars @ Blackhawks (Home Opener) +DTSTART;TZID="America/Chicago":20111008T193000 +DTSTAMP:20120330T013232Z +SEQUENCE:2 +X-MICROSOFT-CDO-BUSYSTATUS:BUSY +LAST-MODIFIED:20120330T013237Z +CLASS:PUBLIC +END:VEVENT +END:VCALENDAR'; + + $vObject = Reader::read($str); + $dt = $vObject->VEVENT->DTSTART->getDateTime(); + $this->assertEquals(new DateTimeImmutable('2011-10-08 19:30:00', new \DateTimeZone('America/Chicago')), $dt); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/EmptyParameterTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/EmptyParameterTest.php new file mode 100644 index 00000000000..a9e9fcc5cd9 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/EmptyParameterTest.php @@ -0,0 +1,69 @@ +<?php + +namespace Sabre\VObject; + +class EmptyParameterTest extends \PHPUnit_Framework_TestCase { + + function testRead() { + + $input = <<<VCF +BEGIN:VCARD +VERSION:2.1 +N:Doe;Jon;;; +FN:Jon Doe +EMAIL;X-INTERN:foo@example.org +UID:foo +END:VCARD +VCF; + + $vcard = Reader::read($input); + + $this->assertInstanceOf('Sabre\\VObject\\Component\\VCard', $vcard); + $vcard = $vcard->convert(\Sabre\VObject\Document::VCARD30); + $vcard = $vcard->serialize(); + + $converted = Reader::read($vcard); + $converted->validate(); + + $this->assertTrue(isset($converted->EMAIL['X-INTERN'])); + + $version = Version::VERSION; + + $expected = <<<VCF +BEGIN:VCARD +VERSION:3.0 +PRODID:-//Sabre//Sabre VObject $version//EN +N:Doe;Jon;;; +FN:Jon Doe +EMAIL;X-INTERN=:foo@example.org +UID:foo +END:VCARD + +VCF; + + $this->assertEquals($expected, str_replace("\r", "", $vcard)); + + } + + function testVCard21Parameter() { + + $vcard = new Component\VCard([], false); + $vcard->VERSION = '2.1'; + $vcard->PHOTO = 'random_stuff'; + $vcard->PHOTO->add(null, 'BASE64'); + $vcard->UID = 'foo-bar'; + + $result = $vcard->serialize(); + $expected = [ + "BEGIN:VCARD", + "VERSION:2.1", + "PHOTO;BASE64:" . base64_encode('random_stuff'), + "UID:foo-bar", + "END:VCARD", + "", + ]; + + $this->assertEquals(implode("\r\n", $expected), $result); + + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/EmptyValueIssueTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/EmptyValueIssueTest.php new file mode 100644 index 00000000000..7a34944992c --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/EmptyValueIssueTest.php @@ -0,0 +1,30 @@ +<?php + +namespace Sabre\VObject; + +/** + * This test is written for Issue 68: + * + * https://github.com/fruux/sabre-vobject/issues/68 + */ +class EmptyValueIssueTest extends \PHPUnit_Framework_TestCase { + + function testDecodeValue() { + + $input = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +DESCRIPTION:This is a descpription\\nwith a linebreak and a \\; \\, and : +END:VEVENT +END:VCALENDAR +ICS; + + $vobj = Reader::read($input); + + // Before this bug was fixed, getValue() would return nothing. + $this->assertEquals("This is a descpription\nwith a linebreak and a ; , and :", $vobj->VEVENT->DESCRIPTION->getValue()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/FreeBusyDataTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/FreeBusyDataTest.php new file mode 100644 index 00000000000..9b5f541b9f6 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/FreeBusyDataTest.php @@ -0,0 +1,318 @@ +<?php + +namespace Sabre\VObject; + +class FreeBusyDataTest extends \PHPUnit_Framework_TestCase { + + function testGetData() { + + $fb = new FreeBusyData(100, 200); + + $this->assertEquals( + [ + [ + 'start' => 100, + 'end' => 200, + 'type' => 'FREE', + ] + ], + $fb->getData() + ); + + } + + /** + * @depends testGetData + */ + function testAddBeginning() { + + $fb = new FreeBusyData(100, 200); + + // Overwriting the first half + $fb->add(100, 150, 'BUSY'); + + + $this->assertEquals( + [ + [ + 'start' => 100, + 'end' => 150, + 'type' => 'BUSY', + ], + [ + 'start' => 150, + 'end' => 200, + 'type' => 'FREE', + ] + ], + $fb->getData() + ); + + // Overwriting the first half again + $fb->add(100, 150, 'BUSY-TENTATIVE'); + + $this->assertEquals( + [ + [ + 'start' => 100, + 'end' => 150, + 'type' => 'BUSY-TENTATIVE', + ], + [ + 'start' => 150, + 'end' => 200, + 'type' => 'FREE', + ] + ], + $fb->getData() + ); + + } + + /** + * @depends testAddBeginning + */ + function testAddEnd() { + + $fb = new FreeBusyData(100, 200); + + // Overwriting the first half + $fb->add(150, 200, 'BUSY'); + + + $this->assertEquals( + [ + [ + 'start' => 100, + 'end' => 150, + 'type' => 'FREE', + ], + [ + 'start' => 150, + 'end' => 200, + 'type' => 'BUSY', + ], + ], + $fb->getData() + ); + + + } + + /** + * @depends testAddEnd + */ + function testAddMiddle() { + + $fb = new FreeBusyData(100, 200); + + // Overwriting the first half + $fb->add(150, 160, 'BUSY'); + + + $this->assertEquals( + [ + [ + 'start' => 100, + 'end' => 150, + 'type' => 'FREE', + ], + [ + 'start' => 150, + 'end' => 160, + 'type' => 'BUSY', + ], + [ + 'start' => 160, + 'end' => 200, + 'type' => 'FREE', + ], + ], + $fb->getData() + ); + + } + + /** + * @depends testAddMiddle + */ + function testAddMultiple() { + + $fb = new FreeBusyData(100, 200); + + $fb->add(110, 120, 'BUSY'); + $fb->add(130, 140, 'BUSY'); + + $this->assertEquals( + [ + [ + 'start' => 100, + 'end' => 110, + 'type' => 'FREE', + ], + [ + 'start' => 110, + 'end' => 120, + 'type' => 'BUSY', + ], + [ + 'start' => 120, + 'end' => 130, + 'type' => 'FREE', + ], + [ + 'start' => 130, + 'end' => 140, + 'type' => 'BUSY', + ], + [ + 'start' => 140, + 'end' => 200, + 'type' => 'FREE', + ], + ], + $fb->getData() + ); + + } + + /** + * @depends testAddMultiple + */ + function testAddMultipleOverlap() { + + $fb = new FreeBusyData(100, 200); + + $fb->add(110, 120, 'BUSY'); + $fb->add(130, 140, 'BUSY'); + + $this->assertEquals( + [ + [ + 'start' => 100, + 'end' => 110, + 'type' => 'FREE', + ], + [ + 'start' => 110, + 'end' => 120, + 'type' => 'BUSY', + ], + [ + 'start' => 120, + 'end' => 130, + 'type' => 'FREE', + ], + [ + 'start' => 130, + 'end' => 140, + 'type' => 'BUSY', + ], + [ + 'start' => 140, + 'end' => 200, + 'type' => 'FREE', + ], + ], + $fb->getData() + ); + + $fb->add(115, 135, 'BUSY-TENTATIVE'); + + $this->assertEquals( + [ + [ + 'start' => 100, + 'end' => 110, + 'type' => 'FREE', + ], + [ + 'start' => 110, + 'end' => 115, + 'type' => 'BUSY', + ], + [ + 'start' => 115, + 'end' => 135, + 'type' => 'BUSY-TENTATIVE', + ], + [ + 'start' => 135, + 'end' => 140, + 'type' => 'BUSY', + ], + [ + 'start' => 140, + 'end' => 200, + 'type' => 'FREE', + ], + ], + $fb->getData() + ); + } + + /** + * @depends testAddMultipleOverlap + */ + function testAddMultipleOverlapAndMerge() { + + $fb = new FreeBusyData(100, 200); + + $fb->add(110, 120, 'BUSY'); + $fb->add(130, 140, 'BUSY'); + + $this->assertEquals( + [ + [ + 'start' => 100, + 'end' => 110, + 'type' => 'FREE', + ], + [ + 'start' => 110, + 'end' => 120, + 'type' => 'BUSY', + ], + [ + 'start' => 120, + 'end' => 130, + 'type' => 'FREE', + ], + [ + 'start' => 130, + 'end' => 140, + 'type' => 'BUSY', + ], + [ + 'start' => 140, + 'end' => 200, + 'type' => 'FREE', + ], + ], + $fb->getData() + ); + + $fb->add(115, 135, 'BUSY'); + + $this->assertEquals( + [ + [ + 'start' => 100, + 'end' => 110, + 'type' => 'FREE', + ], + [ + 'start' => 110, + 'end' => 140, + 'type' => 'BUSY', + ], + [ + 'start' => 140, + 'end' => 200, + 'type' => 'FREE', + ], + ], + $fb->getData() + ); + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/FreeBusyGeneratorTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/FreeBusyGeneratorTest.php new file mode 100644 index 00000000000..70e83ab2f30 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/FreeBusyGeneratorTest.php @@ -0,0 +1,751 @@ +<?php + +namespace Sabre\VObject; + +class FreeBusyGeneratorTest extends \PHPUnit_Framework_TestCase { + + use PHPUnitAssertions; + + function testGeneratorBaseObject() { + + $obj = new Component\VCalendar(); + $obj->METHOD = 'PUBLISH'; + + $gen = new FreeBusyGenerator(); + $gen->setObjects([]); + $gen->setBaseObject($obj); + + $result = $gen->getResult(); + + $this->assertEquals('PUBLISH', $result->METHOD->getValue()); + + } + + /** + * @expectedException InvalidArgumentException + */ + function testInvalidArg() { + + $gen = new FreeBusyGenerator( + new \DateTime('2012-01-01'), + new \DateTime('2012-12-31'), + new \StdClass() + ); + + } + + /** + * This function takes a list of objects (icalendar objects), and turns + * them into a freebusy report. + * + * Then it takes the expected output and compares it to what we actually + * got. + * + * It only generates the freebusy report for the following time-range: + * 2011-01-01 11:00:00 until 2011-01-03 11:11:11 + * + * @param string $expected + * @param array $input + * @param string|null $timeZone + * @param string $vavailability + * @return void + */ + function assertFreeBusyReport($expected, $input, $timeZone = null, $vavailability = null) { + + $gen = new FreeBusyGenerator( + new \DateTime('20110101T110000Z', new \DateTimeZone('UTC')), + new \DateTime('20110103T110000Z', new \DateTimeZone('UTC')), + $input, + $timeZone + ); + + if ($vavailability) { + if (is_string($vavailability)) { + $vavailability = Reader::read($vavailability); + } + $gen->setVAvailability($vavailability); + } + + $output = $gen->getResult(); + + // Removing DTSTAMP because it changes every time. + unset($output->VFREEBUSY->DTSTAMP); + + $expected = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VFREEBUSY +DTSTART:20110101T110000Z +DTEND:20110103T110000Z +$expected +END:VFREEBUSY +END:VCALENDAR +ICS; + + $this->assertVObjectEqualsVObject($expected, $output); + + } + + function testSimple() { + + $blob = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:foobar +DTSTART:20110101T120000Z +DTEND:20110101T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + + $this->assertFreeBusyReport( + "FREEBUSY:20110101T120000Z/20110101T130000Z", + $blob + ); + + } + + function testSource() { + + $blob = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:foobar +DTSTART:20110101T120000Z +DTEND:20110101T130000Z +END:VEVENT +END:VCALENDAR +ICS; + $h = fopen('php://memory', 'r+'); + fwrite($h, $blob); + rewind($h); + + + $this->assertFreeBusyReport( + "FREEBUSY:20110101T120000Z/20110101T130000Z", + $h + ); + + } + + /** + * Testing TRANSP:OPAQUE + */ + function testOpaque() { + + $blob = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:foobar2 +TRANSP:OPAQUE +DTSTART:20110101T130000Z +DTEND:20110101T140000Z +END:VEVENT +END:VCALENDAR +ICS; + + $this->assertFreeBusyReport( + "FREEBUSY:20110101T130000Z/20110101T140000Z", + $blob + ); + + } + + /** + * Testing TRANSP:TRANSPARENT + */ + function testTransparent() { + + // transparent, hidden + $blob = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:foobar3 +TRANSP:TRANSPARENT +DTSTART:20110101T140000Z +DTEND:20110101T150000Z +END:VEVENT +END:VCALENDAR +ICS; + + $this->assertFreeBusyReport( + "", + $blob + ); + + } + + /** + * Testing STATUS:CANCELLED + */ + function testCancelled() { + + // transparent, hidden + $blob = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:foobar4 +STATUS:CANCELLED +DTSTART:20110101T160000Z +DTEND:20110101T170000Z +END:VEVENT +END:VCALENDAR +ICS; + + $this->assertFreeBusyReport( + "", + $blob + ); + + } + + /** + * Testing STATUS:TENTATIVE + */ + function testTentative() { + + // tentative, shows up + $blob = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:foobar5 +STATUS:TENTATIVE +DTSTART:20110101T180000Z +DTEND:20110101T190000Z +END:VEVENT +END:VCALENDAR +ICS; + + $this->assertFreeBusyReport( + 'FREEBUSY;FBTYPE=BUSY-TENTATIVE:20110101T180000Z/20110101T190000Z', + $blob + ); + + } + + /** + * Testing an event that falls outside of the report time-range. + */ + function testOutsideTimeRange() { + + // outside of time-range, hidden + $blob = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:foobar6 +DTSTART:20110101T090000Z +DTEND:20110101T100000Z +END:VEVENT +END:VCALENDAR +ICS; + + $this->assertFreeBusyReport( + '', + $blob + ); + + } + + /** + * Testing an event that falls outside of the report time-range. + */ + function testOutsideTimeRange2() { + + // outside of time-range, hidden + $blob = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:foobar7 +DTSTART:20110104T090000Z +DTEND:20110104T100000Z +END:VEVENT +END:VCALENDAR +ICS; + + $this->assertFreeBusyReport( + '', + $blob + ); + + } + + /** + * Testing an event that uses DURATION + */ + function testDuration() { + + // using duration, shows up + $blob = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:foobar8 +DTSTART:20110101T190000Z +DURATION:PT1H +END:VEVENT +END:VCALENDAR +ICS; + + $this->assertFreeBusyReport( + 'FREEBUSY:20110101T190000Z/20110101T200000Z', + $blob + ); + + } + + /** + * Testing an all-day event + */ + function testAllDay() { + + // Day-long event, shows up + $blob = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:foobar9 +DTSTART;VALUE=DATE:20110102 +END:VEVENT +END:VCALENDAR +ICS; + + $this->assertFreeBusyReport( + 'FREEBUSY:20110102T000000Z/20110103T000000Z', + $blob + ); + + } + + /** + * Testing an event that has no end or duration. + */ + function testNoDuration() { + + // No duration, does not show up + $blob = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:foobar10 +DTSTART:20110101T200000Z +END:VEVENT +END:VCALENDAR +ICS; + + $this->assertFreeBusyReport( + '', + $blob + ); + + } + + /** + * Testing feeding the freebusy generator an object instead of a string. + */ + function testObject() { + + // encoded as object, shows up + $blob = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:foobar11 +DTSTART:20110101T210000Z +DURATION:PT1H +END:VEVENT +END:VCALENDAR +ICS; + + $this->assertFreeBusyReport( + 'FREEBUSY:20110101T210000Z/20110101T220000Z', + Reader::read($blob) + ); + + + } + + /** + * Testing feeding VFREEBUSY objects instead of VEVENT + */ + function testVFreeBusy() { + + // Freebusy. Some parts show up + $blob = <<<ICS +BEGIN:VCALENDAR +BEGIN:VFREEBUSY +FREEBUSY:20110103T010000Z/20110103T020000Z +FREEBUSY;FBTYPE=FREE:20110103T020000Z/20110103T030000Z +FREEBUSY:20110103T030000Z/20110103T040000Z,20110103T040000Z/20110103T050000Z +FREEBUSY:20120101T000000Z/20120101T010000Z +FREEBUSY:20110103T050000Z/PT1H +END:VFREEBUSY +END:VCALENDAR +ICS; + + $this->assertFreeBusyReport( + "FREEBUSY:20110103T010000Z/20110103T020000Z\n" . + 'FREEBUSY:20110103T030000Z/20110103T060000Z', + $blob + ); + + } + + function testYearlyRecurrence() { + + // Yearly recurrence rule, shows up + $blob = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:foobar13 +DTSTART:20100101T220000Z +DTEND:20100101T230000Z +RRULE:FREQ=YEARLY +END:VEVENT +END:VCALENDAR +ICS; + + $this->assertFreeBusyReport( + 'FREEBUSY:20110101T220000Z/20110101T230000Z', + $blob + ); + + } + + function testYearlyRecurrenceDuration() { + + // Yearly recurrence rule + duration, shows up + $blob = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:foobar14 +DTSTART:20100101T230000Z +DURATION:PT1H +RRULE:FREQ=YEARLY +END:VEVENT +END:VCALENDAR +ICS; + + $this->assertFreeBusyReport( + 'FREEBUSY:20110101T230000Z/20110102T000000Z', + $blob + ); + + } + + function testFloatingTime() { + + // Floating time, no timezone + $blob = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:foobar +DTSTART:20110101T120000 +DTEND:20110101T130000 +END:VEVENT +END:VCALENDAR +ICS; + + $this->assertFreeBusyReport( + "FREEBUSY:20110101T120000Z/20110101T130000Z", + $blob + ); + + } + + function testFloatingTimeReferenceTimeZone() { + + // Floating time + reference timezone + $blob = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:foobar +DTSTART:20110101T120000 +DTEND:20110101T130000 +END:VEVENT +END:VCALENDAR +ICS; + + $this->assertFreeBusyReport( + "FREEBUSY:20110101T170000Z/20110101T180000Z", + $blob, + new \DateTimeZone('America/Toronto') + ); + + } + + function testAllDay2() { + + // All-day event, slightly outside of the VFREEBUSY range. + $blob = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:foobar +DTSTART;VALUE=DATE:20110101 +END:VEVENT +END:VCALENDAR +ICS; + + $this->assertFreeBusyReport( + "FREEBUSY:20110101T110000Z/20110102T000000Z", + $blob + ); + + } + + function testAllDayReferenceTimeZone() { + + // All-day event + reference timezone + $blob = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:foobar +DTSTART;VALUE=DATE:20110101 +END:VEVENT +END:VCALENDAR +ICS; + + $this->assertFreeBusyReport( + "FREEBUSY:20110101T110000Z/20110102T050000Z", + $blob, + new \DateTimeZone('America/Toronto') + ); + + } + + function testNoValidInstances() { + + // Recurrence rule with no valid instances + $blob = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:foobar +DTSTART:20110101T100000Z +DTEND:20110103T120000Z +RRULE:FREQ=WEEKLY;COUNT=1 +EXDATE:20110101T100000Z +END:VEVENT +END:VCALENDAR +ICS; + + $this->assertFreeBusyReport( + "", + $blob + ); + + } + + /** + * This VAVAILABILITY object overlaps with the time-range, but we're just + * busy the entire time. + */ + function testVAvailabilitySimple() { + + $blob = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:lalala +DTSTART:20110101T120000Z +DTEND:20110101T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + $vavail = <<<ICS +BEGIN:VCALENDAR +BEGIN:VAVAILABILITY +DTSTART:20110101T000000Z +DTEND:20120101T000000Z +BEGIN:AVAILABLE +DTSTART:20110101T000000Z +DTEND:20110101T010000Z +END:AVAILABLE +END:VAVAILABILITY +END:VCALENDAR +ICS; + + $this->assertFreeBusyReport( + "FREEBUSY;FBTYPE=BUSY-UNAVAILABLE:20110101T110000Z/20110101T120000Z\n" . + "FREEBUSY:20110101T120000Z/20110101T130000Z\n" . + "FREEBUSY;FBTYPE=BUSY-UNAVAILABLE:20110101T130000Z/20110103T110000Z", + $blob, + null, + $vavail + ); + + } + + /** + * This VAVAILABILITY object does not overlap at all with the freebusy + * report, so it should be ignored. + */ + function testVAvailabilityIrrelevant() { + + $blob = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:lalala +DTSTART:20110101T120000Z +DTEND:20110101T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + $vavail = <<<ICS +BEGIN:VCALENDAR +BEGIN:VAVAILABILITY +DTSTART:20150101T000000Z +DTEND:20160101T000000Z +BEGIN:AVAILABLE +DTSTART:20150101T000000Z +DTEND:20150101T010000Z +END:AVAILABLE +END:VAVAILABILITY +END:VCALENDAR +ICS; + + $this->assertFreeBusyReport( + "FREEBUSY:20110101T120000Z/20110101T130000Z", + $blob, + null, + $vavail + ); + + } + + /** + * This VAVAILABILITY object has a 9am-5pm AVAILABLE object for office + * hours. + */ + function testVAvailabilityOfficeHours() { + + $blob = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:lalala +DTSTART:20110101T120000Z +DTEND:20110101T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + $vavail = <<<ICS +BEGIN:VCALENDAR +BEGIN:VAVAILABILITY +DTSTART:20100101T000000Z +DTEND:20120101T000000Z +BUSYTYPE:BUSY-TENTATIVE +BEGIN:AVAILABLE +DTSTART:20101213T090000Z +DTEND:20101213T170000Z +RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR +END:AVAILABLE +END:VAVAILABILITY +END:VCALENDAR +ICS; + + $this->assertFreeBusyReport( + "FREEBUSY;FBTYPE=BUSY-TENTATIVE:20110101T110000Z/20110101T120000Z\n" . + "FREEBUSY:20110101T120000Z/20110101T130000Z\n" . + "FREEBUSY;FBTYPE=BUSY-TENTATIVE:20110101T130000Z/20110103T090000Z\n", + $blob, + null, + $vavail + ); + + } + + /** + * This test has the same office hours, but has a vacation blocked off for + * the relevant time, using a higher priority. (lower number). + */ + function testVAvailabilityOfficeHoursVacation() { + + $blob = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:lalala +DTSTART:20110101T120000Z +DTEND:20110101T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + $vavail = <<<ICS +BEGIN:VCALENDAR +BEGIN:VAVAILABILITY +DTSTART:20100101T000000Z +DTEND:20120101T000000Z +BUSYTYPE:BUSY-TENTATIVE +PRIORITY:2 +BEGIN:AVAILABLE +DTSTART:20101213T090000Z +DTEND:20101213T170000Z +RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR +END:AVAILABLE +END:VAVAILABILITY +BEGIN:VAVAILABILITY +PRIORITY:1 +DTSTART:20101214T000000Z +DTEND:20110107T000000Z +BUSYTYPE:BUSY +END:VAVAILABILITY +END:VCALENDAR +ICS; + + $this->assertFreeBusyReport( + "FREEBUSY:20110101T110000Z/20110103T110000Z", + $blob, + null, + $vavail + ); + + } + + /** + * This test has the same input as the last, except somebody mixed up the + * PRIORITY values. + * + * The end-result is that the vacation VAVAILABILITY is completely ignored. + */ + function testVAvailabilityOfficeHoursVacation2() { + + $blob = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:lalala +DTSTART:20110101T120000Z +DTEND:20110101T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + $vavail = <<<ICS +BEGIN:VCALENDAR +BEGIN:VAVAILABILITY +DTSTART:20100101T000000Z +DTEND:20120101T000000Z +BUSYTYPE:BUSY-TENTATIVE +PRIORITY:1 +BEGIN:AVAILABLE +DTSTART:20101213T090000Z +DTEND:20101213T170000Z +RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR +END:AVAILABLE +END:VAVAILABILITY +BEGIN:VAVAILABILITY +PRIORITY:2 +DTSTART:20101214T000000Z +DTEND:20110107T000000Z +BUSYTYPE:BUSY +END:VAVAILABILITY +END:VCALENDAR +ICS; + + $this->assertFreeBusyReport( + "FREEBUSY;FBTYPE=BUSY-TENTATIVE:20110101T110000Z/20110101T120000Z\n" . + "FREEBUSY:20110101T120000Z/20110101T130000Z\n" . + "FREEBUSY;FBTYPE=BUSY-TENTATIVE:20110101T130000Z/20110103T090000Z\n", + $blob, + null, + $vavail + ); + + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/GoogleColonEscapingTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/GoogleColonEscapingTest.php new file mode 100644 index 00000000000..ee37aa8875e --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/GoogleColonEscapingTest.php @@ -0,0 +1,31 @@ +<?php + +namespace Sabre\VObject; + +/** + * Google produces vcards with a weird escaping of urls. + * + * VObject will provide a workaround for this, so end-user still get expected + * values. + */ +class GoogleColonEscapingTest extends \PHPUnit_Framework_TestCase { + + function testDecode() { + + $vcard = <<<VCF +BEGIN:VCARD +VERSION:3.0 +FN:Evert Pot +N:Pot;Evert;;; +EMAIL;TYPE=INTERNET;TYPE=WORK:evert@fruux.com +BDAY:1985-04-07 +item7.URL:http\://www.rooftopsolutions.nl/ +END:VCARD +VCF; + + $vobj = Reader::read($vcard); + $this->assertEquals('http://www.rooftopsolutions.nl/', $vobj->URL->getValue()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/ICalendar/AttachParseTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ICalendar/AttachParseTest.php new file mode 100644 index 00000000000..0c4fc8790e5 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ICalendar/AttachParseTest.php @@ -0,0 +1,31 @@ +<?php + +namespace Sabre\VObject\ICalendar; + +use Sabre\VObject\Reader; + +class AttachParseTest extends \PHPUnit_Framework_TestCase { + + /** + * See issue #128 for more info. + */ + function testParseAttach() { + + $vcal = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +ATTACH;FMTTYPE=application/postscript:ftp://example.com/pub/reports/r-960812.ps +END:VEVENT +END:VCALENDAR +ICS; + + $vcal = Reader::read($vcal); + $prop = $vcal->VEVENT->ATTACH; + + $this->assertInstanceOf('Sabre\\VObject\\Property\\URI', $prop); + $this->assertEquals('ftp://example.com/pub/reports/r-960812.ps', $prop->getValue()); + + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerAttendeeReplyTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerAttendeeReplyTest.php new file mode 100644 index 00000000000..9519ed36828 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerAttendeeReplyTest.php @@ -0,0 +1,1146 @@ +<?php + +namespace Sabre\VObject\ITip; + +class BrokerAttendeeReplyTest extends BrokerTester { + + function testAccepted() { + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SUMMARY:B-day party +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +DTSTART:20140716T120000Z +END:VEVENT +END:VCALENDAR +ICS; + + + $newMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SUMMARY:B-day party +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=ACCEPTED;CN=One:mailto:one@example.org +DTSTART:20140716T120000Z +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + + $expected = [ + [ + 'uid' => 'foobar', + 'method' => 'REPLY', + 'component' => 'VEVENT', + 'sender' => 'mailto:one@example.org', + 'senderName' => 'One', + 'recipient' => 'mailto:strunk@example.org', + 'recipientName' => 'Strunk', + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:REPLY +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:1 +DTSTART:20140716T120000Z +SUMMARY:B-day party +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=ACCEPTED;CN=One:mailto:one@example.org +END:VEVENT +END:VCALENDAR +ICS + + ], + + ]; + + $this->parse($oldMessage, $newMessage, $expected); + + } + + function testRecurringReply() { + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +DTSTART:20140724T120000Z +SUMMARY:Daily sprint +RRULE;FREQ=DAILY +END:VEVENT +END:VCALENDAR +ICS; + + + $newMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=NEEDS-ACTION;CN=One:mailto:one@example.org +DTSTART:20140724T120000Z +SUMMARY:Daily sprint +END:VEVENT +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=ACCEPTED;CN=One:mailto:one@example.org +DTSTART:20140726T120000Z +RECURRENCE-ID:20140726T120000Z +END:VEVENT +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=DECLINED;CN=One:mailto:one@example.org +DTSTART:20140724T120000Z +RECURRENCE-ID:20140724T120000Z +END:VEVENT +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=TENTATIVE;CN=One:mailto:one@example.org +DTSTART:20140728T120000Z +RECURRENCE-ID:20140728T120000Z +END:VEVENT +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=ACCEPTED;CN=One:mailto:one@example.org +DTSTART:20140729T120000Z +RECURRENCE-ID:20140729T120000Z +END:VEVENT +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=DECLINED;CN=One:mailto:one@example.org +DTSTART:20140725T120000Z +RECURRENCE-ID:20140725T120000Z +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + + $expected = [ + [ + 'uid' => 'foobar', + 'method' => 'REPLY', + 'component' => 'VEVENT', + 'sender' => 'mailto:one@example.org', + 'senderName' => 'One', + 'recipient' => 'mailto:strunk@example.org', + 'recipientName' => 'Strunk', + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:REPLY +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:1 +DTSTART:20140726T120000Z +SUMMARY:Daily sprint +RECURRENCE-ID:20140726T120000Z +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=ACCEPTED;CN=One:mailto:one@example.org +END:VEVENT +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:1 +DTSTART:20140724T120000Z +SUMMARY:Daily sprint +RECURRENCE-ID:20140724T120000Z +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=DECLINED;CN=One:mailto:one@example.org +END:VEVENT +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:1 +DTSTART:20140728T120000Z +SUMMARY:Daily sprint +RECURRENCE-ID:20140728T120000Z +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=TENTATIVE;CN=One:mailto:one@example.org +END:VEVENT +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:1 +DTSTART:20140729T120000Z +SUMMARY:Daily sprint +RECURRENCE-ID:20140729T120000Z +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=ACCEPTED;CN=One:mailto:one@example.org +END:VEVENT +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:1 +DTSTART:20140725T120000Z +SUMMARY:Daily sprint +RECURRENCE-ID:20140725T120000Z +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=DECLINED;CN=One:mailto:one@example.org +END:VEVENT +END:VCALENDAR +ICS + + ], + + ]; + + $this->parse($oldMessage, $newMessage, $expected); + + } + + function testRecurringAllDay() { + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +DTSTART;VALUE=DATE:20140724 +RRULE;FREQ=DAILY +END:VEVENT +END:VCALENDAR +ICS; + + + $newMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=NEEDS-ACTION;CN=One:mailto:one@example.org +DTSTART;VALUE=DATE:20140724 +END:VEVENT +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=ACCEPTED;CN=One:mailto:one@example.org +DTSTART;VALUE=DATE:20140726 +RECURRENCE-ID;VALUE=DATE:20140726 +END:VEVENT +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=DECLINED;CN=One:mailto:one@example.org +DTSTART;VALUE=DATE:20140724 +RECURRENCE-ID;VALUE=DATE:20140724 +END:VEVENT +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=TENTATIVE;CN=One:mailto:one@example.org +DTSTART;VALUE=DATE:20140728 +RECURRENCE-ID;VALUE=DATE:20140728 +END:VEVENT +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=ACCEPTED;CN=One:mailto:one@example.org +DTSTART;VALUE=DATE:20140729 +RECURRENCE-ID;VALUE=DATE:20140729 +END:VEVENT +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=DECLINED;CN=One:mailto:one@example.org +DTSTART;VALUE=DATE:20140725 +RECURRENCE-ID;VALUE=DATE:20140725 +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + + $expected = [ + [ + 'uid' => 'foobar', + 'method' => 'REPLY', + 'component' => 'VEVENT', + 'sender' => 'mailto:one@example.org', + 'senderName' => 'One', + 'recipient' => 'mailto:strunk@example.org', + 'recipientName' => 'Strunk', + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:REPLY +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:1 +DTSTART;VALUE=DATE:20140726 +RECURRENCE-ID;VALUE=DATE:20140726 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=ACCEPTED;CN=One:mailto:one@example.org +END:VEVENT +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:1 +DTSTART;VALUE=DATE:20140724 +RECURRENCE-ID;VALUE=DATE:20140724 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=DECLINED;CN=One:mailto:one@example.org +END:VEVENT +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:1 +DTSTART;VALUE=DATE:20140728 +RECURRENCE-ID;VALUE=DATE:20140728 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=TENTATIVE;CN=One:mailto:one@example.org +END:VEVENT +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:1 +DTSTART;VALUE=DATE:20140729 +RECURRENCE-ID;VALUE=DATE:20140729 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=ACCEPTED;CN=One:mailto:one@example.org +END:VEVENT +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:1 +DTSTART;VALUE=DATE:20140725 +RECURRENCE-ID;VALUE=DATE:20140725 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=DECLINED;CN=One:mailto:one@example.org +END:VEVENT +END:VCALENDAR +ICS + + ], + + ]; + + $this->parse($oldMessage, $newMessage, $expected); + + } + + function testNoChange() { + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +DTSTART:20140716T120000Z +END:VEVENT +END:VCALENDAR +ICS; + + + $newMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=NEEDS-ACTION;CN=One:mailto:one@example.org +DTSTART:20140716T120000Z +END:VEVENT +END:VCALENDAR +ICS; + + + $expected = []; + $this->parse($oldMessage, $newMessage, $expected); + + } + + function testNoChangeForceSend() { + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +DTSTART:20140716T120000Z +END:VEVENT +END:VCALENDAR +ICS; + + + $newMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;SCHEDULE-FORCE-SEND=REPLY;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=NEEDS-ACTION;CN=One:mailto:one@example.org +DTSTART:20140716T120000Z +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + $expected = [ + [ + 'uid' => 'foobar', + 'method' => 'REPLY', + 'component' => 'VEVENT', + 'sender' => 'mailto:one@example.org', + 'senderName' => 'One', + 'recipient' => 'mailto:strunk@example.org', + 'recipientName' => 'Strunk', + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:REPLY +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:1 +DTSTART:20140716T120000Z +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=NEEDS-ACTION;CN=One:mailto:one@example.org +END:VEVENT +END:VCALENDAR +ICS + ] + + ]; + $this->parse($oldMessage, $newMessage, $expected); + + } + + function testNoRelevantAttendee() { + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Two:mailto:two@example.org +DTSTART:20140716T120000Z +END:VEVENT +END:VCALENDAR +ICS; + + + $newMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=ACCEPTED;CN=Two:mailto:two@example.org +DTSTART:20140716T120000Z +END:VEVENT +END:VCALENDAR +ICS; + + $expected = []; + $this->parse($oldMessage, $newMessage, $expected); + + } + + /** + * In this test, an event exists in an attendees calendar. The event + * is recurring, and the attendee deletes 1 instance of the event. + * This instance shows up in EXDATE + * + * This should automatically generate a DECLINED message for that + * specific instance. + */ + function testCreateReplyByException() { + + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +DTSTART:20140811T200000Z +RRULE:FREQ=WEEKLY +ORGANIZER:mailto:organizer@example.org +ATTENDEE:mailto:one@example.org +END:VEVENT +END:VCALENDAR +ICS; + + $newMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +DTSTART:20140811T200000Z +RRULE:FREQ=WEEKLY +ORGANIZER:mailto:organizer@example.org +ATTENDEE:mailto:one@example.org +EXDATE:20140818T200000Z +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + $expected = [ + [ + 'uid' => 'foobar', + 'method' => 'REPLY', + 'component' => 'VEVENT', + 'sender' => 'mailto:one@example.org', + 'senderName' => null, + 'recipient' => 'mailto:organizer@example.org', + 'recipientName' => null, + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:REPLY +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:1 +DTSTART:20140818T200000Z +RECURRENCE-ID:20140818T200000Z +ORGANIZER:mailto:organizer@example.org +ATTENDEE;PARTSTAT=DECLINED:mailto:one@example.org +END:VEVENT +END:VCALENDAR +ICS + + ], + ]; + $this->parse($oldMessage, $newMessage, $expected); + + } + + /** + * This test is identical to the last, but now we're working with + * timezones. + * + * @depends testCreateReplyByException + */ + function testCreateReplyByExceptionTz() { + + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +DTSTART;TZID=America/Toronto:20140811T200000 +RRULE:FREQ=WEEKLY +ORGANIZER:mailto:organizer@example.org +ATTENDEE:mailto:one@example.org +END:VEVENT +END:VCALENDAR +ICS; + + $newMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +DTSTART;TZID=America/Toronto:20140811T200000 +RRULE:FREQ=WEEKLY +ORGANIZER:mailto:organizer@example.org +ATTENDEE:mailto:one@example.org +EXDATE;TZID=America/Toronto:20140818T200000 +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + $expected = [ + [ + 'uid' => 'foobar', + 'method' => 'REPLY', + 'component' => 'VEVENT', + 'sender' => 'mailto:one@example.org', + 'senderName' => null, + 'recipient' => 'mailto:organizer@example.org', + 'recipientName' => null, + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:REPLY +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:1 +DTSTART;TZID=America/Toronto:20140818T200000 +RECURRENCE-ID;TZID=America/Toronto:20140818T200000 +ORGANIZER:mailto:organizer@example.org +ATTENDEE;PARTSTAT=DECLINED:mailto:one@example.org +END:VEVENT +END:VCALENDAR +ICS + + ], + ]; + $this->parse($oldMessage, $newMessage, $expected); + + } + + /** + * @depends testCreateReplyByException + */ + function testCreateReplyByExceptionAllDay() { + + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +SUMMARY:Weekly meeting +UID:foobar +SEQUENCE:1 +DTSTART;VALUE=DATE:20140811 +RRULE:FREQ=WEEKLY +ORGANIZER:mailto:organizer@example.org +ATTENDEE:mailto:one@example.org +END:VEVENT +END:VCALENDAR +ICS; + + $newMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +SUMMARY:Weekly meeting +UID:foobar +SEQUENCE:1 +DTSTART;VALUE=DATE:20140811 +RRULE:FREQ=WEEKLY +ORGANIZER:mailto:organizer@example.org +ATTENDEE:mailto:one@example.org +EXDATE;VALUE=DATE:20140818 +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + $expected = [ + [ + 'uid' => 'foobar', + 'method' => 'REPLY', + 'component' => 'VEVENT', + 'sender' => 'mailto:one@example.org', + 'senderName' => null, + 'recipient' => 'mailto:organizer@example.org', + 'recipientName' => null, + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:REPLY +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:1 +DTSTART;VALUE=DATE:20140818 +SUMMARY:Weekly meeting +RECURRENCE-ID;VALUE=DATE:20140818 +ORGANIZER:mailto:organizer@example.org +ATTENDEE;PARTSTAT=DECLINED:mailto:one@example.org +END:VEVENT +END:VCALENDAR +ICS + + ], + ]; + $this->parse($oldMessage, $newMessage, $expected); + + } + + function testDeclined() { + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +DTSTART:20140716T120000Z +END:VEVENT +END:VCALENDAR +ICS; + + + $newMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=DECLINED;CN=One:mailto:one@example.org +DTSTART:20140716T120000Z +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + + $expected = [ + [ + 'uid' => 'foobar', + 'method' => 'REPLY', + 'component' => 'VEVENT', + 'sender' => 'mailto:one@example.org', + 'senderName' => 'One', + 'recipient' => 'mailto:strunk@example.org', + 'recipientName' => 'Strunk', + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:REPLY +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:1 +DTSTART:20140716T120000Z +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=DECLINED;CN=One:mailto:one@example.org +END:VEVENT +END:VCALENDAR +ICS + + ], + + ]; + + $this->parse($oldMessage, $newMessage, $expected); + + } + + function testDeclinedCancelledEvent() { + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +STATUS:CANCELLED +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +DTSTART:20140716T120000Z +END:VEVENT +END:VCALENDAR +ICS; + + + $newMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +STATUS:CANCELLED +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=DECLINED;CN=One:mailto:one@example.org +DTSTART:20140716T120000Z +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + + $expected = []; + + $this->parse($oldMessage, $newMessage, $expected); + + } + + /** + * In this test, a new exception is created by an attendee as well. + * + * Except in this case, there was already an overridden event, and the + * overridden event was marked as cancelled by the attendee. + * + * For any other attendence status, the new status would have been + * declined, but for this, no message should we sent. + */ + function testDontCreateReplyWhenEventWasDeclined() { + + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +DTSTART:20140811T200000Z +RRULE:FREQ=WEEKLY +ORGANIZER:mailto:organizer@example.org +ATTENDEE:mailto:one@example.org +END:VEVENT +BEGIN:VEVENT +RECURRENCE-ID:20140818T200000Z +UID:foobar +SEQUENCE:1 +DTSTART:20140818T200000Z +RRULE:FREQ=WEEKLY +ORGANIZER:mailto:organizer@example.org +ATTENDEE;PARTSTAT=DECLINED:mailto:one@example.org +END:VEVENT +END:VCALENDAR +ICS; + + $newMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +DTSTART:20140811T200000Z +RRULE:FREQ=WEEKLY +ORGANIZER:mailto:organizer@example.org +ATTENDEE:mailto:one@example.org +EXDATE:20140818T200000Z +END:VEVENT +END:VCALENDAR +ICS; + + $expected = []; + + $this->parse($oldMessage, $newMessage, $expected); + + } + + function testScheduleAgentOnOrganizer() { + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +DTSTART:20140716T120000Z +END:VEVENT +END:VCALENDAR +ICS; + + + $newMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;SCHEDULE-AGENT=CLIENT;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=ACCEPTED;CN=One:mailto:one@example.org +DTSTART:20140716T120000Z +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + + $expected = []; + $this->parse($oldMessage, $newMessage, $expected); + + } + + function testAcceptedAllDay() { + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +DTSTART;VALUE=DATE:20140716 +END:VEVENT +END:VCALENDAR +ICS; + + + $newMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=ACCEPTED;CN=One:mailto:one@example.org +DTSTART;VALUE=DATE:20140716 +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + + $expected = [ + [ + 'uid' => 'foobar', + 'method' => 'REPLY', + 'component' => 'VEVENT', + 'sender' => 'mailto:one@example.org', + 'senderName' => 'One', + 'recipient' => 'mailto:strunk@example.org', + 'recipientName' => 'Strunk', + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:REPLY +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:1 +DTSTART;VALUE=DATE:20140716 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=ACCEPTED;CN=One:mailto:one@example.org +END:VEVENT +END:VCALENDAR +ICS + + ], + + ]; + + $this->parse($oldMessage, $newMessage, $expected); + + } + + /** + * This function tests an attendee updating their status to an event where + * they don't have the master event of. + * + * This is possible in cases an organizer created a recurring event, and + * invited an attendee for one instance of the event. + */ + function testReplyNoMasterEvent() { + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +RECURRENCE-ID:20140724T120000Z +DTSTART:20140724T120000Z +SUMMARY:Daily sprint +END:VEVENT +END:VCALENDAR +ICS; + + + $newMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=ACCEPTED;CN=One:mailto:one@example.org +RECURRENCE-ID:20140724T120000Z +DTSTART:20140724T120000Z +SUMMARY:Daily sprint +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + + $expected = [ + [ + 'uid' => 'foobar', + 'method' => 'REPLY', + 'component' => 'VEVENT', + 'sender' => 'mailto:one@example.org', + 'senderName' => 'One', + 'recipient' => 'mailto:strunk@example.org', + 'recipientName' => 'Strunk', + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:REPLY +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:1 +DTSTART:20140724T120000Z +SUMMARY:Daily sprint +RECURRENCE-ID:20140724T120000Z +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=ACCEPTED;CN=One:mailto:one@example.org +END:VEVENT +END:VCALENDAR +ICS + + ], + + ]; + + $this->parse($oldMessage, $newMessage, $expected); + + } + + /** + * A party crasher is an attendee that accepted an event, but was not in + * any original invite. + * + * @depends testAccepted + */ + function testPartyCrasher() { + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SUMMARY:B-day party +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +DTSTART:20140716T120000Z +RRULE:FREQ=DAILY +END:VEVENT +BEGIN:VEVENT +UID:foobar +RECURRENCE-ID:20140717T120000Z +SUMMARY:B-day party +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +DTSTART:20140717T120000Z +RRULE:FREQ=DAILY +END:VEVENT +END:VCALENDAR +ICS; + + + $newMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SUMMARY:B-day party +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +DTSTART:20140716T120000Z +RRULE:FREQ=DAILY +END:VEVENT +BEGIN:VEVENT +UID:foobar +RECURRENCE-ID:20140717T120000Z +SUMMARY:B-day party +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=ACCEPTED;CN=One:mailto:one@example.org +DTSTART:20140717T120000Z +RRULE:FREQ=DAILY +END:VEVENT +END:VCALENDAR +ICS; + + $expected = [ + [ + 'uid' => 'foobar', + 'method' => 'REPLY', + 'component' => 'VEVENT', + 'sender' => 'mailto:one@example.org', + 'senderName' => 'One', + 'recipient' => 'mailto:strunk@example.org', + 'recipientName' => 'Strunk', + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:REPLY +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:1 +DTSTART:20140717T120000Z +SUMMARY:B-day party +RECURRENCE-ID:20140717T120000Z +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=ACCEPTED;CN=One:mailto:one@example.org +END:VEVENT +END:VCALENDAR + +ICS + + ], + + ]; + + $this->parse($oldMessage, $newMessage, $expected); + + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerDeleteEventTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerDeleteEventTest.php new file mode 100644 index 00000000000..935c451fe76 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerDeleteEventTest.php @@ -0,0 +1,344 @@ +<?php + +namespace Sabre\VObject\ITip; + +class BrokerDeleteEventTest extends BrokerTester { + + function testOrganizerDeleteWithDtend() { + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +SUMMARY:foo +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +ATTENDEE;CN=Two:mailto:two@example.org +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + + $newMessage = null; + + $version = \Sabre\VObject\Version::VERSION; + + $expected = [ + [ + 'uid' => 'foobar', + 'method' => 'CANCEL', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + 'senderName' => 'Strunk', + 'recipient' => 'mailto:one@example.org', + 'recipientName' => 'One', + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:CANCEL +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:2 +SUMMARY:foo +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +END:VEVENT +END:VCALENDAR +ICS + ], + + [ + 'uid' => 'foobar', + 'method' => 'CANCEL', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + 'senderName' => 'Strunk', + 'recipient' => 'mailto:two@example.org', + 'recipientName' => 'Two', + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:CANCEL +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:2 +SUMMARY:foo +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Two:mailto:two@example.org +END:VEVENT +END:VCALENDAR +ICS + + ], + ]; + + $this->parse($oldMessage, $newMessage, $expected, 'mailto:strunk@example.org'); + + } + + function testOrganizerDeleteWithDuration() { + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +SUMMARY:foo +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +ATTENDEE;CN=Two:mailto:two@example.org +DTSTART:20140716T120000Z +DURATION:PT1H +END:VEVENT +END:VCALENDAR +ICS; + + + $newMessage = null; + + $version = \Sabre\VObject\Version::VERSION; + + $expected = [ + [ + 'uid' => 'foobar', + 'method' => 'CANCEL', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + 'senderName' => 'Strunk', + 'recipient' => 'mailto:one@example.org', + 'recipientName' => 'One', + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:CANCEL +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:2 +SUMMARY:foo +DTSTART:20140716T120000Z +DURATION:PT1H +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +END:VEVENT +END:VCALENDAR +ICS + ], + + [ + 'uid' => 'foobar', + 'method' => 'CANCEL', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + 'senderName' => 'Strunk', + 'recipient' => 'mailto:two@example.org', + 'recipientName' => 'Two', + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:CANCEL +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:2 +SUMMARY:foo +DTSTART:20140716T120000Z +DURATION:PT1H +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Two:mailto:two@example.org +END:VEVENT +END:VCALENDAR +ICS + + ], + ]; + + $this->parse($oldMessage, $newMessage, $expected, 'mailto:strunk@example.org'); + + } + + function testAttendeeDeleteWithDtend() { + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +SUMMARY:foo +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +ATTENDEE;CN=Two:mailto:two@example.org +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + + $newMessage = null; + + $version = \Sabre\VObject\Version::VERSION; + + $expected = [ + [ + 'uid' => 'foobar', + 'method' => 'REPLY', + 'component' => 'VEVENT', + 'sender' => 'mailto:one@example.org', + 'senderName' => 'One', + 'recipient' => 'mailto:strunk@example.org', + 'recipientName' => 'Strunk', + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:REPLY +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:1 +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +SUMMARY:foo +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=DECLINED;CN=One:mailto:one@example.org +END:VEVENT +END:VCALENDAR +ICS + ], + ]; + + $this->parse($oldMessage, $newMessage, $expected, 'mailto:one@example.org'); + + + } + + function testAttendeeReplyWithDuration() { + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +SUMMARY:foo +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +ATTENDEE;CN=Two:mailto:two@example.org +DTSTART:20140716T120000Z +DURATION:PT1H +END:VEVENT +END:VCALENDAR +ICS; + + + $newMessage = null; + + $version = \Sabre\VObject\Version::VERSION; + + $expected = [ + [ + 'uid' => 'foobar', + 'method' => 'REPLY', + 'component' => 'VEVENT', + 'sender' => 'mailto:one@example.org', + 'senderName' => 'One', + 'recipient' => 'mailto:strunk@example.org', + 'recipientName' => 'Strunk', + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:REPLY +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:1 +DTSTART:20140716T120000Z +DURATION:PT1H +SUMMARY:foo +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;PARTSTAT=DECLINED;CN=One:mailto:one@example.org +END:VEVENT +END:VCALENDAR +ICS + ], + ]; + + $this->parse($oldMessage, $newMessage, $expected, 'mailto:one@example.org'); + + + } + + function testAttendeeDeleteCancelledEvent() { + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +STATUS:CANCELLED +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +ATTENDEE;CN=Two:mailto:two@example.org +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + + $newMessage = null; + + $expected = []; + + $this->parse($oldMessage, $newMessage, $expected, 'mailto:one@example.org'); + + + } + + function testNoCalendar() { + + $this->parse(null, null, [], 'mailto:one@example.org'); + + } + + function testVTodo() { + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VTODO +UID:foobar +SEQUENCE:1 +END:VTODO +END:VCALENDAR +ICS; + $this->parse($oldMessage, null, [], 'mailto:one@example.org'); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerNewEventTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerNewEventTest.php new file mode 100644 index 00000000000..05cf452a85c --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerNewEventTest.php @@ -0,0 +1,496 @@ +<?php + +namespace Sabre\VObject\ITip; + +class BrokerNewEventTest extends BrokerTester { + + function testNoAttendee() { + + $message = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:foobar +DTSTART:20140811T220000Z +DTEND:20140811T230000Z +END:VEVENT +END:VCALENDAR +ICS; + + $result = $this->parse(null, $message, []); + + } + + function testVTODO() { + + $message = <<<ICS +BEGIN:VCALENDAR +BEGIN:VTODO +UID:foobar +END:VTODO +END:VCALENDAR +ICS; + + $result = $this->parse(null, $message, []); + + } + + function testSimpleInvite() { + + $message = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +DTSTART:20140811T220000Z +DTEND:20140811T230000Z +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=White:mailto:white@example.org +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + $expectedMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VEVENT +UID:foobar +DTSTART:20140811T220000Z +DTEND:20140811T230000Z +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=White;PARTSTAT=NEEDS-ACTION:mailto:white@example.org +END:VEVENT +END:VCALENDAR +ICS; + + $expected = [ + [ + 'uid' => 'foobar', + 'method' => 'REQUEST', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + 'senderName' => 'Strunk', + 'recipient' => 'mailto:white@example.org', + 'recipientName' => 'White', + 'message' => $expectedMessage, + ], + ]; + + $this->parse(null, $message, $expected, 'mailto:strunk@example.org'); + + } + + /** + * @expectedException \Sabre\VObject\ITip\ITipException + */ + function testBrokenEventUIDMisMatch() { + + $message = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=White:mailto:white@example.org +END:VEVENT +BEGIN:VEVENT +UID:foobar2 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=White:mailto:white@example.org +END:VEVENT +END:VCALENDAR +ICS; + + $this->parse(null, $message, [], 'mailto:strunk@example.org'); + + } + /** + * @expectedException \Sabre\VObject\ITip\ITipException + */ + function testBrokenEventOrganizerMisMatch() { + + $message = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=White:mailto:white@example.org +END:VEVENT +BEGIN:VEVENT +UID:foobar +ORGANIZER:mailto:foo@example.org +ATTENDEE;CN=White:mailto:white@example.org +END:VEVENT +END:VCALENDAR +ICS; + + $this->parse(null, $message, [], 'mailto:strunk@example.org'); + + } + + function testRecurrenceInvite() { + + $message = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +ATTENDEE;CN=Two:mailto:two@example.org +DTSTART:20140716T120000Z +DURATION:PT1H +RRULE:FREQ=DAILY +EXDATE:20140717T120000Z +END:VEVENT +BEGIN:VEVENT +UID:foobar +RECURRENCE-ID:20140718T120000Z +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Two:mailto:two@example.org +ATTENDEE;CN=Three:mailto:three@example.org +DTSTART:20140718T120000Z +DURATION:PT1H +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + + $expected = [ + [ + 'uid' => 'foobar', + 'method' => 'REQUEST', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + 'senderName' => 'Strunk', + 'recipient' => 'mailto:one@example.org', + 'recipientName' => 'One', + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VEVENT +UID:foobar +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One;PARTSTAT=NEEDS-ACTION:mailto:one@example.org +ATTENDEE;CN=Two;PARTSTAT=NEEDS-ACTION:mailto:two@example.org +DTSTART:20140716T120000Z +DURATION:PT1H +RRULE:FREQ=DAILY +EXDATE:20140717T120000Z,20140718T120000Z +END:VEVENT +END:VCALENDAR +ICS + + ], + [ + 'uid' => 'foobar', + 'method' => 'REQUEST', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + 'senderName' => 'Strunk', + 'recipient' => 'mailto:two@example.org', + 'recipientName' => 'Two', + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VEVENT +UID:foobar +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One;PARTSTAT=NEEDS-ACTION:mailto:one@example.org +ATTENDEE;CN=Two;PARTSTAT=NEEDS-ACTION:mailto:two@example.org +DTSTART:20140716T120000Z +DURATION:PT1H +RRULE:FREQ=DAILY +EXDATE:20140717T120000Z +END:VEVENT +BEGIN:VEVENT +UID:foobar +RECURRENCE-ID:20140718T120000Z +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Two:mailto:two@example.org +ATTENDEE;CN=Three:mailto:three@example.org +DTSTART:20140718T120000Z +DURATION:PT1H +END:VEVENT +END:VCALENDAR +ICS + + ], + [ + 'uid' => 'foobar', + 'method' => 'REQUEST', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + 'senderName' => 'Strunk', + 'recipient' => 'mailto:three@example.org', + 'recipientName' => 'Three', + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VEVENT +UID:foobar +RECURRENCE-ID:20140718T120000Z +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Two:mailto:two@example.org +ATTENDEE;CN=Three:mailto:three@example.org +DTSTART:20140718T120000Z +DURATION:PT1H +END:VEVENT +END:VCALENDAR +ICS + + ], + ]; + + $this->parse(null, $message, $expected, 'mailto:strunk@example.org'); + + } + + function testRecurrenceInvite2() { + + // This method tests a nearly identical path, but in this case the + // master event does not have an EXDATE. + $message = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +ATTENDEE;CN=Two:mailto:two@example.org +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +RRULE:FREQ=DAILY +END:VEVENT +BEGIN:VEVENT +UID:foobar +RECURRENCE-ID:20140718T120000Z +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Two:mailto:two@example.org +ATTENDEE;CN=Three:mailto:three@example.org +DTSTART:20140718T120000Z +DTEND:20140718T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + + $expected = [ + [ + 'uid' => 'foobar', + 'method' => 'REQUEST', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + 'senderName' => 'Strunk', + 'recipient' => 'mailto:one@example.org', + 'recipientName' => 'One', + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VEVENT +UID:foobar +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One;PARTSTAT=NEEDS-ACTION:mailto:one@example.org +ATTENDEE;CN=Two;PARTSTAT=NEEDS-ACTION:mailto:two@example.org +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +RRULE:FREQ=DAILY +EXDATE:20140718T120000Z +END:VEVENT +END:VCALENDAR +ICS + + ], + [ + 'uid' => 'foobar', + 'method' => 'REQUEST', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + 'senderName' => 'Strunk', + 'recipient' => 'mailto:two@example.org', + 'recipientName' => 'Two', + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VEVENT +UID:foobar +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One;PARTSTAT=NEEDS-ACTION:mailto:one@example.org +ATTENDEE;CN=Two;PARTSTAT=NEEDS-ACTION:mailto:two@example.org +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +RRULE:FREQ=DAILY +END:VEVENT +BEGIN:VEVENT +UID:foobar +RECURRENCE-ID:20140718T120000Z +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Two:mailto:two@example.org +ATTENDEE;CN=Three:mailto:three@example.org +DTSTART:20140718T120000Z +DTEND:20140718T130000Z +END:VEVENT +END:VCALENDAR +ICS + + ], + [ + 'uid' => 'foobar', + 'method' => 'REQUEST', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + 'senderName' => 'Strunk', + 'recipient' => 'mailto:three@example.org', + 'recipientName' => 'Three', + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VEVENT +UID:foobar +RECURRENCE-ID:20140718T120000Z +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Two:mailto:two@example.org +ATTENDEE;CN=Three:mailto:three@example.org +DTSTART:20140718T120000Z +DTEND:20140718T130000Z +END:VEVENT +END:VCALENDAR +ICS + + ], + ]; + + $this->parse(null, $message, $expected, 'mailto:strunk@example.org'); + + } + + function testScheduleAgentClient() { + + $message = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +DTSTART:20140811T220000Z +DTEND:20140811T230000Z +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=White;SCHEDULE-AGENT=CLIENT:mailto:white@example.org +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + + $this->parse(null, $message, [], 'mailto:strunk@example.org'); + + } + + /** + * @expectedException Sabre\VObject\ITip\ITipException + */ + function testMultipleUID() { + + $message = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +ATTENDEE;CN=Two:mailto:two@example.org +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +RRULE:FREQ=DAILY +END:VEVENT +BEGIN:VEVENT +UID:foobar2 +RECURRENCE-ID:20140718T120000Z +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Two:mailto:two@example.org +ATTENDEE;CN=Three:mailto:three@example.org +DTSTART:20140718T120000Z +DTEND:20140718T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + $this->parse(null, $message, [], 'mailto:strunk@example.org'); + + } + + /** + * @expectedException Sabre\VObject\ITip\SameOrganizerForAllComponentsException + */ + function testChangingOrganizers() { + + $message = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +ATTENDEE;CN=Two:mailto:two@example.org +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +RRULE:FREQ=DAILY +END:VEVENT +BEGIN:VEVENT +UID:foobar +RECURRENCE-ID:20140718T120000Z +ORGANIZER;CN=Strunk:mailto:ew@example.org +ATTENDEE;CN=Two:mailto:two@example.org +ATTENDEE;CN=Three:mailto:three@example.org +DTSTART:20140718T120000Z +DTEND:20140718T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + $this->parse(null, $message, [], 'mailto:strunk@example.org'); + + } + function testNoOrganizerHasAttendee() { + + $message = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:foobar +DTSTART:20140811T220000Z +DTEND:20140811T230000Z +ATTENDEE;CN=Two:mailto:two@example.org +END:VEVENT +END:VCALENDAR +ICS; + + $this->parse(null, $message, [], 'mailto:strunk@example.org'); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerProcessMessageTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerProcessMessageTest.php new file mode 100644 index 00000000000..691574a8991 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerProcessMessageTest.php @@ -0,0 +1,164 @@ +<?php + +namespace Sabre\VObject\ITip; + +class BrokerProcessMessageTest extends BrokerTester { + + function testRequestNew() { + + $itip = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +METHOD:REQUEST +BEGIN:VEVENT +SEQUENCE:1 +UID:foobar +END:VEVENT +END:VCALENDAR +ICS; + + $expected = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +SEQUENCE:1 +UID:foobar +END:VEVENT +END:VCALENDAR +ICS; + + $result = $this->process($itip, null, $expected); + + } + + function testRequestUpdate() { + + $itip = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +METHOD:REQUEST +BEGIN:VEVENT +SEQUENCE:2 +UID:foobar +END:VEVENT +END:VCALENDAR +ICS; + + $old = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +SEQUENCE:1 +UID:foobar +END:VEVENT +END:VCALENDAR +ICS; + + $expected = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +SEQUENCE:2 +UID:foobar +END:VEVENT +END:VCALENDAR +ICS; + + $result = $this->process($itip, $old, $expected); + + } + + function testCancel() { + + $itip = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +METHOD:CANCEL +BEGIN:VEVENT +SEQUENCE:2 +UID:foobar +END:VEVENT +END:VCALENDAR +ICS; + + $old = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +SEQUENCE:1 +UID:foobar +END:VEVENT +END:VCALENDAR +ICS; + + $expected = <<<ICS +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:foobar +STATUS:CANCELLED +SEQUENCE:2 +END:VEVENT +END:VCALENDAR +ICS; + + $result = $this->process($itip, $old, $expected); + + } + + function testCancelNoExistingEvent() { + + $itip = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +METHOD:CANCEL +BEGIN:VEVENT +SEQUENCE:2 +UID:foobar +END:VEVENT +END:VCALENDAR +ICS; + + $old = null; + $expected = null; + + $result = $this->process($itip, $old, $expected); + + } + + function testUnsupportedComponent() { + + $itip = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VTODO +SEQUENCE:2 +UID:foobar +END:VTODO +END:VCALENDAR +ICS; + + $old = null; + $expected = null; + + $result = $this->process($itip, $old, $expected); + + } + + function testUnsupportedMethod() { + + $itip = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +METHOD:PUBLISH +BEGIN:VEVENT +SEQUENCE:2 +UID:foobar +END:VEVENT +END:VCALENDAR +ICS; + + $old = null; + $expected = null; + + $result = $this->process($itip, $old, $expected); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerProcessReplyTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerProcessReplyTest.php new file mode 100644 index 00000000000..533fdce1512 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerProcessReplyTest.php @@ -0,0 +1,496 @@ +<?php + +namespace Sabre\VObject\ITip; + +class BrokerProcessReplyTest extends BrokerTester { + + function testReplyNoOriginal() { + + $itip = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +METHOD:REPLY +BEGIN:VEVENT +SEQUENCE:2 +UID:foobar +ATTENDEE;PARTSTAT=ACCEPTED:mailto:foo@example.org +ORGANIZER:mailto:bar@example.org +END:VEVENT +END:VCALENDAR +ICS; + + $old = null; + $expected = null; + + $result = $this->process($itip, $old, $expected); + + } + + function testReplyAccept() { + + $itip = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +METHOD:REPLY +BEGIN:VEVENT +ATTENDEE;PARTSTAT=ACCEPTED:mailto:foo@example.org +ORGANIZER:mailto:bar@example.org +SEQUENCE:2 +UID:foobar +END:VEVENT +END:VCALENDAR +ICS; + + $old = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +SEQUENCE:2 +UID:foobar +ATTENDEE:mailto:foo@example.org +ORGANIZER:mailto:bar@example.org +END:VEVENT +END:VCALENDAR +ICS; + + $expected = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +SEQUENCE:2 +UID:foobar +ATTENDEE;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:foo@example.org +ORGANIZER:mailto:bar@example.org +END:VEVENT +END:VCALENDAR +ICS; + + $result = $this->process($itip, $old, $expected); + + } + + function testReplyRequestStatus() { + + $itip = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +METHOD:REPLY +BEGIN:VEVENT +UID:foobar +REQUEST-STATUS:2.3;foo-bar! +ATTENDEE;PARTSTAT=ACCEPTED:mailto:foo@example.org +ORGANIZER:mailto:bar@example.org +SEQUENCE:2 +UID:foobar +END:VEVENT +END:VCALENDAR +ICS; + + $old = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:2 +ATTENDEE:mailto:foo@example.org +ORGANIZER:mailto:bar@example.org +END:VEVENT +END:VCALENDAR +ICS; + + $expected = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:2 +ATTENDEE;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.3:mailto:foo@example.org +ORGANIZER:mailto:bar@example.org +END:VEVENT +END:VCALENDAR +ICS; + + $result = $this->process($itip, $old, $expected); + + } + + + function testReplyPartyCrasher() { + + $itip = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +METHOD:REPLY +BEGIN:VEVENT +ATTENDEE;PARTSTAT=ACCEPTED:mailto:crasher@example.org +ORGANIZER:mailto:bar@example.org +SEQUENCE:2 +UID:foobar +END:VEVENT +END:VCALENDAR +ICS; + + $old = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +SEQUENCE:2 +UID:foobar +ATTENDEE:mailto:foo@example.org +ORGANIZER:mailto:bar@example.org +END:VEVENT +END:VCALENDAR +ICS; + + $expected = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +SEQUENCE:2 +UID:foobar +ATTENDEE:mailto:foo@example.org +ORGANIZER:mailto:bar@example.org +ATTENDEE;PARTSTAT=ACCEPTED:mailto:crasher@example.org +END:VEVENT +END:VCALENDAR +ICS; + + $result = $this->process($itip, $old, $expected); + + } + + function testReplyNewException() { + + // This is a reply to 1 instance of a recurring event. This should + // automatically create an exception. + $itip = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +METHOD:REPLY +BEGIN:VEVENT +ATTENDEE;PARTSTAT=ACCEPTED:mailto:foo@example.org +ORGANIZER:mailto:bar@example.org +SEQUENCE:2 +RECURRENCE-ID:20140725T000000Z +UID:foobar +END:VEVENT +END:VCALENDAR +ICS; + + $old = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +SEQUENCE:2 +UID:foobar +RRULE:FREQ=DAILY +DTSTART:20140724T000000Z +DTEND:20140724T010000Z +ATTENDEE:mailto:foo@example.org +ORGANIZER:mailto:bar@example.org +END:VEVENT +END:VCALENDAR +ICS; + + $expected = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +SEQUENCE:2 +UID:foobar +RRULE:FREQ=DAILY +DTSTART:20140724T000000Z +DTEND:20140724T010000Z +ATTENDEE:mailto:foo@example.org +ORGANIZER:mailto:bar@example.org +END:VEVENT +BEGIN:VEVENT +SEQUENCE:2 +UID:foobar +DTSTART:20140725T000000Z +DTEND:20140725T010000Z +ATTENDEE;PARTSTAT=ACCEPTED:mailto:foo@example.org +ORGANIZER:mailto:bar@example.org +RECURRENCE-ID:20140725T000000Z +END:VEVENT +END:VCALENDAR +ICS; + + $result = $this->process($itip, $old, $expected); + + } + + function testReplyNewExceptionTz() { + + // This is a reply to 1 instance of a recurring event. This should + // automatically create an exception. + $itip = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +METHOD:REPLY +BEGIN:VEVENT +ATTENDEE;PARTSTAT=ACCEPTED:mailto:foo@example.org +ORGANIZER:mailto:bar@example.org +SEQUENCE:2 +RECURRENCE-ID;TZID=America/Toronto:20140725T000000 +UID:foobar +END:VEVENT +END:VCALENDAR +ICS; + + $old = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +SEQUENCE:2 +UID:foobar +RRULE:FREQ=DAILY +DTSTART;TZID=America/Toronto:20140724T000000 +DTEND;TZID=America/Toronto:20140724T010000 +ATTENDEE:mailto:foo@example.org +ORGANIZER:mailto:bar@example.org +END:VEVENT +END:VCALENDAR +ICS; + + $expected = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +SEQUENCE:2 +UID:foobar +RRULE:FREQ=DAILY +DTSTART;TZID=America/Toronto:20140724T000000 +DTEND;TZID=America/Toronto:20140724T010000 +ATTENDEE:mailto:foo@example.org +ORGANIZER:mailto:bar@example.org +END:VEVENT +BEGIN:VEVENT +SEQUENCE:2 +UID:foobar +DTSTART;TZID=America/Toronto:20140725T000000 +DTEND;TZID=America/Toronto:20140725T010000 +ATTENDEE;PARTSTAT=ACCEPTED:mailto:foo@example.org +ORGANIZER:mailto:bar@example.org +RECURRENCE-ID;TZID=America/Toronto:20140725T000000 +END:VEVENT +END:VCALENDAR +ICS; + + $result = $this->process($itip, $old, $expected); + + } + + function testReplyPartyCrashCreateExcepton() { + + // IN this test there's a recurring event that has an exception. The + // exception is missing the attendee. + // + // The attendee party crashes the instance, so it should show up in the + // resulting object. + $itip = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +METHOD:REPLY +BEGIN:VEVENT +ATTENDEE;PARTSTAT=ACCEPTED;CN=Crasher!:mailto:crasher@example.org +ORGANIZER:mailto:bar@example.org +SEQUENCE:2 +RECURRENCE-ID:20140725T000000Z +UID:foobar +END:VEVENT +END:VCALENDAR +ICS; + + $old = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +SEQUENCE:2 +UID:foobar +RRULE:FREQ=DAILY +DTSTART:20140724T000000Z +DTEND:20140724T010000Z +ORGANIZER:mailto:bar@example.org +END:VEVENT +END:VCALENDAR +ICS; + + $expected = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +SEQUENCE:2 +UID:foobar +RRULE:FREQ=DAILY +DTSTART:20140724T000000Z +DTEND:20140724T010000Z +ORGANIZER:mailto:bar@example.org +END:VEVENT +BEGIN:VEVENT +SEQUENCE:2 +UID:foobar +DTSTART:20140725T000000Z +DTEND:20140725T010000Z +ORGANIZER:mailto:bar@example.org +RECURRENCE-ID:20140725T000000Z +ATTENDEE;PARTSTAT=ACCEPTED;CN=Crasher!:mailto:crasher@example.org +END:VEVENT +END:VCALENDAR +ICS; + + $result = $this->process($itip, $old, $expected); + + } + + function testReplyNewExceptionNoMasterEvent() { + + /** + * This iTip message would normally create a new exception, but the + * server is not able to create this new instance, because there's no + * master event to clone from. + * + * This test checks if the message is ignored. + */ + $itip = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +METHOD:REPLY +BEGIN:VEVENT +ATTENDEE;PARTSTAT=ACCEPTED;CN=Crasher!:mailto:crasher@example.org +ORGANIZER:mailto:bar@example.org +SEQUENCE:2 +RECURRENCE-ID:20140725T000000Z +UID:foobar +END:VEVENT +END:VCALENDAR +ICS; + + $old = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +SEQUENCE:2 +UID:foobar +RRULE:FREQ=DAILY +DTSTART:20140724T000000Z +DTEND:20140724T010000Z +RECURRENCE-ID:20140724T000000Z +ORGANIZER:mailto:bar@example.org +END:VEVENT +END:VCALENDAR +ICS; + + $expected = null; + $result = $this->process($itip, $old, $expected); + + } + + /** + * @depends testReplyAccept + */ + function testReplyAcceptUpdateRSVP() { + + $itip = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +METHOD:REPLY +BEGIN:VEVENT +ATTENDEE;PARTSTAT=ACCEPTED:mailto:foo@example.org +ORGANIZER:mailto:bar@example.org +SEQUENCE:2 +UID:foobar +END:VEVENT +END:VCALENDAR +ICS; + + $old = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +SEQUENCE:2 +UID:foobar +ATTENDEE;RSVP=TRUE:mailto:foo@example.org +ORGANIZER:mailto:bar@example.org +END:VEVENT +END:VCALENDAR +ICS; + + $expected = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +SEQUENCE:2 +UID:foobar +ATTENDEE;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:foo@example.org +ORGANIZER:mailto:bar@example.org +END:VEVENT +END:VCALENDAR +ICS; + + $result = $this->process($itip, $old, $expected); + + } + + function testReplyNewExceptionFirstOccurence() { + + // This is a reply to 1 instance of a recurring event. This should + // automatically create an exception. + $itip = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +METHOD:REPLY +BEGIN:VEVENT +ATTENDEE;PARTSTAT=ACCEPTED:mailto:foo@example.org +ORGANIZER:mailto:bar@example.org +SEQUENCE:2 +RECURRENCE-ID:20140724T000000Z +UID:foobar +END:VEVENT +END:VCALENDAR +ICS; + + $old = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +SEQUENCE:2 +UID:foobar +RRULE:FREQ=DAILY +DTSTART:20140724T000000Z +DTEND:20140724T010000Z +ATTENDEE:mailto:foo@example.org +ORGANIZER:mailto:bar@example.org +END:VEVENT +END:VCALENDAR +ICS; + + $expected = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +SEQUENCE:2 +UID:foobar +RRULE:FREQ=DAILY +DTSTART:20140724T000000Z +DTEND:20140724T010000Z +ATTENDEE:mailto:foo@example.org +ORGANIZER:mailto:bar@example.org +END:VEVENT +BEGIN:VEVENT +SEQUENCE:2 +UID:foobar +DTSTART:20140724T000000Z +DTEND:20140724T010000Z +ATTENDEE;PARTSTAT=ACCEPTED:mailto:foo@example.org +ORGANIZER:mailto:bar@example.org +RECURRENCE-ID:20140724T000000Z +END:VEVENT +END:VCALENDAR +ICS; + + $result = $this->process($itip, $old, $expected); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerTester.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerTester.php new file mode 100644 index 00000000000..6dbb517496e --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerTester.php @@ -0,0 +1,96 @@ +<?php + +namespace Sabre\VObject\ITip; + +use Sabre\VObject\Reader; + +/** + * Utilities for testing the broker + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +abstract class BrokerTester extends \PHPUnit_Framework_TestCase { + + use \Sabre\VObject\PHPUnitAssertions; + + function parse($oldMessage, $newMessage, $expected = [], $currentUser = 'mailto:one@example.org') { + + $broker = new Broker(); + $result = $broker->parseEvent($newMessage, $currentUser, $oldMessage); + + $this->assertEquals(count($expected), count($result)); + + foreach ($expected as $index => $ex) { + + $message = $result[$index]; + + foreach ($ex as $key => $val) { + + if ($key === 'message') { + $this->assertVObjectEqualsVObject( + $val, + $message->message->serialize() + ); + } else { + $this->assertEquals($val, $message->$key); + } + + } + + } + + } + + function process($input, $existingObject = null, $expected = false) { + + $version = \Sabre\VObject\Version::VERSION; + + $vcal = Reader::read($input); + + foreach ($vcal->getComponents() as $mainComponent) { + break; + } + + $message = new Message(); + $message->message = $vcal; + $message->method = isset($vcal->METHOD) ? $vcal->METHOD->getValue() : null; + $message->component = $mainComponent->name; + $message->uid = $mainComponent->UID->getValue(); + $message->sequence = isset($vcal->VEVENT[0]) ? (string)$vcal->VEVENT[0]->SEQUENCE : null; + + if ($message->method === 'REPLY') { + + $message->sender = $mainComponent->ATTENDEE->getValue(); + $message->senderName = isset($mainComponent->ATTENDEE['CN']) ? $mainComponent->ATTENDEE['CN']->getValue() : null; + $message->recipient = $mainComponent->ORGANIZER->getValue(); + $message->recipientName = isset($mainComponent->ORGANIZER['CN']) ? $mainComponent->ORGANIZER['CN'] : null; + + } + + $broker = new Broker(); + + if (is_string($existingObject)) { + $existingObject = str_replace( + '%foo%', + "VERSION:2.0\nPRODID:-//Sabre//Sabre VObject $version//EN\nCALSCALE:GREGORIAN", + $existingObject + ); + $existingObject = Reader::read($existingObject); + } + + $result = $broker->processMessage($message, $existingObject); + + if (is_null($expected)) { + $this->assertTrue(!$result); + return; + } + + $this->assertVObjectEqualsVObject( + $expected, + $result + ); + + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerTimezoneInParseEventInfoWithoutMasterTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerTimezoneInParseEventInfoWithoutMasterTest.php new file mode 100644 index 00000000000..255a84e8c1e --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerTimezoneInParseEventInfoWithoutMasterTest.php @@ -0,0 +1,77 @@ +<?php + +namespace Sabre\VObject\ITip; + +use Sabre\VObject\Reader; + +class BrokerTimezoneInParseEventInfoWithoutMasterTest extends \PHPUnit_Framework_TestCase { + + function testTimezoneInParseEventInfoWithoutMaster() + { + $calendar = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//Mac OS X 10.9.5//EN +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/Minsk +BEGIN:DAYLIGHT +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;UNTIL=20100328T000000Z;BYMONTH=3;BYDAY=-1SU +DTSTART:19930328T020000 +TZNAME:GMT+3 +TZOFFSETTO:+0300 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +DTSTART:20110327T020000 +TZNAME:GMT+3 +TZOFFSETTO:+0300 +RDATE:20110327T020000 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20160331T163031Z +UID:B9301437-417C-4136-8DB3-8D1555863791 +DTEND;TZID=Europe/Minsk:20160405T100000 +TRANSP:OPAQUE +ATTENDEE;CN=User Invitee;CUTYPE=INDIVIDUAL;EMAIL=invitee@test.com;PARTSTAT= + ACCEPTED;ROLE=REQ-PARTICIPANT:mailto:invitee@test.com +ATTENDEE;CN=User Organizer;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:organ + izer@test.com +SUMMARY:Event title +DTSTART;TZID=Europe/Minsk:20160405T090000 +DTSTAMP:20160331T164108Z +ORGANIZER;CN=User Organizer:mailto:organizer@test.com +SEQUENCE:6 +RECURRENCE-ID;TZID=Europe/Minsk:20160405T090000 +END:VEVENT +BEGIN:VEVENT +CREATED:20160331T163031Z +UID:B9301437-417C-4136-8DB3-8D1555863791 +DTEND;TZID=Europe/Minsk:20160406T100000 +TRANSP:OPAQUE +ATTENDEE;CN=User Invitee;CUTYPE=INDIVIDUAL;EMAIL=invitee@test.com;PARTSTAT= + ACCEPTED;ROLE=REQ-PARTICIPANT:mailto:invitee@test.com +ATTENDEE;CN=User Organizer;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:organ + izer@test.com +SUMMARY:Event title +DTSTART;TZID=Europe/Minsk:20160406T090000 +DTSTAMP:20160331T165845Z +ORGANIZER;CN=User Organizer:mailto:organizer@test.com +SEQUENCE:6 +RECURRENCE-ID;TZID=Europe/Minsk:20160406T090000 +END:VEVENT +END:VCALENDAR +ICS; + + $calendar = Reader::read($calendar); + $broker = new Broker(); + + $reflectionMethod = new \ReflectionMethod($broker, 'parseEventInfo'); + $reflectionMethod->setAccessible(true); + $data = $reflectionMethod->invoke($broker, $calendar); + $this->assertInstanceOf('DateTimeZone', $data['timezone']); + $this->assertEquals($data['timezone']->getName(), 'Europe/Minsk'); + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerUpdateEventTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerUpdateEventTest.php new file mode 100644 index 00000000000..bc109009e70 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/BrokerUpdateEventTest.php @@ -0,0 +1,846 @@ +<?php + +namespace Sabre\VObject\ITip; + +class BrokerUpdateEventTest extends BrokerTester { + + function testInviteChange() { + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +SUMMARY:foo +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Strunk;PARTSTAT=ACCEPTED:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +ATTENDEE;CN=Two:mailto:two@example.org +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + + $newMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:2 +SUMMARY:foo +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Strunk;PARTSTAT=ACCEPTED:mailto:strunk@example.org +ATTENDEE;CN=Two:mailto:two@example.org +ATTENDEE;CN=Three:mailto:three@example.org +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + + $expected = [ + [ + 'uid' => 'foobar', + 'method' => 'CANCEL', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + 'senderName' => 'Strunk', + 'recipient' => 'mailto:one@example.org', + 'recipientName' => 'One', + 'significantChange' => true, + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:CANCEL +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:2 +SUMMARY:foo +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +END:VEVENT +END:VCALENDAR +ICS + + ], + [ + 'uid' => 'foobar', + 'method' => 'REQUEST', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + 'senderName' => 'Strunk', + 'recipient' => 'mailto:two@example.org', + 'recipientName' => 'Two', + 'significantChange' => false, + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VEVENT +UID:foobar +SEQUENCE:2 +SUMMARY:foo +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Strunk;PARTSTAT=ACCEPTED:mailto:strunk@example.org +ATTENDEE;CN=Two;PARTSTAT=NEEDS-ACTION:mailto:two@example.org +ATTENDEE;CN=Three;PARTSTAT=NEEDS-ACTION:mailto:three@example.org +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS + + ], + [ + 'uid' => 'foobar', + 'method' => 'REQUEST', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + 'senderName' => 'Strunk', + 'recipient' => 'mailto:three@example.org', + 'recipientName' => 'Three', + 'significantChange' => true, + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VEVENT +UID:foobar +SEQUENCE:2 +SUMMARY:foo +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Strunk;PARTSTAT=ACCEPTED:mailto:strunk@example.org +ATTENDEE;CN=Two;PARTSTAT=NEEDS-ACTION:mailto:two@example.org +ATTENDEE;CN=Three;PARTSTAT=NEEDS-ACTION:mailto:three@example.org +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS + + ], + ]; + + $this->parse($oldMessage, $newMessage, $expected, 'mailto:strunk@example.org'); + + } + + function testInviteChangeFromNonSchedulingToSchedulingObject() { + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + + $newMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:2 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + + $expected = [ + [ + 'uid' => 'foobar', + 'method' => 'REQUEST', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + 'senderName' => 'Strunk', + 'recipient' => 'mailto:one@example.org', + 'recipientName' => 'One', + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VEVENT +UID:foobar +SEQUENCE:2 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One;PARTSTAT=NEEDS-ACTION:mailto:one@example.org +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS + + ], + + ]; + + $this->parse($oldMessage, $newMessage, $expected, 'mailto:strunk@example.org'); + + } + + function testInviteChangeFromSchedulingToNonSchedulingObject() { + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:2 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + + $newMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + + $expected = [ + [ + 'uid' => 'foobar', + 'method' => 'CANCEL', + 'component' => 'VEVENT', + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:CANCEL +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:1 +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +END:VEVENT +END:VCALENDAR +ICS + + ], + + ]; + + $this->parse($oldMessage, $newMessage, $expected, 'mailto:strunk@example.org'); + + } + + function testNoAttendees() { + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + + $newMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:2 +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + + $expected = []; + $this->parse($oldMessage, $newMessage, $expected, 'mailto:strunk@example.org'); + + } + + function testRemoveInstance() { + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +DTSTART;TZID=America/Toronto:20140716T120000 +DTEND;TZID=America/Toronto:20140716T130000 +RRULE:FREQ=WEEKLY +END:VEVENT +END:VCALENDAR +ICS; + + + $newMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:2 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +DTSTART;TZID=America/Toronto:20140716T120000 +DTEND;TZID=America/Toronto:20140716T130000 +RRULE:FREQ=WEEKLY +EXDATE;TZID=America/Toronto:20140724T120000 +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + + $expected = [ + [ + 'uid' => 'foobar', + 'method' => 'REQUEST', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + 'senderName' => 'Strunk', + 'recipient' => 'mailto:one@example.org', + 'recipientName' => 'One', + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VEVENT +UID:foobar +SEQUENCE:2 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One;PARTSTAT=NEEDS-ACTION:mailto:one@example.org +DTSTART;TZID=America/Toronto:20140716T120000 +DTEND;TZID=America/Toronto:20140716T130000 +RRULE:FREQ=WEEKLY +EXDATE;TZID=America/Toronto:20140724T120000 +END:VEVENT +END:VCALENDAR +ICS + + ], + ]; + + $this->parse($oldMessage, $newMessage, $expected, 'mailto:strunk@example.org'); + + } + + /** + * This test is identical to the first test, except this time we change the + * DURATION property. + * + * This should ensure that the message is significant for every attendee, + */ + function testInviteChangeSignificantChange() { + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +DURATION:PT1H +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Strunk;PARTSTAT=ACCEPTED:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +ATTENDEE;CN=Two:mailto:two@example.org +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + + $newMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +DURATION:PT2H +SEQUENCE:2 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Strunk;PARTSTAT=ACCEPTED:mailto:strunk@example.org +ATTENDEE;CN=Two:mailto:two@example.org +ATTENDEE;CN=Three:mailto:three@example.org +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + + $expected = [ + [ + 'uid' => 'foobar', + 'method' => 'CANCEL', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + 'senderName' => 'Strunk', + 'recipient' => 'mailto:one@example.org', + 'recipientName' => 'One', + 'significantChange' => true, + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:CANCEL +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:2 +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +END:VEVENT +END:VCALENDAR +ICS + + ], + [ + 'uid' => 'foobar', + 'method' => 'REQUEST', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + 'senderName' => 'Strunk', + 'recipient' => 'mailto:two@example.org', + 'recipientName' => 'Two', + 'significantChange' => true, + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VEVENT +UID:foobar +DURATION:PT2H +SEQUENCE:2 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Strunk;PARTSTAT=ACCEPTED:mailto:strunk@example.org +ATTENDEE;CN=Two;PARTSTAT=NEEDS-ACTION:mailto:two@example.org +ATTENDEE;CN=Three;PARTSTAT=NEEDS-ACTION:mailto:three@example.org +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS + + ], + [ + 'uid' => 'foobar', + 'method' => 'REQUEST', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + 'senderName' => 'Strunk', + 'recipient' => 'mailto:three@example.org', + 'recipientName' => 'Three', + 'significantChange' => true, + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VEVENT +UID:foobar +DURATION:PT2H +SEQUENCE:2 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Strunk;PARTSTAT=ACCEPTED:mailto:strunk@example.org +ATTENDEE;CN=Two;PARTSTAT=NEEDS-ACTION:mailto:two@example.org +ATTENDEE;CN=Three;PARTSTAT=NEEDS-ACTION:mailto:three@example.org +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS + + ], + ]; + + $this->parse($oldMessage, $newMessage, $expected, 'mailto:strunk@example.org'); + + } + + function testInviteNoChange() { + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Strunk;PARTSTAT=ACCEPTED:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + + $newMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:2 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Strunk;PARTSTAT=ACCEPTED:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + + $expected = [ + [ + 'uid' => 'foobar', + 'method' => 'REQUEST', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + 'senderName' => 'Strunk', + 'recipient' => 'mailto:one@example.org', + 'recipientName' => 'One', + 'significantChange' => false, + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VEVENT +UID:foobar +SEQUENCE:2 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Strunk;PARTSTAT=ACCEPTED:mailto:strunk@example.org +ATTENDEE;CN=One;PARTSTAT=NEEDS-ACTION:mailto:one@example.org +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS + + ], + + ]; + + $this->parse($oldMessage, $newMessage, $expected, 'mailto:strunk@example.org'); + + } + + function testInviteNoChangeForceSend() { + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Strunk;PARTSTAT=ACCEPTED:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + + $newMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:2 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Strunk;PARTSTAT=ACCEPTED:mailto:strunk@example.org +ATTENDEE;SCHEDULE-FORCE-SEND=REQUEST;CN=One:mailto:one@example.org +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + + $expected = [ + [ + 'uid' => 'foobar', + 'method' => 'REQUEST', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + 'senderName' => 'Strunk', + 'recipient' => 'mailto:one@example.org', + 'recipientName' => 'One', + 'significantChange' => true, + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VEVENT +UID:foobar +SEQUENCE:2 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Strunk;PARTSTAT=ACCEPTED:mailto:strunk@example.org +ATTENDEE;CN=One;PARTSTAT=NEEDS-ACTION:mailto:one@example.org +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS + + ], + + ]; + + $this->parse($oldMessage, $newMessage, $expected, 'mailto:strunk@example.org'); + + } + + function testInviteRemoveAttendees() { + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +SUMMARY:foo +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +ATTENDEE;CN=Two:mailto:two@example.org +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + + $newMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:foobar +SEQUENCE:2 +SUMMARY:foo +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + + $expected = [ + [ + 'uid' => 'foobar', + 'method' => 'CANCEL', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + 'senderName' => 'Strunk', + 'recipient' => 'mailto:one@example.org', + 'recipientName' => 'One', + 'significantChange' => true, + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:CANCEL +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:2 +SUMMARY:foo +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=One:mailto:one@example.org +END:VEVENT +END:VCALENDAR +ICS + + ], + [ + 'uid' => 'foobar', + 'method' => 'CANCEL', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + 'senderName' => 'Strunk', + 'recipient' => 'mailto:two@example.org', + 'recipientName' => 'Two', + 'significantChange' => true, + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:CANCEL +BEGIN:VEVENT +UID:foobar +DTSTAMP:**ANY** +SEQUENCE:2 +SUMMARY:foo +DTSTART:20140716T120000Z +DTEND:20140716T130000Z +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Two:mailto:two@example.org +END:VEVENT +END:VCALENDAR +ICS + + ], + ]; + + $result = $this->parse($oldMessage, $newMessage, $expected, 'mailto:strunk@example.org'); + + } + + function testInviteChangeExdateOrder() { + + $oldMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//Mac OS X 10.10.1//EN +CALSCALE:GREGORIAN +BEGIN:VEVENT +UID:foobar +SEQUENCE:0 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Strunk;CUTYPE=INDIVIDUAL;EMAIL=strunk@example.org;PARTSTAT=ACCE + PTED:mailto:strunk@example.org +ATTENDEE;CN=One;CUTYPE=INDIVIDUAL;EMAIL=one@example.org;PARTSTAT=ACCEPTED;R + OLE=REQ-PARTICIPANT;SCHEDULE-STATUS="1.2;Message delivered locally":mailto + :one@example.org +SUMMARY:foo +DTSTART:20141211T160000Z +DTEND:20141211T170000Z +RRULE:FREQ=WEEKLY +EXDATE:20141225T160000Z,20150101T160000Z +EXDATE:20150108T160000Z +END:VEVENT +END:VCALENDAR +ICS; + + + $newMessage = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//Mac OS X 10.10.1//EN +CALSCALE:GREGORIAN +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Strunk;CUTYPE=INDIVIDUAL;EMAIL=strunk@example.org;PARTSTAT=ACCE + PTED:mailto:strunk@example.org +ATTENDEE;CN=One;CUTYPE=INDIVIDUAL;EMAIL=one@example.org;PARTSTAT=ACCEPTED;R + OLE=REQ-PARTICIPANT;SCHEDULE-STATUS=1.2:mailto:one@example.org +DTSTART:20141211T160000Z +DTEND:20141211T170000Z +RRULE:FREQ=WEEKLY +EXDATE:20150101T160000Z +EXDATE:20150108T160000Z,20141225T160000Z +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + + $expected = [ + [ + 'uid' => 'foobar', + 'method' => 'REQUEST', + 'component' => 'VEVENT', + 'sender' => 'mailto:strunk@example.org', + 'senderName' => 'Strunk', + 'recipient' => 'mailto:one@example.org', + 'recipientName' => 'One', + 'significantChange' => false, + 'message' => <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VEVENT +UID:foobar +SEQUENCE:1 +ORGANIZER;CN=Strunk:mailto:strunk@example.org +ATTENDEE;CN=Strunk;CUTYPE=INDIVIDUAL;EMAIL=strunk@example.org;PARTSTAT=ACCE + PTED:mailto:strunk@example.org +ATTENDEE;CN=One;CUTYPE=INDIVIDUAL;EMAIL=one@example.org;PARTSTAT=ACCEPTED;R + OLE=REQ-PARTICIPANT:mailto:one@example.org +DTSTART:20141211T160000Z +DTEND:20141211T170000Z +RRULE:FREQ=WEEKLY +EXDATE:20150101T160000Z +EXDATE:20150108T160000Z,20141225T160000Z +END:VEVENT +END:VCALENDAR +ICS + + ], + ]; + + $this->parse($oldMessage, $newMessage, $expected, 'mailto:strunk@example.org'); + + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/EvolutionTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/EvolutionTest.php new file mode 100644 index 00000000000..3afe560d508 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/EvolutionTest.php @@ -0,0 +1,2653 @@ +<?php + +namespace Sabre\VObject\ITip; + +class EvolutionTest extends BrokerTester { + + /** + * Evolution does things as usual a little bit differently. + * + * We're adding a seprate test just for it. + */ + function testNewEvolutionEvent() { + + $ics = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +PRODID:-//Ximian//NONSGML Evolution Calendar//EN +BEGIN:VTIMEZONE +TZID:/freeassociation.sourceforge.net/Tzfile/America/Toronto +X-LIC-LOCATION:America/Toronto +BEGIN:STANDARD +TZNAME:EST +DTSTART:19691026T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19700426T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19701025T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19710425T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19711031T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19720430T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19721029T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19730429T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19731028T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19740428T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19741027T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19750427T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19751026T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19760425T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19761031T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19770424T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19771030T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19780430T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19781029T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19790429T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19791028T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19800427T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19801026T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19810426T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19811025T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19820425T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19821031T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19830424T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19831030T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19840429T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19841028T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19850428T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19851027T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19860427T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19861026T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19870405T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19871025T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19880403T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19881030T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19890402T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19891029T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19900401T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19901028T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19910407T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19911027T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19920405T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19921025T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19930404T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19931031T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19940403T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19941030T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19950402T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19951029T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19960407T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19961027T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19970406T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19971026T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19980405T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19981025T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19990404T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19991031T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20000402T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20001029T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20010401T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20011028T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20020407T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20021027T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20030406T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20031026T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20040404T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20041031T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20050403T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20051030T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20060402T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20061029T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20070311T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20071104T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20080309T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20081102T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20090308T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20091101T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20100314T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20101107T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20110313T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20111106T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20120311T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20121104T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20130310T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20131103T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20140309T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20141102T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20150308T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20151101T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20160313T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20161106T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20170312T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20171105T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20180311T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20181104T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20190310T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20191103T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20200308T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20201101T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20210314T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20211107T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20220313T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20221106T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20230312T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20231105T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20240310T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20241103T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20250309T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20251102T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20260308T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20261101T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20270314T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20271107T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20280312T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20281105T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20290311T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20291104T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20300310T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20301103T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20310309T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20311102T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20320314T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20321107T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20330313T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20331106T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20340312T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20341105T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20350311T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20351104T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20360309T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20361102T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20370308T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20371101T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +UID:20140813T153116Z-12176-1000-1065-6@johnny-lubuntu +DTSTAMP:20140813T142829Z +DTSTART;TZID=/freeassociation.sourceforge.net/Tzfile/America/Toronto:201408 + 15T110000 +DTEND;TZID=/freeassociation.sourceforge.net/Tzfile/America/Toronto:20140815 + T113000 +TRANSP:OPAQUE +SEQUENCE:2 +SUMMARY:Evo makes a Meeting (fruux HQ) (fruux HQ) +LOCATION:fruux HQ +CLASS:PUBLIC +ORGANIZER;SENT-BY="MAILTO:martin+johnny@fruux.com":MAILTO:martin@fruux.com +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRUE + ;SENT-BY="MAILTO:martin+johnny@fruux.com";LANGUAGE=en:MAILTO:martin@fruux. + com +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP= + TRUE;LANGUAGE=en:MAILTO:dominik@fruux.com +CREATED:20140813T153211Z +LAST-MODIFIED:20140813T155353Z +END:VEVENT +END:VCALENDAR +ICS; + + $version = \Sabre\VObject\Version::VERSION; + $expectedICS = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VTIMEZONE +TZID:/freeassociation.sourceforge.net/Tzfile/America/Toronto +X-LIC-LOCATION:America/Toronto +BEGIN:STANDARD +TZNAME:EST +DTSTART:19691026T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19700426T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19701025T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19710425T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19711031T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19720430T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19721029T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19730429T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19731028T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19740428T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19741027T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19750427T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19751026T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19760425T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19761031T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19770424T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19771030T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19780430T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19781029T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19790429T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19791028T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19800427T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19801026T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19810426T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19811025T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19820425T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19821031T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19830424T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19831030T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19840429T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19841028T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19850428T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19851027T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19860427T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19861026T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19870405T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19871025T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19880403T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19881030T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19890402T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19891029T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19900401T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19901028T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19910407T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19911027T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19920405T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19921025T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19930404T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19931031T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19940403T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19941030T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19950402T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19951029T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19960407T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19961027T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19970406T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19971026T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19980405T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19981025T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19990404T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19991031T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20000402T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20001029T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20010401T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20011028T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20020407T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20021027T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20030406T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20031026T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20040404T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20041031T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20050403T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20051030T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20060402T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20061029T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20070311T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20071104T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20080309T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20081102T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20090308T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20091101T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20100314T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20101107T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20110313T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20111106T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20120311T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20121104T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20130310T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20131103T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20140309T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20141102T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20150308T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20151101T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20160313T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20161106T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20170312T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20171105T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20180311T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20181104T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20190310T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20191103T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20200308T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20201101T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20210314T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20211107T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20220313T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20221106T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20230312T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20231105T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20240310T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20241103T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20250309T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20251102T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20260308T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20261101T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20270314T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20271107T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20280312T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20281105T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20290311T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20291104T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20300310T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20301103T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20310309T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20311102T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20320314T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20321107T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20330313T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20331106T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20340312T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20341105T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20350311T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20351104T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20360309T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20361102T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20370308T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20371101T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +UID:20140813T153116Z-12176-1000-1065-6@johnny-lubuntu +DTSTAMP:20140813T142829Z +DTSTART;TZID=/freeassociation.sourceforge.net/Tzfile/America/Toronto:201408 + 15T110000 +DTEND;TZID=/freeassociation.sourceforge.net/Tzfile/America/Toronto:20140815 + T113000 +TRANSP:OPAQUE +SEQUENCE:2 +SUMMARY:Evo makes a Meeting (fruux HQ) (fruux HQ) +LOCATION:fruux HQ +CLASS:PUBLIC +ORGANIZER;SENT-BY="MAILTO:martin+johnny@fruux.com":MAILTO:martin@fruux.com +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRUE + ;SENT-BY="MAILTO:martin+johnny@fruux.com";LANGUAGE=en:MAILTO:martin@fruux. + com +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP= + TRUE;LANGUAGE=en:MAILTO:dominik@fruux.com +CREATED:20140813T153211Z +LAST-MODIFIED:20140813T155353Z +END:VEVENT +END:VCALENDAR +ICS; + + $expected = [ + [ + 'uid' => '20140813T153116Z-12176-1000-1065-6@johnny-lubuntu', + 'method' => 'REQUEST', + 'sender' => 'mailto:martin@fruux.com', + 'senderName' => null, + 'recipient' => 'mailto:dominik@fruux.com', + 'recipientName' => null, + 'message' => $expectedICS, + ] + ]; + $this->parse(null, $ics, $expected, 'mailto:martin@fruux.com'); + + } + + /** + * This is an event originally from evolution, then parsed by sabredav and + * again mangled by iCal. This triggered a few bugs related to email + * address scheme casing. + */ + function testAttendeeModify() { + + $old = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject 3.3.1//EN +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:/freeassociation.sourceforge.net/Tzfile/America/Toronto +X-LIC-LOCATION:America/Toronto +BEGIN:STANDARD +TZNAME:EST +DTSTART:19691026T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19700426T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19701025T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19710425T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19711031T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19720430T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19721029T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19730429T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19731028T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19740428T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19741027T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19750427T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19751026T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19760425T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19761031T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19770424T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19771030T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19780430T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19781029T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19790429T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19791028T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19800427T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19801026T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19810426T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19811025T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19820425T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19821031T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19830424T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19831030T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19840429T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19841028T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19850428T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19851027T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19860427T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19861026T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19870405T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19871025T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19880403T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19881030T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19890402T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19891029T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19900401T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19901028T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19910407T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19911027T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19920405T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19921025T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19930404T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19931031T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19940403T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19941030T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19950402T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19951029T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19960407T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19961027T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19970406T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19971026T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19980405T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19981025T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:19990404T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:19991031T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20000402T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20001029T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20010401T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20011028T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20020407T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20021027T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20030406T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20031026T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20040404T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20041031T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20050403T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20051030T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20060402T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20061029T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20070311T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20071104T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20080309T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20081102T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20090308T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20091101T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20100314T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20101107T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20110313T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20111106T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20120311T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20121104T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20130310T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20131103T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20140309T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20141102T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20150308T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20151101T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20160313T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20161106T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20170312T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20171105T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20180311T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20181104T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20190310T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20191103T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20200308T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20201101T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20210314T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20211107T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20220313T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20221106T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20230312T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20231105T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20240310T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20241103T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20250309T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20251102T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20260308T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20261101T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20270314T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20271107T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20280312T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20281105T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20290311T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20291104T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20300310T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20301103T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20310309T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20311102T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20320314T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20321107T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20330313T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20331106T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20340312T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20341105T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20350311T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20351104T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20360309T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20361102T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:EDT +DTSTART:20370308T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +DTSTART:20371101T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +UID:20140813T212317Z-6646-1000-1221-23@evert-ubuntu +DTSTAMP:20140813T212221Z +DTSTART;TZID=/freeassociation.sourceforge.net/Tzfile/America/Toronto:201408 + 13T180000 +DTEND;TZID=/freeassociation.sourceforge.net/Tzfile/America/Toronto:20140813 + T200000 +TRANSP:OPAQUE +SEQUENCE:4 +SUMMARY:Testing evolution +LOCATION:Online +CLASS:PUBLIC +ORGANIZER:MAILTO:o@example.org +CREATED:20140813T212510Z +LAST-MODIFIED:20140813T212541Z +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRUE;LANGUAGE=en:MAILTO:o@example.org +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;LANGUAGE=en:MAILTO:a1@example.org +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;LANGUAGE=en:MAILTO:a2@example.org +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;LANGUAGE=en:MAILTO:a3@example.org +STATUS:CANCELLED +END:VEVENT +END:VCALENDAR +ICS; + + $new = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//Mac OS X 10.9.4//EN +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:America/Toronto +BEGIN:DAYLIGHT +TZOFFSETFROM:-0500 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU +DTSTART:20070311T020000 +TZNAME:EDT +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:-0400 +RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU +DTSTART:20071104T020000 +TZNAME:EST +TZOFFSETTO:-0500 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +TRANSP:OPAQUE +DTEND;TZID=America/Toronto:20140813T200000 +ORGANIZER:MAILTO:o@example.org +UID:20140813T212317Z-6646-1000-1221-23@evert-ubuntu +DTSTAMP:20140813T212221Z +LOCATION:Online +STATUS:CANCELLED +SEQUENCE:4 +CLASS:PUBLIC +SUMMARY:Testing evolution +LAST-MODIFIED:20140813T212541Z +DTSTART;TZID=America/Toronto:20140813T180000 +CREATED:20140813T212510Z +ATTENDEE;CUTYPE=INDIVIDUAL;LANGUAGE=en;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:MAILTO:a2@example.org +ATTENDEE;CUTYPE=INDIVIDUAL;LANGUAGE=en;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPANT;RSVP=TRUE:MAILTO:o@example.org +ATTENDEE;CUTYPE=INDIVIDUAL;LANGUAGE=en;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:MAILTO:a1@example.org +ATTENDEE;CUTYPE=INDIVIDUAL;LANGUAGE=en;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:MAILTO:a3@example.org +END:VEVENT +END:VCALENDAR +ICS; + + $this->parse($old, $new, [], 'mailto:a1@example.org'); + + + } + + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/MessageTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/MessageTest.php new file mode 100644 index 00000000000..0fed7eb4a76 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ITip/MessageTest.php @@ -0,0 +1,32 @@ +<?php + +namespace Sabre\VObject\ITip; + +class MessageTest extends \PHPUnit_Framework_TestCase { + + function testNoScheduleStatus() { + + $message = new Message(); + $this->assertFalse($message->getScheduleStatus()); + + } + + function testScheduleStatus() { + + $message = new Message(); + $message->scheduleStatus = '1.2;Delivered'; + + $this->assertEquals('1.2', $message->getScheduleStatus()); + + } + + function testUnexpectedScheduleStatus() { + + $message = new Message(); + $message->scheduleStatus = '9.9.9'; + + $this->assertEquals('9.9.9', $message->getScheduleStatus()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Issue153Test.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Issue153Test.php new file mode 100644 index 00000000000..fca07fe9f39 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Issue153Test.php @@ -0,0 +1,14 @@ +<?php + +namespace Sabre\VObject; + +class Issue153Test extends \PHPUnit_Framework_TestCase { + + function testRead() { + + $obj = Reader::read(file_get_contents(dirname(__FILE__) . '/issue153.vcf')); + $this->assertEquals('Test Benutzer', (string)$obj->FN); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Issue259Test.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Issue259Test.php new file mode 100644 index 00000000000..4a73be560c2 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Issue259Test.php @@ -0,0 +1,21 @@ +<?php + +namespace Sabre\VObject; + +class Issue259Test extends \PHPUnit_Framework_TestCase { + + function testParsingJcalWithUntil() { + $jcalWithUntil = '["vcalendar",[],[["vevent",[["uid",{},"text","dd1f7d29"],["organizer",{"cn":"robert"},"cal-address","mailto:robert@robert.com"],["dtstart",{"tzid":"Europe/Berlin"},"date-time","2015-10-21T12:00:00"],["dtend",{"tzid":"Europe/Berlin"},"date-time","2015-10-21T13:00:00"],["transp",{},"text","OPAQUE"],["rrule",{},"recur",{"freq":"MONTHLY","until":"2016-01-01T22:00:00Z"}]],[]]]]'; + $parser = new Parser\Json(); + $parser->setInput($jcalWithUntil); + + $vcalendar = $parser->parse(); + $eventAsArray = $vcalendar->select('VEVENT'); + $event = reset($eventAsArray); + $rruleAsArray = $event->select('RRULE'); + $rrule = reset($rruleAsArray); + $this->assertNotNull($rrule); + $this->assertEquals($rrule->getValue(), 'FREQ=MONTHLY;UNTIL=20160101T220000Z'); + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Issue36WorkAroundTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Issue36WorkAroundTest.php new file mode 100644 index 00000000000..e2b9caf1d9f --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Issue36WorkAroundTest.php @@ -0,0 +1,39 @@ +<?php + +namespace Sabre\VObject; + +class Issue36WorkAroundTest extends \PHPUnit_Framework_TestCase { + + function testWorkaround() { + + // See https://github.com/fruux/sabre-vobject/issues/36 + $event = <<<ICS +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +SUMMARY:Titel +SEQUENCE:1 +TRANSP:TRANSPARENT +RRULE:FREQ=YEARLY +LAST-MODIFIED:20130323T225737Z +DTSTAMP:20130323T225737Z +UID:1833bd44-188b-405c-9f85-1a12105318aa +CATEGORIES:Jubiläum +X-MOZ-GENERATION:3 +RECURRENCE-ID;RANGE=THISANDFUTURE;VALUE=DATE:20131013 +DTSTART;VALUE=DATE:20131013 +CREATED:20100721T121914Z +DURATION:P1D +END:VEVENT +END:VCALENDAR +ICS; + + $obj = Reader::read($event); + + // If this does not throw an exception, it's all good. + $it = new Recur\EventIterator($obj, '1833bd44-188b-405c-9f85-1a12105318aa'); + $this->assertInstanceOf('Sabre\\VObject\\Recur\\EventIterator', $it); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Issue40Test.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Issue40Test.php new file mode 100644 index 00000000000..401cc19a05a --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Issue40Test.php @@ -0,0 +1,32 @@ +<?php + +namespace Sabre\VObject; + +/** + * This test is created to handle the issues brought forward by issue 40. + * + * https://github.com/fruux/sabre-vobject/issues/40 + */ +class Issue40Test extends \PHPUnit_Framework_TestCase { + + function testEncode() { + + $card = new Component\VCard(); + $card->add('N', ['van der Harten', ['Rene', 'J.'], "", 'Sir', 'R.D.O.N.'], ['SORT-AS' => ['Harten', 'Rene']]); + + unset($card->UID); + + $expected = implode("\r\n", [ + "BEGIN:VCARD", + "VERSION:4.0", + "PRODID:-//Sabre//Sabre VObject " . Version::VERSION . '//EN', + "N;SORT-AS=Harten,Rene:van der Harten;Rene,J.;;Sir;R.D.O.N.", + "END:VCARD", + "" + ]); + + $this->assertEquals($expected, $card->serialize()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Issue64Test.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Issue64Test.php new file mode 100644 index 00000000000..986a2481361 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Issue64Test.php @@ -0,0 +1,19 @@ +<?php + +namespace Sabre\VObject; + +class Issue64Test extends \PHPUnit_Framework_TestCase { + + function testRead() { + + $vcard = Reader::read(file_get_contents(dirname(__FILE__) . '/issue64.vcf')); + $vcard = $vcard->convert(\Sabre\VObject\Document::VCARD30); + $vcard = $vcard->serialize(); + + $converted = Reader::read($vcard); + + $this->assertInstanceOf('Sabre\\VObject\\Component\\VCard', $converted); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Issue96Test.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Issue96Test.php new file mode 100644 index 00000000000..f0ed54804f1 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Issue96Test.php @@ -0,0 +1,24 @@ +<?php + +namespace Sabre\VObject; + +class Issue96Test extends \PHPUnit_Framework_TestCase { + + function testRead() { + + $input = <<<VCF +BEGIN:VCARD +VERSION:2.1 +SOURCE:Yahoo Contacts (http://contacts.yahoo.com) +URL;CHARSET=utf-8;ENCODING=QUOTED-PRINTABLE:= +http://www.example.org +END:VCARD +VCF; + + $vcard = Reader::read($input, Reader::OPTION_FORGIVING); + $this->assertInstanceOf('Sabre\\VObject\\Component\\VCard', $vcard); + $this->assertEquals("http://www.example.org", $vcard->URL->getValue()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/IssueUndefinedIndexTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/IssueUndefinedIndexTest.php new file mode 100644 index 00000000000..6f8afe60cd7 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/IssueUndefinedIndexTest.php @@ -0,0 +1,29 @@ +<?php + +namespace Sabre\VObject; + +class IssueUndefinedIndexTest extends \PHPUnit_Framework_TestCase { + + /** + * @expectedException \Sabre\VObject\ParseException + */ + function testRead() { + + $input = <<<VCF +BEGIN:VCARD +VERSION:3.0 +PRODID:foo +N:Holmes;Sherlock;;; +FN:Sherlock Holmes +ORG:Acme Inc; +ADR;type=WORK;type=pref:;;, +\\n221B,Baker Street;London;;12345;United Kingdom +UID:foo +END:VCARD +VCF; + + $vcard = Reader::read($input, Reader::OPTION_FORGIVING); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/JCalTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/JCalTest.php new file mode 100644 index 00000000000..8c437162dde --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/JCalTest.php @@ -0,0 +1,149 @@ +<?php + +namespace Sabre\VObject; + +class JCalTest extends \PHPUnit_Framework_TestCase { + + function testToJCal() { + + $cal = new Component\VCalendar(); + + $event = $cal->add('VEVENT', [ + "UID" => "foo", + "DTSTART" => new \DateTime("2013-05-26 18:10:00Z"), + "DURATION" => "P1D", + "CATEGORIES" => ['home', 'testing'], + "CREATED" => new \DateTime("2013-05-26 18:10:00Z"), + + "ATTENDEE" => "mailto:armin@example.org", + "GEO" => [51.96668, 7.61876], + "SEQUENCE" => 5, + "FREEBUSY" => ["20130526T210213Z/PT1H", "20130626T120000Z/20130626T130000Z"], + "URL" => "http://example.org/", + "TZOFFSETFROM" => "+0500", + "RRULE" => ['FREQ' => 'WEEKLY', 'BYDAY' => ['MO', 'TU']], + ], false); + + // Modifying DTSTART to be a date-only. + $event->dtstart['VALUE'] = 'DATE'; + $event->add("X-BOOL", true, ['VALUE' => 'BOOLEAN']); + $event->add("X-TIME", "08:00:00", ['VALUE' => 'TIME']); + $event->add("ATTACH", "attachment", ['VALUE' => 'BINARY']); + $event->add("ATTENDEE", "mailto:dominik@example.org", ["CN" => "Dominik", "PARTSTAT" => "DECLINED"]); + + $event->add('REQUEST-STATUS', ["2.0", "Success"]); + $event->add('REQUEST-STATUS', ["3.7", "Invalid Calendar User", "ATTENDEE:mailto:jsmith@example.org"]); + + $event->add('DTEND', '20150108T133000'); + + $expected = [ + "vcalendar", + [ + [ + "version", + new \StdClass(), + "text", + "2.0" + ], + [ + "prodid", + new \StdClass(), + "text", + "-//Sabre//Sabre VObject " . Version::VERSION . "//EN", + ], + [ + "calscale", + new \StdClass(), + "text", + "GREGORIAN" + ], + ], + [ + ["vevent", + [ + [ + "uid", new \StdClass(), "text", "foo", + ], + [ + "dtstart", new \StdClass(), "date", "2013-05-26", + ], + [ + "duration", new \StdClass(), "duration", "P1D", + ], + [ + "categories", new \StdClass(), "text", "home", "testing", + ], + [ + "created", new \StdClass(), "date-time", "2013-05-26T18:10:00Z", + ], + [ + "attendee", new \StdClass(), "cal-address", "mailto:armin@example.org", + ], + [ + "attendee", + (object)[ + "cn" => "Dominik", + "partstat" => "DECLINED", + ], + "cal-address", + "mailto:dominik@example.org" + ], + [ + "geo", new \StdClass(), "float", [51.96668, 7.61876], + ], + [ + "sequence", new \StdClass(), "integer", 5 + ], + [ + "freebusy", new \StdClass(), "period", ["2013-05-26T21:02:13", "PT1H"], ["2013-06-26T12:00:00", "2013-06-26T13:00:00"], + ], + [ + "url", new \StdClass(), "uri", "http://example.org/", + ], + [ + "tzoffsetfrom", new \StdClass(), "utc-offset", "+05:00", + ], + [ + "rrule", new \StdClass(), "recur", [ + 'freq' => 'WEEKLY', + 'byday' => ['MO', 'TU'], + ], + ], + [ + "x-bool", new \StdClass(), "boolean", true + ], + [ + "x-time", new \StdClass(), "time", "08:00:00", + ], + [ + "attach", new \StdClass(), "binary", base64_encode('attachment') + ], + [ + "request-status", + new \StdClass(), + "text", + ["2.0", "Success"], + ], + [ + "request-status", + new \StdClass(), + "text", + ["3.7", "Invalid Calendar User", "ATTENDEE:mailto:jsmith@example.org"], + ], + [ + 'dtend', + new \StdClass(), + "date-time", + "2015-01-08T13:30:00", + ], + ], + [], + ] + ], + ]; + + $this->assertEquals($expected, $cal->jsonSerialize()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/JCardTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/JCardTest.php new file mode 100644 index 00000000000..87ff2fff885 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/JCardTest.php @@ -0,0 +1,195 @@ +<?php + +namespace Sabre\VObject; + +class JCardTest extends \PHPUnit_Framework_TestCase { + + function testToJCard() { + + $card = new Component\VCard([ + "VERSION" => "4.0", + "UID" => "foo", + "BDAY" => "19850407", + "REV" => "19951031T222710Z", + "LANG" => "nl", + "N" => ["Last", "First", "Middle", "", ""], + "item1.TEL" => "+1 555 123456", + "item1.X-AB-LABEL" => "Walkie Talkie", + "ADR" => [ + "", + "", + ["My Street", "Left Side", "Second Shack"], + "Hometown", + "PA", + "18252", + "U.S.A", + ], + ]); + + $card->add('BDAY', '1979-12-25', ['VALUE' => 'DATE', 'X-PARAM' => [1, 2]]); + $card->add('BDAY', '1979-12-25T02:00:00', ['VALUE' => 'DATE-TIME']); + + + $card->add('X-TRUNCATED', '--1225', ['VALUE' => 'DATE']); + $card->add('X-TIME-LOCAL', '123000', ['VALUE' => 'TIME']); + $card->add('X-TIME-UTC', '12:30:00Z', ['VALUE' => 'TIME']); + $card->add('X-TIME-OFFSET', '12:30:00-08:00', ['VALUE' => 'TIME']); + $card->add('X-TIME-REDUCED', '23', ['VALUE' => 'TIME']); + $card->add('X-TIME-TRUNCATED', '--30', ['VALUE' => 'TIME']); + + $card->add('X-KARMA-POINTS', '42', ['VALUE' => 'INTEGER']); + $card->add('X-GRADE', '1.3', ['VALUE' => 'FLOAT']); + + $card->add('TZ', '-0500', ['VALUE' => 'UTC-OFFSET']); + + $expected = [ + "vcard", + [ + [ + "version", + new \StdClass(), + "text", + "4.0" + ], + [ + "prodid", + new \StdClass(), + "text", + "-//Sabre//Sabre VObject " . Version::VERSION . "//EN", + ], + [ + "uid", + new \StdClass(), + "text", + "foo", + ], + [ + "bday", + new \StdClass(), + "date-and-or-time", + "1985-04-07", + ], + [ + "bday", + (object)[ + 'x-param' => [1,2], + ], + "date", + "1979-12-25", + ], + [ + "bday", + new \StdClass(), + "date-time", + "1979-12-25T02:00:00", + ], + [ + "rev", + new \StdClass(), + "timestamp", + "1995-10-31T22:27:10Z", + ], + [ + "lang", + new \StdClass(), + "language-tag", + "nl", + ], + [ + "n", + new \StdClass(), + "text", + ["Last", "First", "Middle", "", ""], + ], + [ + "tel", + (object)[ + "group" => "item1", + ], + "text", + "+1 555 123456", + ], + [ + "x-ab-label", + (object)[ + "group" => "item1", + ], + "unknown", + "Walkie Talkie", + ], + [ + "adr", + new \StdClass(), + "text", + [ + "", + "", + ["My Street", "Left Side", "Second Shack"], + "Hometown", + "PA", + "18252", + "U.S.A", + ], + ], + [ + "x-truncated", + new \StdClass(), + "date", + "--12-25", + ], + [ + "x-time-local", + new \StdClass(), + "time", + "12:30:00" + ], + [ + "x-time-utc", + new \StdClass(), + "time", + "12:30:00Z" + ], + [ + "x-time-offset", + new \StdClass(), + "time", + "12:30:00-08:00" + ], + [ + "x-time-reduced", + new \StdClass(), + "time", + "23" + ], + [ + "x-time-truncated", + new \StdClass(), + "time", + "--30" + ], + [ + "x-karma-points", + new \StdClass(), + "integer", + 42 + ], + [ + "x-grade", + new \StdClass(), + "float", + 1.3 + ], + [ + "tz", + new \StdClass(), + "utc-offset", + "-05:00", + ], + ], + ]; + + $this->assertEquals($expected, $card->jsonSerialize()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/LineFoldingIssueTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/LineFoldingIssueTest.php new file mode 100644 index 00000000000..47fef1c11e7 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/LineFoldingIssueTest.php @@ -0,0 +1,23 @@ +<?php + +namespace Sabre\VObject; + +class LineFoldingIssueTest extends \PHPUnit_Framework_TestCase { + + function testRead() { + + $event = <<<ICS +BEGIN:VCALENDAR\r +BEGIN:VEVENT\r +DESCRIPTION:TEST\\n\\n \\n\\nTEST\\n\\n \\n\\nTEST\\n\\n \\n\\nTEST\\n\\nTEST\\nTEST, TEST\r +END:VEVENT\r +END:VCALENDAR\r + +ICS; + + $obj = Reader::read($event); + $this->assertEquals($event, $obj->serialize()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/ParameterTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ParameterTest.php new file mode 100644 index 00000000000..e4f973115fb --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ParameterTest.php @@ -0,0 +1,135 @@ +<?php + +namespace Sabre\VObject; + +class ParameterTest extends \PHPUnit_Framework_TestCase { + + function testSetup() { + + $cal = new Component\VCalendar(); + + $param = new Parameter($cal, 'name', 'value'); + $this->assertEquals('NAME', $param->name); + $this->assertEquals('value', $param->getValue()); + + } + + function testSetupNameLess() { + + $card = new Component\VCard(); + + $param = new Parameter($card, null, 'URL'); + $this->assertEquals('VALUE', $param->name); + $this->assertEquals('URL', $param->getValue()); + $this->assertTrue($param->noName); + + } + + function testModify() { + + $cal = new Component\VCalendar(); + + $param = new Parameter($cal, 'name', null); + $param->addValue(1); + $this->assertEquals([1], $param->getParts()); + + $param->setParts([1, 2]); + $this->assertEquals([1, 2], $param->getParts()); + + $param->addValue(3); + $this->assertEquals([1, 2, 3], $param->getParts()); + + $param->setValue(4); + $param->addValue(5); + $this->assertEquals([4, 5], $param->getParts()); + + } + + function testCastToString() { + + $cal = new Component\VCalendar(); + $param = new Parameter($cal, 'name', 'value'); + $this->assertEquals('value', $param->__toString()); + $this->assertEquals('value', (string)$param); + + } + + function testCastNullToString() { + + $cal = new Component\VCalendar(); + $param = new Parameter($cal, 'name', null); + $this->assertEquals('', $param->__toString()); + $this->assertEquals('', (string)$param); + + } + + function testSerialize() { + + $cal = new Component\VCalendar(); + $param = new Parameter($cal, 'name', 'value'); + $this->assertEquals('NAME=value', $param->serialize()); + + } + + function testSerializeEmpty() { + + $cal = new Component\VCalendar(); + $param = new Parameter($cal, 'name', null); + $this->assertEquals('NAME=', $param->serialize()); + + } + + function testSerializeComplex() { + + $cal = new Component\VCalendar(); + $param = new Parameter($cal, 'name', ["val1", "val2;", "val3^", "val4\n", "val5\""]); + $this->assertEquals('NAME=val1,"val2;","val3^^","val4^n","val5^\'"', $param->serialize()); + + } + + /** + * iCal 7.0 (OSX 10.9) has major issues with the EMAIL property, when the + * value contains a plus sign, and it's not quoted. + * + * So we specifically added support for that. + */ + function testSerializePlusSign() { + + $cal = new Component\VCalendar(); + $param = new Parameter($cal, 'EMAIL', "user+something@example.org"); + $this->assertEquals('EMAIL="user+something@example.org"', $param->serialize()); + + } + + function testIterate() { + + $cal = new Component\VCalendar(); + + $param = new Parameter($cal, 'name', [1, 2, 3, 4]); + $result = []; + + foreach ($param as $value) { + $result[] = $value; + } + + $this->assertEquals([1, 2, 3, 4], $result); + + } + + function testSerializeColon() { + + $cal = new Component\VCalendar(); + $param = new Parameter($cal, 'name', 'va:lue'); + $this->assertEquals('NAME="va:lue"', $param->serialize()); + + } + + function testSerializeSemiColon() { + + $cal = new Component\VCalendar(); + $param = new Parameter($cal, 'name', 'va;lue'); + $this->assertEquals('NAME="va;lue"', $param->serialize()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Parser/JsonTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Parser/JsonTest.php new file mode 100644 index 00000000000..c8725dfd06a --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Parser/JsonTest.php @@ -0,0 +1,395 @@ +<?php + +namespace Sabre\VObject\Parser; + +use + Sabre\VObject; + +class JsonTest extends \PHPUnit_Framework_TestCase { + + function testRoundTripJCard() { + + $input = [ + "vcard", + [ + [ + "version", + new \StdClass(), + "text", + "4.0" + ], + [ + "prodid", + new \StdClass(), + "text", + "-//Sabre//Sabre VObject " . VObject\Version::VERSION . "//EN", + ], + [ + "uid", + new \StdClass(), + "text", + "foo", + ], + [ + "bday", + new \StdClass(), + "date-and-or-time", + "1985-04-07", + ], + [ + "bday", + (object)[ + 'x-param' => [1,2], + ], + "date", + "1979-12-25", + ], + [ + "bday", + new \StdClass(), + "date-time", + "1979-12-25T02:00:00", + ], + [ + "rev", + new \StdClass(), + "timestamp", + "1995-10-31T22:27:10Z", + ], + [ + "lang", + new \StdClass(), + "language-tag", + "nl", + ], + [ + "n", + new \StdClass(), + "text", + ["Last", "First", "Middle", "", ""], + ], + [ + "tel", + (object)[ + "group" => "item1", + ], + "text", + "+1 555 123456", + ], + [ + "x-ab-label", + (object)[ + "group" => "item1", + ], + "unknown", + "Walkie Talkie", + ], + [ + "adr", + new \StdClass(), + "text", + [ + "", + "", + ["My Street", "Left Side", "Second Shack"], + "Hometown", + "PA", + "18252", + "U.S.A", + ], + ], + + [ + "x-truncated", + new \StdClass(), + "date", + "--12-25", + ], + [ + "x-time-local", + new \StdClass(), + "time", + "12:30:00" + ], + [ + "x-time-utc", + new \StdClass(), + "time", + "12:30:00Z" + ], + [ + "x-time-offset", + new \StdClass(), + "time", + "12:30:00-08:00" + ], + [ + "x-time-reduced", + new \StdClass(), + "time", + "23" + ], + [ + "x-time-truncated", + new \StdClass(), + "time", + "--30" + ], + [ + "x-karma-points", + new \StdClass(), + "integer", + 42 + ], + [ + "x-grade", + new \StdClass(), + "float", + 1.3 + ], + [ + "tz", + new \StdClass(), + "utc-offset", + "-05:00", + ], + ], + ]; + + $parser = new Json(json_encode($input)); + $vobj = $parser->parse(); + + $version = VObject\Version::VERSION; + + $result = $vobj->serialize(); + $expected = <<<VCF +BEGIN:VCARD +VERSION:4.0 +PRODID:-//Sabre//Sabre VObject $version//EN +UID:foo +BDAY:1985-04-07 +BDAY;X-PARAM=1,2;VALUE=DATE:1979-12-25 +BDAY;VALUE=DATE-TIME:1979-12-25T02:00:00 +REV:1995-10-31T22:27:10Z +LANG:nl +N:Last;First;Middle;; +item1.TEL:+1 555 123456 +item1.X-AB-LABEL:Walkie Talkie +ADR:;;My Street,Left Side,Second Shack;Hometown;PA;18252;U.S.A +X-TRUNCATED;VALUE=DATE:--12-25 +X-TIME-LOCAL;VALUE=TIME:123000 +X-TIME-UTC;VALUE=TIME:123000Z +X-TIME-OFFSET;VALUE=TIME:123000-0800 +X-TIME-REDUCED;VALUE=TIME:23 +X-TIME-TRUNCATED;VALUE=TIME:--30 +X-KARMA-POINTS;VALUE=INTEGER:42 +X-GRADE;VALUE=FLOAT:1.3 +TZ;VALUE=UTC-OFFSET:-0500 +END:VCARD + +VCF; + $this->assertEquals($expected, str_replace("\r", "", $result)); + + $this->assertEquals( + $input, + $vobj->jsonSerialize() + ); + + } + + function testRoundTripJCal() { + + $input = [ + "vcalendar", + [ + [ + "version", + new \StdClass(), + "text", + "2.0" + ], + [ + "prodid", + new \StdClass(), + "text", + "-//Sabre//Sabre VObject " . VObject\Version::VERSION . "//EN", + ], + [ + "calscale", + new \StdClass(), + "text", + "GREGORIAN" + ], + ], + [ + ["vevent", + [ + [ + "uid", new \StdClass(), "text", "foo", + ], + [ + "dtstart", new \StdClass(), "date", "2013-05-26", + ], + [ + "duration", new \StdClass(), "duration", "P1D", + ], + [ + "categories", new \StdClass(), "text", "home", "testing", + ], + [ + "created", new \StdClass(), "date-time", "2013-05-26T18:10:00Z", + ], + [ + "attach", new \StdClass(), "binary", base64_encode('attachment') + ], + [ + "attendee", new \StdClass(), "cal-address", "mailto:armin@example.org", + ], + [ + "attendee", + (object)[ + "cn" => "Dominik", + "partstat" => "DECLINED", + ], + "cal-address", + "mailto:dominik@example.org" + ], + [ + "geo", new \StdClass(), "float", [51.96668, 7.61876], + ], + [ + "sequence", new \StdClass(), "integer", 5 + ], + [ + "freebusy", new \StdClass(), "period", ["2013-05-26T21:02:13", "PT1H"], ["2013-06-26T12:00:00", "2013-06-26T13:00:00"], + ], + [ + "url", new \StdClass(), "uri", "http://example.org/", + ], + [ + "tzoffsetfrom", new \StdClass(), "utc-offset", "+05:00", + ], + [ + "rrule", new \StdClass(), "recur", [ + 'freq' => 'WEEKLY', + 'byday' => ['MO', 'TU'], + ], + ], + [ + "x-bool", new \StdClass(), "boolean", true + ], + [ + "x-time", new \StdClass(), "time", "08:00:00", + ], + [ + "request-status", + new \StdClass(), + "text", + ["2.0", "Success"], + ], + [ + "request-status", + new \StdClass(), + "text", + ["3.7", "Invalid Calendar User", "ATTENDEE:mailto:jsmith@example.org"], + ], + ], + [ + ["valarm", + [ + [ + "action", new \StdClass(), "text", "DISPLAY", + ], + ], + [], + ], + ], + ] + ], + ]; + + $parser = new Json(json_encode($input)); + $vobj = $parser->parse(); + $result = $vobj->serialize(); + + $version = VObject\Version::VERSION; + + $expected = <<<VCF +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject $version//EN +CALSCALE:GREGORIAN +BEGIN:VEVENT +UID:foo +DTSTART;VALUE=DATE:20130526 +DURATION:P1D +CATEGORIES:home,testing +CREATED:20130526T181000Z +ATTACH;VALUE=BINARY:YXR0YWNobWVudA== +ATTENDEE:mailto:armin@example.org +ATTENDEE;CN=Dominik;PARTSTAT=DECLINED:mailto:dominik@example.org +GEO:51.96668;7.61876 +SEQUENCE:5 +FREEBUSY:20130526T210213/PT1H,20130626T120000/20130626T130000 +URL;VALUE=URI:http://example.org/ +TZOFFSETFROM:+0500 +RRULE:FREQ=WEEKLY;BYDAY=MO,TU +X-BOOL;VALUE=BOOLEAN:TRUE +X-TIME;VALUE=TIME:080000 +REQUEST-STATUS:2.0;Success +REQUEST-STATUS:3.7;Invalid Calendar User;ATTENDEE:mailto:jsmith@example.org +BEGIN:VALARM +ACTION:DISPLAY +END:VALARM +END:VEVENT +END:VCALENDAR + +VCF; + $this->assertEquals($expected, str_replace("\r", "", $result)); + + $this->assertEquals( + $input, + $vobj->jsonSerialize() + ); + + } + + function testParseStreamArg() { + + $input = [ + "vcard", + [ + [ + "FN", new \StdClass(), 'text', "foo", + ], + ], + ]; + + $stream = fopen('php://memory', 'r+'); + fwrite($stream, json_encode($input)); + rewind($stream); + + $result = VObject\Reader::readJson($stream, 0); + $this->assertEquals('foo', $result->FN->getValue()); + + } + + /** + * @expectedException \Sabre\VObject\ParseException + */ + function testParseInvalidData() { + + $json = new Json(); + $input = [ + "vlist", + [ + [ + "FN", new \StdClass(), 'text', "foo", + ], + ], + ]; + + $json->parse(json_encode($input), 0); + + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Parser/MimeDirTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Parser/MimeDirTest.php new file mode 100644 index 00000000000..63219dac2b9 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Parser/MimeDirTest.php @@ -0,0 +1,143 @@ +<?php + +namespace Sabre\VObject\Parser; + +/** + * Note that most MimeDir related tests can actually be found in the ReaderTest + * class one level up. + */ +class MimeDirTest extends \PHPUnit_Framework_TestCase { + + /** + * @expectedException \Sabre\VObject\ParseException + */ + function testParseError() { + + $mimeDir = new MimeDir(); + $mimeDir->parse(fopen(__FILE__, 'a')); + + } + + function testDecodeLatin1() { + + $vcard = <<<VCF +BEGIN:VCARD +VERSION:3.0 +FN:umlaut u - \xFC +END:VCARD\n +VCF; + + $mimeDir = new MimeDir(); + $mimeDir->setCharset('ISO-8859-1'); + $vcard = $mimeDir->parse($vcard); + $this->assertEquals("umlaut u - \xC3\xBC", $vcard->FN->getValue()); + + } + + function testDecodeInlineLatin1() { + + $vcard = <<<VCF +BEGIN:VCARD +VERSION:2.1 +FN;CHARSET=ISO-8859-1:umlaut u - \xFC +END:VCARD\n +VCF; + + $mimeDir = new MimeDir(); + $vcard = $mimeDir->parse($vcard); + $this->assertEquals("umlaut u - \xC3\xBC", $vcard->FN->getValue()); + + } + + function testIgnoreCharsetVCard30() { + + $vcard = <<<VCF +BEGIN:VCARD +VERSION:3.0 +FN;CHARSET=unknown:foo-bar - \xFC +END:VCARD\n +VCF; + + $mimeDir = new MimeDir(); + $vcard = $mimeDir->parse($vcard); + $this->assertEquals("foo-bar - \xFC", $vcard->FN->getValue()); + + } + + function testDontDecodeLatin1() { + + $vcard = <<<VCF +BEGIN:VCARD +VERSION:4.0 +FN:umlaut u - \xFC +END:VCARD\n +VCF; + + $mimeDir = new MimeDir(); + $vcard = $mimeDir->parse($vcard); + // This basically tests that we don't touch the input string if + // the encoding was set to UTF-8. The result is actually invalid + // and the validator should report this, but it tests effectively + // that we pass through the string byte-by-byte. + $this->assertEquals("umlaut u - \xFC", $vcard->FN->getValue()); + + } + + /** + * @expectedException \InvalidArgumentException + */ + function testDecodeUnsupportedCharset() { + + $mimeDir = new MimeDir(); + $mimeDir->setCharset('foobar'); + + } + + /** + * @expectedException \Sabre\VObject\ParseException + */ + function testDecodeUnsupportedInlineCharset() { + + $vcard = <<<VCF +BEGIN:VCARD +VERSION:2.1 +FN;CHARSET=foobar:nothing +END:VCARD\n +VCF; + + $mimeDir = new MimeDir(); + $mimeDir->parse($vcard); + + } + + function testDecodeWindows1252() { + + $vcard = <<<VCF +BEGIN:VCARD +VERSION:3.0 +FN:Euro \x80 +END:VCARD\n +VCF; + + $mimeDir = new MimeDir(); + $mimeDir->setCharset('Windows-1252'); + $vcard = $mimeDir->parse($vcard); + $this->assertEquals("Euro \xE2\x82\xAC", $vcard->FN->getValue()); + + } + + function testDecodeWindows1252Inline() { + + $vcard = <<<VCF +BEGIN:VCARD +VERSION:2.1 +FN;CHARSET=Windows-1252:Euro \x80 +END:VCARD\n +VCF; + + $mimeDir = new MimeDir(); + $vcard = $mimeDir->parse($vcard); + $this->assertEquals("Euro \xE2\x82\xAC", $vcard->FN->getValue()); + + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Parser/QuotedPrintableTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Parser/QuotedPrintableTest.php new file mode 100644 index 00000000000..f40d6a677f4 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Parser/QuotedPrintableTest.php @@ -0,0 +1,108 @@ +<?php + +namespace Sabre\VObject\Parser; + +use + Sabre\VObject\Reader; + +class QuotedPrintableTest extends \PHPUnit_Framework_TestCase { + + function testReadQuotedPrintableSimple() { + + $data = "BEGIN:VCARD\r\nLABEL;ENCODING=QUOTED-PRINTABLE:Aach=65n\r\nEND:VCARD"; + + $result = Reader::read($data); + + $this->assertInstanceOf('Sabre\\VObject\\Component', $result); + $this->assertEquals('VCARD', $result->name); + $this->assertEquals(1, count($result->children())); + $this->assertEquals("Aachen", $this->getPropertyValue($result->LABEL)); + + } + + function testReadQuotedPrintableNewlineSoft() { + + $data = "BEGIN:VCARD\r\nLABEL;ENCODING=QUOTED-PRINTABLE:Aa=\r\n ch=\r\n en\r\nEND:VCARD"; + $result = Reader::read($data); + + $this->assertInstanceOf('Sabre\\VObject\\Component', $result); + $this->assertEquals('VCARD', $result->name); + $this->assertEquals(1, count($result->children())); + $this->assertEquals("Aachen", $this->getPropertyValue($result->LABEL)); + + } + + function testReadQuotedPrintableNewlineHard() { + + $data = "BEGIN:VCARD\r\nLABEL;ENCODING=QUOTED-PRINTABLE:Aachen=0D=0A=\r\n Germany\r\nEND:VCARD"; + $result = Reader::read($data); + + $this->assertInstanceOf('Sabre\\VObject\\Component', $result); + $this->assertEquals('VCARD', $result->name); + $this->assertEquals(1, count($result->children())); + $this->assertEquals("Aachen\r\nGermany", $this->getPropertyValue($result->LABEL)); + + + } + + function testReadQuotedPrintableCompatibilityMS() { + + $data = "BEGIN:VCARD\r\nLABEL;ENCODING=QUOTED-PRINTABLE:Aachen=0D=0A=\r\nDeutschland:okay\r\nEND:VCARD"; + $result = Reader::read($data, Reader::OPTION_FORGIVING); + + $this->assertInstanceOf('Sabre\\VObject\\Component', $result); + $this->assertEquals('VCARD', $result->name); + $this->assertEquals(1, count($result->children())); + $this->assertEquals("Aachen\r\nDeutschland:okay", $this->getPropertyValue($result->LABEL)); + + } + + function testReadQuotesPrintableCompoundValues() { + + $data = <<<VCF +BEGIN:VCARD +VERSION:2.1 +N:Doe;John;;; +FN:John Doe +ADR;WORK;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:;;M=C3=BCnster = +Str. 1;M=C3=BCnster;;48143;Deutschland +END:VCARD +VCF; + + $result = Reader::read($data, Reader::OPTION_FORGIVING); + $this->assertEquals([ + '', '', 'Münster Str. 1', 'Münster', '', '48143', 'Deutschland' + ], $result->ADR->getParts()); + + + } + + private function getPropertyValue(\Sabre\VObject\Property $property) { + + return (string)$property; + + /* + $param = $property['encoding']; + if ($param !== null) { + $encoding = strtoupper((string)$param); + if ($encoding === 'QUOTED-PRINTABLE') { + $value = quoted_printable_decode($value); + } else { + throw new Exception(); + } + } + + $param = $property['charset']; + if ($param !== null) { + $charset = strtoupper((string)$param); + if ($charset !== 'UTF-8') { + $value = mb_convert_encoding($value, 'UTF-8', $charset); + } + } else { + $value = StringUtil::convertToUTF8($value); + } + + return $value; + */ + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Parser/XmlTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Parser/XmlTest.php new file mode 100644 index 00000000000..b8ba67d1719 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Parser/XmlTest.php @@ -0,0 +1,2893 @@ +<?php + +namespace Sabre\VObject\Parser; + +use Sabre\VObject; + +class XmlTest extends \PHPUnit_Framework_TestCase { + + use VObject\PHPUnitAssertions; + + function testRFC6321Example1() { + + $this->assertXMLEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <calscale> + <text>GREGORIAN</text> + </calscale> + <prodid> + <text>-//Example Inc.//Example Calendar//EN</text> + </prodid> + <version> + <text>2.0</text> + </version> + </properties> + <components> + <vevent> + <properties> + <dtstamp> + <date-time>2008-02-05T19:12:24Z</date-time> + </dtstamp> + <dtstart> + <date>2008-10-06</date> + </dtstart> + <summary> + <text>Planning meeting</text> + </summary> + <uid> + <text>4088E990AD89CB3DBB484909</text> + </uid> + </properties> + </vevent> + </components> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + // VERSION comes first because this is required by vCard 4.0. + 'VERSION:2.0' . "\n" . + 'CALSCALE:GREGORIAN' . "\n" . + 'PRODID:-//Example Inc.//Example Calendar//EN' . "\n" . + 'BEGIN:VEVENT' . "\n" . + 'DTSTAMP:20080205T191224Z' . "\n" . + 'DTSTART;VALUE=DATE:20081006' . "\n" . + 'SUMMARY:Planning meeting' . "\n" . + 'UID:4088E990AD89CB3DBB484909' . "\n" . + 'END:VEVENT' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + } + + function testRFC6321Example2() { + + $xml = <<<XML +<?xml version="1.0" encoding="UTF-8" ?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <prodid> + <text>-//Example Inc.//Example Client//EN</text> + </prodid> + <version> + <text>2.0</text> + </version> + </properties> + <components> + <vtimezone> + <properties> + <last-modified> + <date-time>2004-01-10T03:28:45Z</date-time> + </last-modified> + <tzid><text>US/Eastern</text></tzid> + </properties> + <components> + <daylight> + <properties> + <dtstart> + <date-time>2000-04-04T02:00:00</date-time> + </dtstart> + <rrule> + <recur> + <freq>YEARLY</freq> + <byday>1SU</byday> + <bymonth>4</bymonth> + </recur> + </rrule> + <tzname> + <text>EDT</text> + </tzname> + <tzoffsetfrom> + <utc-offset>-05:00</utc-offset> + </tzoffsetfrom> + <tzoffsetto> + <utc-offset>-04:00</utc-offset> + </tzoffsetto> + </properties> + </daylight> + <standard> + <properties> + <dtstart> + <date-time>2000-10-26T02:00:00</date-time> + </dtstart> + <rrule> + <recur> + <freq>YEARLY</freq> + <byday>-1SU</byday> + <bymonth>10</bymonth> + </recur> + </rrule> + <tzname> + <text>EST</text> + </tzname> + <tzoffsetfrom> + <utc-offset>-04:00</utc-offset> + </tzoffsetfrom> + <tzoffsetto> + <utc-offset>-05:00</utc-offset> + </tzoffsetto> + </properties> + </standard> + </components> + </vtimezone> + <vevent> + <properties> + <dtstamp> + <date-time>2006-02-06T00:11:21Z</date-time> + </dtstamp> + <dtstart> + <parameters> + <tzid><text>US/Eastern</text></tzid> + </parameters> + <date-time>2006-01-02T12:00:00</date-time> + </dtstart> + <duration> + <duration>PT1H</duration> + </duration> + <rrule> + <recur> + <freq>DAILY</freq> + <count>5</count> + </recur> + </rrule> + <rdate> + <parameters> + <tzid><text>US/Eastern</text></tzid> + </parameters> + <period> + <start>2006-01-02T15:00:00</start> + <duration>PT2H</duration> + </period> + </rdate> + <summary> + <text>Event #2</text> + </summary> + <description> + <text>We are having a meeting all this week at 12 +pm for one hour, with an additional meeting on the first day +2 hours long. Please bring your own lunch for the 12 pm +meetings.</text> + </description> + <uid> + <text>00959BC664CA650E933C892C@example.com</text> + </uid> + </properties> + </vevent> + <vevent> + <properties> + <dtstamp> + <date-time>2006-02-06T00:11:21Z</date-time> + </dtstamp> + <dtstart> + <parameters> + <tzid><text>US/Eastern</text></tzid> + </parameters> + <date-time>2006-01-04T14:00:00</date-time> + </dtstart> + <duration> + <duration>PT1H</duration> + </duration> + <recurrence-id> + <parameters> + <tzid><text>US/Eastern</text></tzid> + </parameters> + <date-time>2006-01-04T12:00:00</date-time> + </recurrence-id> + <summary> + <text>Event #2 bis</text> + </summary> + <uid> + <text>00959BC664CA650E933C892C@example.com</text> + </uid> + </properties> + </vevent> + </components> + </vcalendar> +</icalendar> +XML; + + $component = VObject\Reader::readXML($xml); + $this->assertVObjectEqualsVObject( + 'BEGIN:VCALENDAR' . "\n" . + 'VERSION:2.0' . "\n" . + 'PRODID:-//Example Inc.//Example Client//EN' . "\n" . + 'BEGIN:VTIMEZONE' . "\n" . + 'LAST-MODIFIED:20040110T032845Z' . "\n" . + 'TZID:US/Eastern' . "\n" . + 'BEGIN:DAYLIGHT' . "\n" . + 'DTSTART:20000404T020000' . "\n" . + 'RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4' . "\n" . + 'TZNAME:EDT' . "\n" . + 'TZOFFSETFROM:-0500' . "\n" . + 'TZOFFSETTO:-0400' . "\n" . + 'END:DAYLIGHT' . "\n" . + 'BEGIN:STANDARD' . "\n" . + 'DTSTART:20001026T020000' . "\n" . + 'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10' . "\n" . + 'TZNAME:EST' . "\n" . + 'TZOFFSETFROM:-0400' . "\n" . + 'TZOFFSETTO:-0500' . "\n" . + 'END:STANDARD' . "\n" . + 'END:VTIMEZONE' . "\n" . + 'BEGIN:VEVENT' . "\n" . + 'DTSTAMP:20060206T001121Z' . "\n" . + 'DTSTART;TZID=US/Eastern:20060102T120000' . "\n" . + 'DURATION:PT1H' . "\n" . + 'RRULE:FREQ=DAILY;COUNT=5' . "\n" . + 'RDATE;TZID=US/Eastern;VALUE=PERIOD:20060102T150000/PT2H' . "\n" . + 'SUMMARY:Event #2' . "\n" . + 'DESCRIPTION:We are having a meeting all this week at 12\npm for one hour\, ' . "\n" . + ' with an additional meeting on the first day\n2 hours long.\nPlease bring y' . "\n" . + ' our own lunch for the 12 pm\nmeetings.' . "\n" . + 'UID:00959BC664CA650E933C892C@example.com' . "\n" . + 'END:VEVENT' . "\n" . + 'BEGIN:VEVENT' . "\n" . + 'DTSTAMP:20060206T001121Z' . "\n" . + 'DTSTART;TZID=US/Eastern:20060104T140000' . "\n" . + 'DURATION:PT1H' . "\n" . + 'RECURRENCE-ID;TZID=US/Eastern:20060104T120000' . "\n" . + 'SUMMARY:Event #2 bis' . "\n" . + 'UID:00959BC664CA650E933C892C@example.com' . "\n" . + 'END:VEVENT' . "\n" . + 'END:VCALENDAR' . "\n", + VObject\Writer::write($component) + ); + + } + + /** + * iCalendar Stream. + */ + function testRFC6321Section3_2() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar/> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'END:VCALENDAR' . "\n" + ); + } + + /** + * All components exist. + */ + function testRFC6321Section3_3() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <components> + <vtimezone/> + <vevent/> + <vtodo/> + <vjournal/> + <vfreebusy/> + <standard/> + <daylight/> + <valarm/> + </components> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'BEGIN:VTIMEZONE' . "\n" . + 'END:VTIMEZONE' . "\n" . + 'BEGIN:VEVENT' . "\n" . + 'END:VEVENT' . "\n" . + 'BEGIN:VTODO' . "\n" . + 'END:VTODO' . "\n" . + 'BEGIN:VJOURNAL' . "\n" . + 'END:VJOURNAL' . "\n" . + 'BEGIN:VFREEBUSY' . "\n" . + 'END:VFREEBUSY' . "\n" . + 'BEGIN:STANDARD' . "\n" . + 'END:STANDARD' . "\n" . + 'BEGIN:DAYLIGHT' . "\n" . + 'END:DAYLIGHT' . "\n" . + 'BEGIN:VALARM' . "\n" . + 'END:VALARM' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + } + + /** + * Properties, Special Cases, GEO. + */ + function testRFC6321Section3_4_1_2() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <geo> + <latitude>37.386013</latitude> + <longitude>-122.082932</longitude> + </geo> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'GEO:37.386013;-122.082932' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + } + + /** + * Properties, Special Cases, REQUEST-STATUS. + */ + function testRFC6321Section3_4_1_3() { + + // Example 1 of RFC5545, Section 3.8.8.3. + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <request-status> + <code>2.0</code> + <description>Success</description> + </request-status> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'REQUEST-STATUS:2.0;Success' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + // Example 2 of RFC5545, Section 3.8.8.3. + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <request-status> + <code>3.1</code> + <description>Invalid property value</description> + <data>DTSTART:96-Apr-01</data> + </request-status> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'REQUEST-STATUS:3.1;Invalid property value;DTSTART:96-Apr-01' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + // Example 3 of RFC5545, Section 3.8.8.3. + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <request-status> + <code>2.8</code> + <description>Success, repeating event ignored. Scheduled as a single event.</description> + <data>RRULE:FREQ=WEEKLY;INTERVAL=2</data> + </request-status> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'REQUEST-STATUS:2.8;Success\, repeating event ignored. Scheduled as a single' . "\n" . + ' event.;RRULE:FREQ=WEEKLY\;INTERVAL=2' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + // Example 4 of RFC5545, Section 3.8.8.3. + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <request-status> + <code>4.1</code> + <description>Event conflict. Date-time is busy.</description> + </request-status> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'REQUEST-STATUS:4.1;Event conflict. Date-time is busy.' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + // Example 5 of RFC5545, Section 3.8.8.3. + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <request-status> + <code>3.7</code> + <description>Invalid calendar user</description> + <data>ATTENDEE:mailto:jsmith@example.com</data> + </request-status> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'REQUEST-STATUS:3.7;Invalid calendar user;ATTENDEE:mailto:jsmith@example.com' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + } + + /** + * Values, Binary. + */ + function testRFC6321Section3_6_1() { + + $this->assertXMLEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <attach> + <binary>SGVsbG8gV29ybGQh</binary> + </attach> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'ATTACH:SGVsbG8gV29ybGQh' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + // In vCard 4, BINARY no longer exists and is replaced by URI. + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <attach> + <uri>SGVsbG8gV29ybGQh</uri> + </attach> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'ATTACH:SGVsbG8gV29ybGQh' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + } + + /** + * Values, Boolean. + */ + function testRFC6321Section3_6_2() { + + $this->assertXMLEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <attendee> + <parameters> + <rsvp><boolean>true</boolean></rsvp> + </parameters> + <cal-address>mailto:cyrus@example.com</cal-address> + </attendee> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'ATTENDEE;RSVP=true:mailto:cyrus@example.com' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + } + + /** + * Values, Calendar User Address. + */ + function testRFC6321Section3_6_3() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <attendee> + <cal-address>mailto:cyrus@example.com</cal-address> + </attendee> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'ATTENDEE:mailto:cyrus@example.com' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + } + + /** + * Values, Date. + */ + function testRFC6321Section3_6_4() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <dtstart> + <date>2011-05-17</date> + </dtstart> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'DTSTART;VALUE=DATE:20110517' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + } + + /** + * Values, Date-Time. + */ + function testRFC6321Section3_6_5() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <dtstart> + <date-time>2011-05-17T12:00:00</date-time> + </dtstart> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'DTSTART:20110517T120000' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + } + + /** + * Values, Duration. + */ + function testRFC6321Section3_6_6() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <duration> + <duration>P1D</duration> + </duration> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'DURATION:P1D' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + } + + /** + * Values, Float. + */ + function testRFC6321Section3_6_7() { + + // GEO uses <float /> with a positive and a non-negative numbers. + $this->testRFC6321Section3_4_1_2(); + + } + + /** + * Values, Integer. + */ + function testRFC6321Section3_6_8() { + + $this->assertXMLEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <foo> + <integer>42</integer> + </foo> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'FOO:42' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + $this->assertXMLEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <foo> + <integer>-42</integer> + </foo> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'FOO:-42' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + } + + /** + * Values, Period of Time. + */ + function testRFC6321Section3_6_9() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <freebusy> + <period> + <start>2011-05-17T12:00:00</start> + <duration>P1H</duration> + </period> + </freebusy> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'FREEBUSY:20110517T120000/P1H' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <freebusy> + <period> + <start>2011-05-17T12:00:00</start> + <end>2012-05-17T12:00:00</end> + </period> + </freebusy> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'FREEBUSY:20110517T120000/20120517T120000' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + } + + /** + * Values, Recurrence Rule. + */ + function testRFC6321Section3_6_10() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <rrule> + <recur> + <freq>YEARLY</freq> + <count>5</count> + <byday>-1SU</byday> + <bymonth>10</bymonth> + </recur> + </rrule> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'RRULE:FREQ=YEARLY;COUNT=5;BYDAY=-1SU;BYMONTH=10' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + } + + /** + * Values, Text. + */ + function testRFC6321Section3_6_11() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <calscale> + <text>GREGORIAN</text> + </calscale> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'CALSCALE:GREGORIAN' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + } + + /** + * Values, Time. + */ + function testRFC6321Section3_6_12() { + + $this->assertXMLEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <foo> + <time>12:00:00</time> + </foo> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'FOO:120000' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + } + + /** + * Values, URI. + */ + function testRFC6321Section3_6_13() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <attach> + <uri>http://calendar.example.com</uri> + </attach> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'ATTACH:http://calendar.example.com' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + } + + /** + * Values, UTC Offset. + */ + function testRFC6321Section3_6_14() { + + // Example 1 of RFC5545, Section 3.3.14. + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <tzoffsetfrom> + <utc-offset>-05:00</utc-offset> + </tzoffsetfrom> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'TZOFFSETFROM:-0500' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + // Example 2 of RFC5545, Section 3.3.14. + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <tzoffsetfrom> + <utc-offset>+01:00</utc-offset> + </tzoffsetfrom> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'TZOFFSETFROM:+0100' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + } + + /** + * Handling Unrecognized Properties or Parameters. + */ + function testRFC6321Section5() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <x-property> + <unknown>20110512T120000Z</unknown> + </x-property> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'X-PROPERTY:20110512T120000Z' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <dtstart> + <parameters> + <x-param> + <text>PT30M</text> + </x-param> + </parameters> + <date-time>2011-05-12T13:00:00Z</date-time> + </dtstart> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'DTSTART;X-PARAM=PT30M:20110512T130000Z' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + } + + function testRDateWithDateTime() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <rdate> + <date-time>2008-02-05T19:12:24Z</date-time> + </rdate> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'RDATE:20080205T191224Z' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <rdate> + <date-time>2008-02-05T19:12:24Z</date-time> + <date-time>2009-02-05T19:12:24Z</date-time> + </rdate> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'RDATE:20080205T191224Z,20090205T191224Z' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + } + + function testRDateWithDate() { + + $this->assertXMLEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <rdate> + <date>2008-10-06</date> + </rdate> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'RDATE:20081006' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + $this->assertXMLEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <rdate> + <date>2008-10-06</date> + <date>2009-10-06</date> + <date>2010-10-06</date> + </rdate> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'RDATE:20081006,20091006,20101006' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + } + + function testRDateWithPeriod() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <rdate> + <parameters> + <tzid> + <text>US/Eastern</text> + </tzid> + </parameters> + <period> + <start>2006-01-02T15:00:00</start> + <duration>PT2H</duration> + </period> + </rdate> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'RDATE;TZID=US/Eastern;VALUE=PERIOD:20060102T150000/PT2H' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + $this->assertXMLEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <rdate> + <parameters> + <tzid> + <text>US/Eastern</text> + </tzid> + </parameters> + <period> + <start>2006-01-02T15:00:00</start> + <duration>PT2H</duration> + </period> + <period> + <start>2008-01-02T15:00:00</start> + <duration>PT1H</duration> + </period> + </rdate> + </properties> + </vcalendar> +</icalendar> +XML +, + 'BEGIN:VCALENDAR' . "\n" . + 'RDATE;TZID=US/Eastern;VALUE=PERIOD:20060102T150000/PT2H,20080102T150000/PT1' . "\n" . + ' H' . "\n" . + 'END:VCALENDAR' . "\n" + ); + + } + + /** + * Basic example. + */ + function testRFC6351Basic() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <fn> + <text>J. Doe</text> + </fn> + <n> + <surname>Doe</surname> + <given>J.</given> + <additional/> + <prefix/> + <suffix/> + </n> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'FN:J. Doe' . "\n" . + 'N:Doe;J.;;;' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Example 1. + */ + function testRFC6351Example1() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <fn> + <text>J. Doe</text> + </fn> + <n> + <surname>Doe</surname> + <given>J.</given> + <additional/> + <prefix/> + <suffix/> + </n> + <x-file> + <parameters> + <mediatype> + <text>image/jpeg</text> + </mediatype> + </parameters> + <unknown>alien.jpg</unknown> + </x-file> + <x1:a href="http://www.example.com" xmlns:x1="http://www.w3.org/1999/xhtml">My web page!</x1:a> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'FN:J. Doe' . "\n" . + 'N:Doe;J.;;;' . "\n" . + 'X-FILE;MEDIATYPE=image/jpeg:alien.jpg' . "\n" . + 'XML:<a xmlns="http://www.w3.org/1999/xhtml" href="http://www.example.com">M' . "\n" . + ' y web page!</a>' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Design Considerations. + */ + function testRFC6351Section5() { + + $this->assertXMLEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <tel> + <parameters> + <type> + <text>voice</text> + <text>video</text> + </type> + </parameters> + <uri>tel:+1-555-555-555</uri> + </tel> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'TEL;TYPE="voice,video":tel:+1-555-555-555' . "\n" . + 'END:VCARD' . "\n" + ); + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <tel> + <parameters> + <type> + <text>voice</text> + <text>video</text> + </type> + </parameters> + <text>tel:+1-555-555-555</text> + </tel> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'TEL;TYPE="voice,video":tel:+1-555-555-555' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Design Considerations. + */ + function testRFC6351Section5Group() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <tel> + <text>tel:+1-555-555-556</text> + </tel> + <group name="contact"> + <tel> + <text>tel:+1-555-555-555</text> + </tel> + <fn> + <text>Gordon</text> + </fn> + </group> + <group name="media"> + <fn> + <text>Gordon</text> + </fn> + </group> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'TEL:tel:+1-555-555-556' . "\n" . + 'contact.TEL:tel:+1-555-555-555' . "\n" . + 'contact.FN:Gordon' . "\n" . + 'media.FN:Gordon' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Extensibility. + */ + function testRFC6351Section5_1_NoNamespace() { + + $this->assertXMLEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <x-my-prop> + <parameters> + <pref> + <integer>1</integer> + </pref> + </parameters> + <text>value goes here</text> + </x-my-prop> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'X-MY-PROP;PREF=1:value goes here' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Section 4.3.1 of Relax NG Schema: value-date. + */ + function testRFC6351ValueDateWithYearMonthDay() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <bday> + <date-and-or-time>20150128</date-and-or-time> + </bday> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'BDAY:20150128' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Section 4.3.1 of Relax NG Schema: value-date. + */ + function testRFC6351ValueDateWithYearMonth() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <bday> + <date-and-or-time>2015-01</date-and-or-time> + </bday> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'BDAY:2015-01' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Section 4.3.1 of Relax NG Schema: value-date. + */ + function testRFC6351ValueDateWithMonth() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <bday> + <date-and-or-time>--01</date-and-or-time> + </bday> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'BDAY:--01' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Section 4.3.1 of Relax NG Schema: value-date. + */ + function testRFC6351ValueDateWithMonthDay() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <bday> + <date-and-or-time>--0128</date-and-or-time> + </bday> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'BDAY:--0128' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Section 4.3.1 of Relax NG Schema: value-date. + */ + function testRFC6351ValueDateWithDay() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <bday> + <date-and-or-time>---28</date-and-or-time> + </bday> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'BDAY:---28' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Section 4.3.2 of Relax NG Schema: value-time. + */ + function testRFC6351ValueTimeWithHour() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <bday> + <date-and-or-time>13</date-and-or-time> + </bday> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'BDAY:13' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Section 4.3.2 of Relax NG Schema: value-time. + */ + function testRFC6351ValueTimeWithHourMinute() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <bday> + <date-and-or-time>1353</date-and-or-time> + </bday> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'BDAY:1353' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Section 4.3.2 of Relax NG Schema: value-time. + */ + function testRFC6351ValueTimeWithHourMinuteSecond() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <bday> + <date-and-or-time>135301</date-and-or-time> + </bday> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'BDAY:135301' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Section 4.3.2 of Relax NG Schema: value-time. + */ + function testRFC6351ValueTimeWithMinute() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <bday> + <date-and-or-time>-53</date-and-or-time> + </bday> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'BDAY:-53' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Section 4.3.2 of Relax NG Schema: value-time. + */ + function testRFC6351ValueTimeWithMinuteSecond() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <bday> + <date-and-or-time>-5301</date-and-or-time> + </bday> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'BDAY:-5301' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Section 4.3.2 of Relax NG Schema: value-time. + */ + function testRFC6351ValueTimeWithSecond() { + + $this->assertTrue(true); + + /* + * According to the Relax NG Schema, there is a conflict between + * value-date and value-time. The --01 syntax can only match a + * value-date because of the higher priority set in + * value-date-and-or-time. So we basically skip this test. + * + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <bday> + <date-and-or-time>--01</date-and-or-time> + </bday> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'BDAY:--01' . "\n" . + 'END:VCARD' . "\n" + ); + */ + + } + + /** + * Section 4.3.2 of Relax NG Schema: value-time. + */ + function testRFC6351ValueTimeWithSecondZ() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <bday> + <date-and-or-time>--01Z</date-and-or-time> + </bday> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'BDAY:--01Z' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Section 4.3.2 of Relax NG Schema: value-time. + */ + function testRFC6351ValueTimeWithSecondTZ() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <bday> + <date-and-or-time>--01+1234</date-and-or-time> + </bday> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'BDAY:--01+1234' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Section 4.3.3 of Relax NG Schema: value-date-time. + */ + function testRFC6351ValueDateTimeWithYearMonthDayHour() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <bday> + <date-and-or-time>20150128T13</date-and-or-time> + </bday> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'BDAY:20150128T13' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Section 4.3.3 of Relax NG Schema: value-date-time. + */ + function testRFC6351ValueDateTimeWithMonthDayHour() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <bday> + <date-and-or-time>--0128T13</date-and-or-time> + </bday> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'BDAY:--0128T13' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Section 4.3.3 of Relax NG Schema: value-date-time. + */ + function testRFC6351ValueDateTimeWithDayHour() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <bday> + <date-and-or-time>---28T13</date-and-or-time> + </bday> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'BDAY:---28T13' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Section 4.3.3 of Relax NG Schema: value-date-time. + */ + function testRFC6351ValueDateTimeWithDayHourMinute() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <bday> + <date-and-or-time>---28T1353</date-and-or-time> + </bday> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'BDAY:---28T1353' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Section 4.3.3 of Relax NG Schema: value-date-time. + */ + function testRFC6351ValueDateTimeWithDayHourMinuteSecond() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <bday> + <date-and-or-time>---28T135301</date-and-or-time> + </bday> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'BDAY:---28T135301' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Section 4.3.3 of Relax NG Schema: value-date-time. + */ + function testRFC6351ValueDateTimeWithDayHourZ() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <bday> + <date-and-or-time>---28T13Z</date-and-or-time> + </bday> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'BDAY:---28T13Z' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Section 4.3.3 of Relax NG Schema: value-date-time. + */ + function testRFC6351ValueDateTimeWithDayHourTZ() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <bday> + <date-and-or-time>---28T13+1234</date-and-or-time> + </bday> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'BDAY:---28T13+1234' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: SOURCE. + */ + function testRFC6350Section6_1_3() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <source> + <uri>ldap://ldap.example.com/cn=Babs%20Jensen,%20o=Babsco,%20c=US</uri> + </source> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'SOURCE:ldap://ldap.example.com/cn=Babs%20Jensen\,%20o=Babsco\,%20c=US' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: KIND. + */ + function testRFC6350Section6_1_4() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <kind> + <text>individual</text> + </kind> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'KIND:individual' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: FN. + */ + function testRFC6350Section6_2_1() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <fn> + <text>Mr. John Q. Public, Esq.</text> + </fn> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'FN:Mr. John Q. Public\, Esq.' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: N. + */ + function testRFC6350Section6_2_2() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <n> + <surname>Stevenson</surname> + <given>John</given> + <additional>Philip,Paul</additional> + <prefix>Dr.</prefix> + <suffix>Jr.,M.D.,A.C.P.</suffix> + </n> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'N:Stevenson;John;Philip\,Paul;Dr.;Jr.\,M.D.\,A.C.P.' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: NICKNAME. + */ + function testRFC6350Section6_2_3() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <nickname> + <text>Jim</text> + <text>Jimmie</text> + </nickname> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'NICKNAME:Jim,Jimmie' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: PHOTO. + */ + function testRFC6350Section6_2_4() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <photo> + <uri>http://www.example.com/pub/photos/jqpublic.gif</uri> + </photo> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'PHOTO:http://www.example.com/pub/photos/jqpublic.gif' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + function testRFC6350Section6_2_5() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <bday> + <date-and-or-time>19531015T231000Z</date-and-or-time> + </bday> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'BDAY:19531015T231000Z' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + function testRFC6350Section6_2_6() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <anniversary> + <date-and-or-time>19960415</date-and-or-time> + </anniversary> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'ANNIVERSARY:19960415' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: GENDER. + */ + function testRFC6350Section6_2_7() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <gender> + <sex>Jim</sex> + <text>Jimmie</text> + </gender> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'GENDER:Jim;Jimmie' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: ADR. + */ + function testRFC6350Section6_3_1() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <adr> + <pobox/> + <ext/> + <street>123 Main Street</street> + <locality>Any Town</locality> + <region>CA</region> + <code>91921-1234</code> + <country>U.S.A.</country> + </adr> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'ADR:;;123 Main Street;Any Town;CA;91921-1234;U.S.A.' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: TEL. + */ + function testRFC6350Section6_4_1() { + + /** + * Quoting RFC: + * > Value type: By default, it is a single free-form text value (for + * > backward compatibility with vCard 3), but it SHOULD be reset to a + * > URI value. It is expected that the URI scheme will be "tel", as + * > specified in [RFC3966], but other schemes MAY be used. + * + * So first, we test xCard/URI to vCard/URI. + * Then, we test xCard/TEXT to vCard/TEXT to xCard/TEXT. + */ + $this->assertXMLEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <tel> + <parameters> + <type> + <text>home</text> + </type> + </parameters> + <uri>tel:+33-01-23-45-67</uri> + </tel> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'TEL;TYPE=home:tel:+33-01-23-45-67' . "\n" . + 'END:VCARD' . "\n" + ); + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <tel> + <parameters> + <type> + <text>home</text> + </type> + </parameters> + <text>tel:+33-01-23-45-67</text> + </tel> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'TEL;TYPE=home:tel:+33-01-23-45-67' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: EMAIL. + */ + function testRFC6350Section6_4_2() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <email> + <parameters> + <type> + <text>work</text> + </type> + </parameters> + <text>jqpublic@xyz.example.com</text> + </email> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'EMAIL;TYPE=work:jqpublic@xyz.example.com' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: IMPP. + */ + function testRFC6350Section6_4_3() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <impp> + <parameters> + <pref> + <text>1</text> + </pref> + </parameters> + <uri>xmpp:alice@example.com</uri> + </impp> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'IMPP;PREF=1:xmpp:alice@example.com' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: LANG. + */ + function testRFC6350Section6_4_4() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <lang> + <parameters> + <type> + <text>work</text> + </type> + <pref> + <text>2</text> + </pref> + </parameters> + <language-tag>en</language-tag> + </lang> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'LANG;TYPE=work;PREF=2:en' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: TZ. + */ + function testRFC6350Section6_5_1() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <tz> + <text>Raleigh/North America</text> + </tz> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'TZ:Raleigh/North America' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: GEO. + */ + function testRFC6350Section6_5_2() { + + $this->assertXMLEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <geo> + <uri>geo:37.386013,-122.082932</uri> + </geo> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'GEO:geo:37.386013\,-122.082932' . "\n" . + 'END:VCARD' . "\n" + ); + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <geo> + <text>geo:37.386013,-122.082932</text> + </geo> + </vcard> +</vcards> +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'GEO:geo:37.386013\,-122.082932' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: TITLE. + */ + function testRFC6350Section6_6_1() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<<<XML +<?xml version="1.0" encoding="UTF-8"?> +<vcards xmlns="urn:ietf:params:xml:ns:vcard-4.0"> + <vcard> + <title> + <text>Research Scientist</text> + + + +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'TITLE:Research Scientist' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: ROLE. + */ + function testRFC6350Section6_6_2() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<< + + + + Project Leader + + + +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'ROLE:Project Leader' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: LOGO. + */ + function testRFC6350Section6_6_3() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<< + + + + http://www.example.com/pub/logos/abccorp.jpg + + + +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'LOGO:http://www.example.com/pub/logos/abccorp.jpg' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: ORG. + */ + function testRFC6350Section6_6_4() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<< + + + + ABC, Inc. + North American Division + Marketing + + + +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'ORG:ABC\, Inc.;North American Division;Marketing' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: MEMBER. + */ + function testRFC6350Section6_6_5() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<< + + + + urn:uuid:03a0e51f-d1aa-4385-8a53-e29025acd8af + + + +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'MEMBER:urn:uuid:03a0e51f-d1aa-4385-8a53-e29025acd8af' . "\n" . + 'END:VCARD' . "\n" + ); + + $this->assertXMLReflexivelyEqualsToMimeDir( +<< + + + + mailto:subscriber1@example.com + + + xmpp:subscriber2@example.com + + + sip:subscriber3@example.com + + + tel:+1-418-555-5555 + + + +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'MEMBER:mailto:subscriber1@example.com' . "\n" . + 'MEMBER:xmpp:subscriber2@example.com' . "\n" . + 'MEMBER:sip:subscriber3@example.com' . "\n" . + 'MEMBER:tel:+1-418-555-5555' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: RELATED. + */ + function testRFC6350Section6_6_6() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<< + + + + + + friend + + + urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6 + + + +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'RELATED;TYPE=friend:urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: CATEGORIES. + */ + function testRFC6350Section6_7_1() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<< + + + + INTERNET + IETF + INDUSTRY + INFORMATION TECHNOLOGY + + + +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'CATEGORIES:INTERNET,IETF,INDUSTRY,INFORMATION TECHNOLOGY' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: NOTE. + */ + function testRFC6350Section6_7_2() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<< + + + + Foo, bar + + + +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'NOTE:Foo\, bar' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: PRODID. + */ + function testRFC6350Section6_7_3() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<< + + + + -//ONLINE DIRECTORY//NONSGML Version 1//EN + + + +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'PRODID:-//ONLINE DIRECTORY//NONSGML Version 1//EN' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + function testRFC6350Section6_7_4() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<< + + + + 19951031T222710Z + + + +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'REV:19951031T222710Z' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: SOUND. + */ + function testRFC6350Section6_7_5() { + + $this->assertXMLEqualsToMimeDir( +<< + + + + CID:JOHNQPUBLIC.part8.19960229T080000.xyzMail@example.com + + + +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'SOUND:CID:JOHNQPUBLIC.part8.19960229T080000.xyzMail@example.com' . "\n" . + 'END:VCARD' . "\n" + ); + + $this->assertXMLReflexivelyEqualsToMimeDir( +<< + + + + CID:JOHNQPUBLIC.part8.19960229T080000.xyzMail@example.com + + + +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'SOUND:CID:JOHNQPUBLIC.part8.19960229T080000.xyzMail@example.com' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: UID. + */ + function testRFC6350Section6_7_6() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<< + + + + urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6 + + + +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'UID:urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: CLIENTPIDMAP. + */ + function testRFC6350Section6_7_7() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<< + + + + 1 + urn:uuid:3df403f4-5924-4bb7-b077-3c711d9eb34b + + + +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'CLIENTPIDMAP:1;urn:uuid:3df403f4-5924-4bb7-b077-3c711d9eb34b' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: URL. + */ + function testRFC6350Section6_7_8() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<< + + + + http://example.org/restaurant.french/~chezchic.html + + + +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'URL:http://example.org/restaurant.french/~chezchic.html' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: VERSION. + */ + function testRFC6350Section6_7_9() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<< + + + +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: KEY. + */ + function testRFC6350Section6_8_1() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<< + + + + + + application/pgp-keys + + + ftp://example.com/keys/jdoe + + + +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'KEY;MEDIATYPE=application/pgp-keys:ftp://example.com/keys/jdoe' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: FBURL. + */ + function testRFC6350Section6_9_1() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<< + + + + + + 1 + + + http://www.example.com/busy/janedoe + + + +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'FBURL;PREF=1:http://www.example.com/busy/janedoe' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: CALADRURI. + */ + function testRFC6350Section6_9_2() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<< + + + + http://example.com/calendar/jdoe + + + +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'CALADRURI:http://example.com/calendar/jdoe' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: CALURI. + */ + function testRFC6350Section6_9_3() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<< + + + + + + 1 + + + http://cal.example.com/calA + + + +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'CALURI;PREF=1:http://cal.example.com/calA' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Property: CAPURI. + */ + function testRFC6350SectionA_3() { + + $this->assertXMLReflexivelyEqualsToMimeDir( +<< + + + + http://cap.example.com/capA + + + +XML +, + 'BEGIN:VCARD' . "\n" . + 'VERSION:4.0' . "\n" . + 'CAPURI:http://cap.example.com/capA' . "\n" . + 'END:VCARD' . "\n" + ); + + } + + /** + * Check this equality: + * XML -> object model -> MIME Dir. + */ + protected function assertXMLEqualsToMimeDir($xml, $mimedir) { + + $component = VObject\Reader::readXML($xml); + $this->assertVObjectEqualsVObject($mimedir, $component); + + } + + /** + * Check this (reflexive) equality: + * XML -> object model -> MIME Dir -> object model -> XML. + */ + protected function assertXMLReflexivelyEqualsToMimeDir($xml, $mimedir) { + + $this->assertXMLEqualsToMimeDir($xml, $mimedir); + + $component = VObject\Reader::read($mimedir); + $this->assertXmlStringEqualsXmlString($xml, VObject\Writer::writeXML($component)); + + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/BinaryTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/BinaryTest.php new file mode 100644 index 00000000000..3356b1bd11e --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/BinaryTest.php @@ -0,0 +1,19 @@ + '3.0']); + $vcard->add('PHOTO', ['a', 'b']); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/BooleanTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/BooleanTest.php new file mode 100644 index 00000000000..0c885d37bbe --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/BooleanTest.php @@ -0,0 +1,22 @@ +assertTrue($vcard->{'X-AWESOME'}->getValue()); + $this->assertFalse($vcard->{'X-SUCKS'}->getValue()); + + $this->assertEquals('BOOLEAN', $vcard->{'X-AWESOME'}->getValueType()); + $this->assertEquals($input, $vcard->serialize()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/CompoundTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/CompoundTest.php new file mode 100644 index 00000000000..ced0d7a4c70 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/CompoundTest.php @@ -0,0 +1,50 @@ +createProperty('ORG'); + $elem->setParts($arr); + + $this->assertEquals('ABC\, Inc.;North American Division;Marketing\;Sales', $elem->getValue()); + $this->assertEquals(3, count($elem->getParts())); + $parts = $elem->getParts(); + $this->assertEquals('Marketing;Sales', $parts[2]); + + } + + function testGetParts() { + + $str = 'ABC\, Inc.;North American Division;Marketing\;Sales'; + + $vcard = new VCard(); + $elem = $vcard->createProperty('ORG'); + $elem->setRawMimeDirValue($str); + + $this->assertEquals(3, count($elem->getParts())); + $parts = $elem->getParts(); + $this->assertEquals('Marketing;Sales', $parts[2]); + } + + function testGetPartsNull() { + + $vcard = new VCard(); + $elem = $vcard->createProperty('ORG', null); + + $this->assertEquals(0, count($elem->getParts())); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/FloatTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/FloatTest.php new file mode 100644 index 00000000000..f4636518d83 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/FloatTest.php @@ -0,0 +1,30 @@ +parse($input); + + $this->assertInstanceOf('Sabre\VObject\Property\FloatValue', $result->{'X-FLOAT'}); + + $this->assertEquals([ + 0.234, + 1.245, + ], $result->{'X-FLOAT'}->getParts()); + + $this->assertEquals( + $input, + $result->serialize() + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/ICalendar/CalAddressTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/ICalendar/CalAddressTest.php new file mode 100644 index 00000000000..fe2a550bffe --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/ICalendar/CalAddressTest.php @@ -0,0 +1,32 @@ +add('ATTENDEE', $input); + + $this->assertEquals( + $expected, + $property->getNormalizedValue() + ); + + } + + function values() { + + return [ + ['mailto:a@b.com', 'mailto:a@b.com'], + ['mailto:a@b.com', 'MAILTO:a@b.com'], + ['/foo/bar', '/foo/bar'], + ]; + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/ICalendar/DateTimeTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/ICalendar/DateTimeTest.php new file mode 100644 index 00000000000..c89cfe69bca --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/ICalendar/DateTimeTest.php @@ -0,0 +1,371 @@ +vcal = new VCalendar(); + + } + + function testSetDateTime() { + + $tz = new \DateTimeZone('Europe/Amsterdam'); + $dt = new \DateTime('1985-07-04 01:30:00', $tz); + $dt->setTimeZone($tz); + + $elem = $this->vcal->createProperty('DTSTART'); + $elem->setDateTime($dt); + + $this->assertEquals('19850704T013000', (string)$elem); + $this->assertEquals('Europe/Amsterdam', (string)$elem['TZID']); + $this->assertNull($elem['VALUE']); + + $this->assertTrue($elem->hasTime()); + + } + + function testSetDateTimeLOCAL() { + + $tz = new \DateTimeZone('Europe/Amsterdam'); + $dt = new \DateTime('1985-07-04 01:30:00', $tz); + $dt->setTimeZone($tz); + + $elem = $this->vcal->createProperty('DTSTART'); + $elem->setDateTime($dt, $isFloating = true); + + $this->assertEquals('19850704T013000', (string)$elem); + $this->assertNull($elem['TZID']); + + $this->assertTrue($elem->hasTime()); + } + + function testSetDateTimeUTC() { + + $tz = new \DateTimeZone('GMT'); + $dt = new \DateTime('1985-07-04 01:30:00', $tz); + $dt->setTimeZone($tz); + + $elem = $this->vcal->createProperty('DTSTART'); + $elem->setDateTime($dt); + + $this->assertEquals('19850704T013000Z', (string)$elem); + $this->assertNull($elem['TZID']); + + $this->assertTrue($elem->hasTime()); + } + + function testSetDateTimeFromUnixTimestamp() { + + // When initialized from a Unix timestamp, the timezone is set to "+00:00". + $dt = new \DateTime('@489288600'); + + $elem = $this->vcal->createProperty('DTSTART'); + $elem->setDateTime($dt); + + $this->assertEquals('19850704T013000Z', (string)$elem); + $this->assertNull($elem['TZID']); + + $this->assertTrue($elem->hasTime()); + } + + function testSetDateTimeLOCALTZ() { + + $tz = new \DateTimeZone('Europe/Amsterdam'); + $dt = new \DateTime('1985-07-04 01:30:00', $tz); + $dt->setTimeZone($tz); + + $elem = $this->vcal->createProperty('DTSTART'); + $elem->setDateTime($dt); + + $this->assertEquals('19850704T013000', (string)$elem); + $this->assertEquals('Europe/Amsterdam', (string)$elem['TZID']); + + $this->assertTrue($elem->hasTime()); + } + + function testSetDateTimeDATE() { + + $tz = new \DateTimeZone('Europe/Amsterdam'); + $dt = new \DateTime('1985-07-04 01:30:00', $tz); + $dt->setTimeZone($tz); + + $elem = $this->vcal->createProperty('DTSTART'); + $elem['VALUE'] = 'DATE'; + $elem->setDateTime($dt); + + $this->assertEquals('19850704', (string)$elem); + $this->assertNull($elem['TZID']); + $this->assertEquals('DATE', (string)$elem['VALUE']); + + $this->assertFalse($elem->hasTime()); + } + + function testSetValue() { + + $tz = new \DateTimeZone('Europe/Amsterdam'); + $dt = new \DateTime('1985-07-04 01:30:00', $tz); + $dt->setTimeZone($tz); + + $elem = $this->vcal->createProperty('DTSTART'); + $elem->setValue($dt); + + $this->assertEquals('19850704T013000', (string)$elem); + $this->assertEquals('Europe/Amsterdam', (string)$elem['TZID']); + $this->assertNull($elem['VALUE']); + + $this->assertTrue($elem->hasTime()); + + } + + function testSetValueArray() { + + $tz = new \DateTimeZone('Europe/Amsterdam'); + $dt1 = new \DateTime('1985-07-04 01:30:00', $tz); + $dt2 = new \DateTime('1985-07-04 02:30:00', $tz); + $dt1->setTimeZone($tz); + $dt2->setTimeZone($tz); + + $elem = $this->vcal->createProperty('DTSTART'); + $elem->setValue([$dt1, $dt2]); + + $this->assertEquals('19850704T013000,19850704T023000', (string)$elem); + $this->assertEquals('Europe/Amsterdam', (string)$elem['TZID']); + $this->assertNull($elem['VALUE']); + + $this->assertTrue($elem->hasTime()); + + } + + function testSetParts() { + + $tz = new \DateTimeZone('Europe/Amsterdam'); + $dt1 = new \DateTime('1985-07-04 01:30:00', $tz); + $dt2 = new \DateTime('1985-07-04 02:30:00', $tz); + $dt1->setTimeZone($tz); + $dt2->setTimeZone($tz); + + $elem = $this->vcal->createProperty('DTSTART'); + $elem->setParts([$dt1, $dt2]); + + $this->assertEquals('19850704T013000,19850704T023000', (string)$elem); + $this->assertEquals('Europe/Amsterdam', (string)$elem['TZID']); + $this->assertNull($elem['VALUE']); + + $this->assertTrue($elem->hasTime()); + + } + function testSetPartsStrings() { + + $dt1 = '19850704T013000Z'; + $dt2 = '19850704T023000Z'; + + $elem = $this->vcal->createProperty('DTSTART'); + $elem->setParts([$dt1, $dt2]); + + $this->assertEquals('19850704T013000Z,19850704T023000Z', (string)$elem); + $this->assertNull($elem['VALUE']); + + $this->assertTrue($elem->hasTime()); + + } + + + function testGetDateTimeCached() { + + $tz = new \DateTimeZone('Europe/Amsterdam'); + $dt = new \DateTimeImmutable('1985-07-04 01:30:00', $tz); + $dt->setTimeZone($tz); + + $elem = $this->vcal->createProperty('DTSTART'); + $elem->setDateTime($dt); + + $this->assertEquals($elem->getDateTime(), $dt); + + } + + function testGetDateTimeDateNULL() { + + $elem = $this->vcal->createProperty('DTSTART'); + $dt = $elem->getDateTime(); + + $this->assertNull($dt); + + } + + function testGetDateTimeDateDATE() { + + $elem = $this->vcal->createProperty('DTSTART', '19850704'); + $dt = $elem->getDateTime(); + + $this->assertInstanceOf('DateTimeImmutable', $dt); + $this->assertEquals('1985-07-04 00:00:00', $dt->format('Y-m-d H:i:s')); + + } + + function testGetDateTimeDateDATEReferenceTimeZone() { + + $elem = $this->vcal->createProperty('DTSTART', '19850704'); + + $tz = new \DateTimeZone('America/Toronto'); + $dt = $elem->getDateTime($tz); + $dt = $dt->setTimeZone(new \DateTimeZone('UTC')); + + $this->assertInstanceOf('DateTimeImmutable', $dt); + $this->assertEquals('1985-07-04 04:00:00', $dt->format('Y-m-d H:i:s')); + + } + + function testGetDateTimeDateFloating() { + + $elem = $this->vcal->createProperty('DTSTART', '19850704T013000'); + $dt = $elem->getDateTime(); + + $this->assertInstanceOf('DateTimeImmutable', $dt); + $this->assertEquals('1985-07-04 01:30:00', $dt->format('Y-m-d H:i:s')); + + } + + function testGetDateTimeDateFloatingReferenceTimeZone() { + + $elem = $this->vcal->createProperty('DTSTART', '19850704T013000'); + + $tz = new \DateTimeZone('America/Toronto'); + $dt = $elem->getDateTime($tz); + $dt = $dt->setTimeZone(new \DateTimeZone('UTC')); + + $this->assertInstanceOf('DateTimeInterface', $dt); + $this->assertEquals('1985-07-04 05:30:00', $dt->format('Y-m-d H:i:s')); + + } + + function testGetDateTimeDateUTC() { + + $elem = $this->vcal->createProperty('DTSTART', '19850704T013000Z'); + $dt = $elem->getDateTime(); + + $this->assertInstanceOf('DateTimeImmutable', $dt); + $this->assertEquals('1985-07-04 01:30:00', $dt->format('Y-m-d H:i:s')); + $this->assertEquals('UTC', $dt->getTimeZone()->getName()); + + } + + function testGetDateTimeDateLOCALTZ() { + + $elem = $this->vcal->createProperty('DTSTART', '19850704T013000'); + $elem['TZID'] = 'Europe/Amsterdam'; + + $dt = $elem->getDateTime(); + + $this->assertInstanceOf('DateTimeImmutable', $dt); + $this->assertEquals('1985-07-04 01:30:00', $dt->format('Y-m-d H:i:s')); + $this->assertEquals('Europe/Amsterdam', $dt->getTimeZone()->getName()); + + } + + /** + * @expectedException \Sabre\VObject\InvalidDataException + */ + function testGetDateTimeDateInvalid() { + + $elem = $this->vcal->createProperty('DTSTART', 'bla'); + $dt = $elem->getDateTime(); + + } + + function testGetDateTimeWeirdTZ() { + + $elem = $this->vcal->createProperty('DTSTART', '19850704T013000'); + $elem['TZID'] = '/freeassociation.sourceforge.net/Tzfile/Europe/Amsterdam'; + + + $event = $this->vcal->createComponent('VEVENT'); + $event->add($elem); + + $timezone = $this->vcal->createComponent('VTIMEZONE'); + $timezone->TZID = '/freeassociation.sourceforge.net/Tzfile/Europe/Amsterdam'; + $timezone->{'X-LIC-LOCATION'} = 'Europe/Amsterdam'; + + $this->vcal->add($event); + $this->vcal->add($timezone); + + $dt = $elem->getDateTime(); + + $this->assertInstanceOf('DateTimeImmutable', $dt); + $this->assertEquals('1985-07-04 01:30:00', $dt->format('Y-m-d H:i:s')); + $this->assertEquals('Europe/Amsterdam', $dt->getTimeZone()->getName()); + + } + + function testGetDateTimeBadTimeZone() { + + $default = date_default_timezone_get(); + date_default_timezone_set('Canada/Eastern'); + + $elem = $this->vcal->createProperty('DTSTART', '19850704T013000'); + $elem['TZID'] = 'Moon'; + + + $event = $this->vcal->createComponent('VEVENT'); + $event->add($elem); + + $timezone = $this->vcal->createComponent('VTIMEZONE'); + $timezone->TZID = 'Moon'; + $timezone->{'X-LIC-LOCATION'} = 'Moon'; + + + $this->vcal->add($event); + $this->vcal->add($timezone); + + $dt = $elem->getDateTime(); + + $this->assertInstanceOf('DateTimeImmutable', $dt); + $this->assertEquals('1985-07-04 01:30:00', $dt->format('Y-m-d H:i:s')); + $this->assertEquals('Canada/Eastern', $dt->getTimeZone()->getName()); + date_default_timezone_set($default); + + } + + function testUpdateValueParameter() { + + $dtStart = $this->vcal->createProperty('DTSTART', new \DateTime('2013-06-07 15:05:00')); + $dtStart['VALUE'] = 'DATE'; + + $this->assertEquals("DTSTART;VALUE=DATE:20130607\r\n", $dtStart->serialize()); + + } + + function testValidate() { + + $exDate = $this->vcal->createProperty('EXDATE', '-00011130T143000Z'); + $messages = $exDate->validate(); + $this->assertEquals(1, count($messages)); + $this->assertEquals(3, $messages[0]['level']); + + } + + /** + * This issue was discovered on the sabredav mailing list. + */ + function testCreateDatePropertyThroughAdd() { + + $vcal = new VCalendar(); + $vevent = $vcal->add('VEVENT'); + + $dtstart = $vevent->add( + 'DTSTART', + new \DateTime('2014-03-07'), + ['VALUE' => 'DATE'] + ); + + $this->assertEquals("DTSTART;VALUE=DATE:20140307\r\n", $dtstart->serialize()); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/ICalendar/DurationTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/ICalendar/DurationTest.php new file mode 100644 index 00000000000..9aaaebce0a0 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/ICalendar/DurationTest.php @@ -0,0 +1,20 @@ +add('VEVENT', ['DURATION' => ['PT1H']]); + + $this->assertEquals( + new \DateInterval('PT1H'), + $event->{'DURATION'}->getDateInterval() + ); + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/ICalendar/RecurTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/ICalendar/RecurTest.php new file mode 100644 index 00000000000..df95e3bc849 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/ICalendar/RecurTest.php @@ -0,0 +1,453 @@ +add('RRULE', 'FREQ=Daily'); + + $this->assertInstanceOf('Sabre\VObject\Property\ICalendar\Recur', $recur); + + $this->assertEquals(['FREQ' => 'DAILY'], $recur->getParts()); + $recur->setParts(['freq' => 'MONTHLY']); + + $this->assertEquals(['FREQ' => 'MONTHLY'], $recur->getParts()); + + } + + /** + * @expectedException \InvalidArgumentException + */ + function testSetValueBadVal() { + + $vcal = new VCalendar(); + $recur = $vcal->add('RRULE', 'FREQ=Daily'); + $recur->setValue(new \Exception()); + + } + + function testSetValueWithCount() { + $vcal = new VCalendar(); + $recur = $vcal->add('RRULE', 'FREQ=Daily'); + $recur->setValue(['COUNT' => 3]); + $this->assertEquals($recur->getParts()['COUNT'], 3); + } + + function testGetJSONWithCount() { + $input = 'BEGIN:VCALENDAR +BEGIN:VEVENT +UID:908d53c0-e1a3-4883-b69f-530954d6bd62 +TRANSP:OPAQUE +DTSTART;TZID=Europe/Berlin:20160301T150000 +DTEND;TZID=Europe/Berlin:20160301T170000 +SUMMARY:test +RRULE:FREQ=DAILY;COUNT=3 +ORGANIZER;CN=robert pipo:mailto:robert@example.org +END:VEVENT +END:VCALENDAR +'; + + $vcal = Reader::read($input); + $rrule = $vcal->VEVENT->RRULE; + $count = $rrule->getJsonValue()[0]['count']; + $this->assertTrue(is_int($count)); + $this->assertEquals(3, $count); + } + + function testSetSubParts() { + + $vcal = new VCalendar(); + $recur = $vcal->add('RRULE', ['FREQ' => 'DAILY', 'BYDAY' => 'mo,tu', 'BYMONTH' => [0, 1]]); + + $this->assertEquals([ + 'FREQ' => 'DAILY', + 'BYDAY' => ['MO', 'TU'], + 'BYMONTH' => [0, 1], + ], $recur->getParts()); + + } + + function testGetJSONWithUntil() { + $input = 'BEGIN:VCALENDAR +BEGIN:VEVENT +UID:908d53c0-e1a3-4883-b69f-530954d6bd62 +TRANSP:OPAQUE +DTSTART;TZID=Europe/Berlin:20160301T150000 +DTEND;TZID=Europe/Berlin:20160301T170000 +SUMMARY:test +RRULE:FREQ=DAILY;UNTIL=20160305T230000Z +ORGANIZER;CN=robert pipo:mailto:robert@example.org +END:VEVENT +END:VCALENDAR +'; + + $vcal = Reader::read($input); + $rrule = $vcal->VEVENT->RRULE; + $untilJsonString = $rrule->getJsonValue()[0]['until']; + $this->assertEquals('2016-03-05T23:00:00Z', $untilJsonString); + } + + + function testValidateStripEmpties() { + + $input = 'BEGIN:VCALENDAR +VERSION:2.0 +PRODID:foobar +BEGIN:VEVENT +UID:908d53c0-e1a3-4883-b69f-530954d6bd62 +TRANSP:OPAQUE +DTSTART;TZID=Europe/Berlin:20160301T150000 +DTEND;TZID=Europe/Berlin:20160301T170000 +SUMMARY:test +RRULE:FREQ=DAILY;BYMONTH=;UNTIL=20160305T230000Z +ORGANIZER;CN=robert pipo:mailto:robert@example.org +DTSTAMP:20160312T183800Z +END:VEVENT +END:VCALENDAR +'; + + $vcal = Reader::read($input); + $this->assertEquals( + 1, + count($vcal->validate()) + ); + $this->assertEquals( + 1, + count($vcal->validate($vcal::REPAIR)) + ); + + $expected = 'BEGIN:VCALENDAR +VERSION:2.0 +PRODID:foobar +BEGIN:VEVENT +UID:908d53c0-e1a3-4883-b69f-530954d6bd62 +TRANSP:OPAQUE +DTSTART;TZID=Europe/Berlin:20160301T150000 +DTEND;TZID=Europe/Berlin:20160301T170000 +SUMMARY:test +RRULE:FREQ=DAILY;UNTIL=20160305T230000Z +ORGANIZER;CN=robert pipo:mailto:robert@example.org +DTSTAMP:20160312T183800Z +END:VEVENT +END:VCALENDAR +'; + + $this->assertVObjectEqualsVObject( + $expected, + $vcal + ); + + } + + function testValidateStripNoFreq() { + + $input = 'BEGIN:VCALENDAR +VERSION:2.0 +PRODID:foobar +BEGIN:VEVENT +UID:908d53c0-e1a3-4883-b69f-530954d6bd62 +TRANSP:OPAQUE +DTSTART;TZID=Europe/Berlin:20160301T150000 +DTEND;TZID=Europe/Berlin:20160301T170000 +SUMMARY:test +RRULE:UNTIL=20160305T230000Z +ORGANIZER;CN=robert pipo:mailto:robert@example.org +DTSTAMP:20160312T183800Z +END:VEVENT +END:VCALENDAR +'; + + $vcal = Reader::read($input); + $this->assertEquals( + 1, + count($vcal->validate()) + ); + $this->assertEquals( + 1, + count($vcal->validate($vcal::REPAIR)) + ); + + $expected = 'BEGIN:VCALENDAR +VERSION:2.0 +PRODID:foobar +BEGIN:VEVENT +UID:908d53c0-e1a3-4883-b69f-530954d6bd62 +TRANSP:OPAQUE +DTSTART;TZID=Europe/Berlin:20160301T150000 +DTEND;TZID=Europe/Berlin:20160301T170000 +SUMMARY:test +ORGANIZER;CN=robert pipo:mailto:robert@example.org +DTSTAMP:20160312T183800Z +END:VEVENT +END:VCALENDAR +'; + + $this->assertVObjectEqualsVObject( + $expected, + $vcal + ); + + } + + function testValidateInvalidByMonthRruleWithRepair() { + + $calendar = new VCalendar(); + $property = $calendar->createProperty('RRULE', 'FREQ=YEARLY;COUNT=6;BYMONTHDAY=24;BYMONTH=0'); + $result = $property->validate(Node::REPAIR); + + $this->assertCount(1, $result); + $this->assertEquals('BYMONTH in RRULE must have value(s) between 1 and 12!', $result[0]['message']); + $this->assertEquals(1, $result[0]['level']); + $this->assertEquals('FREQ=YEARLY;COUNT=6;BYMONTHDAY=24', $property->getValue()); + + } + + function testValidateInvalidByMonthRruleWithoutRepair() { + + $calendar = new VCalendar(); + $property = $calendar->createProperty('RRULE', 'FREQ=YEARLY;COUNT=6;BYMONTHDAY=24;BYMONTH=0'); + $result = $property->validate(); + + $this->assertCount(1, $result); + $this->assertEquals('BYMONTH in RRULE must have value(s) between 1 and 12!', $result[0]['message']); + $this->assertEquals(3, $result[0]['level']); + $this->assertEquals('FREQ=YEARLY;COUNT=6;BYMONTHDAY=24;BYMONTH=0', $property->getValue()); + + } + + function testValidateInvalidByMonthRruleWithRepair2() { + + $calendar = new VCalendar(); + $property = $calendar->createProperty('RRULE', 'FREQ=YEARLY;COUNT=6;BYMONTHDAY=24;BYMONTH=bla'); + $result = $property->validate(Node::REPAIR); + + $this->assertCount(1, $result); + $this->assertEquals('BYMONTH in RRULE must have value(s) between 1 and 12!', $result[0]['message']); + $this->assertEquals(1, $result[0]['level']); + $this->assertEquals('FREQ=YEARLY;COUNT=6;BYMONTHDAY=24', $property->getValue()); + + } + + function testValidateInvalidByMonthRruleWithoutRepair2() { + + $calendar = new VCalendar(); + $property = $calendar->createProperty('RRULE', 'FREQ=YEARLY;COUNT=6;BYMONTHDAY=24;BYMONTH=bla'); + $result = $property->validate(); + + $this->assertCount(1, $result); + $this->assertEquals('BYMONTH in RRULE must have value(s) between 1 and 12!', $result[0]['message']); + $this->assertEquals(3, $result[0]['level']); + // Without repair the invalid BYMONTH is still there, but the value is changed to uppercase + $this->assertEquals('FREQ=YEARLY;COUNT=6;BYMONTHDAY=24;BYMONTH=BLA', $property->getValue()); + + } + + function testValidateInvalidByMonthRruleValue14WithRepair() { + + $calendar = new VCalendar(); + $property = $calendar->createProperty('RRULE', 'FREQ=YEARLY;COUNT=6;BYMONTHDAY=24;BYMONTH=14'); + $result = $property->validate(Node::REPAIR); + + $this->assertCount(1, $result); + $this->assertEquals('BYMONTH in RRULE must have value(s) between 1 and 12!', $result[0]['message']); + $this->assertEquals(1, $result[0]['level']); + $this->assertEquals('FREQ=YEARLY;COUNT=6;BYMONTHDAY=24', $property->getValue()); + + } + + function testValidateInvalidByMonthRruleMultipleWithRepair() { + + $calendar = new VCalendar(); + $property = $calendar->createProperty('RRULE', 'FREQ=YEARLY;COUNT=6;BYMONTHDAY=24;BYMONTH=0,1,2,3,4,14'); + $result = $property->validate(Node::REPAIR); + + $this->assertCount(2, $result); + $this->assertEquals('BYMONTH in RRULE must have value(s) between 1 and 12!', $result[0]['message']); + $this->assertEquals(1, $result[0]['level']); + $this->assertEquals('BYMONTH in RRULE must have value(s) between 1 and 12!', $result[1]['message']); + $this->assertEquals(1, $result[1]['level']); + $this->assertEquals('FREQ=YEARLY;COUNT=6;BYMONTHDAY=24;BYMONTH=1,2,3,4', $property->getValue()); + + } + + function testValidateOneOfManyInvalidByMonthRruleWithRepair() { + + $calendar = new VCalendar(); + $property = $calendar->createProperty('RRULE', 'FREQ=YEARLY;COUNT=6;BYMONTHDAY=24;BYMONTH=bla,3,foo'); + $result = $property->validate(Node::REPAIR); + + $this->assertCount(2, $result); + $this->assertEquals('BYMONTH in RRULE must have value(s) between 1 and 12!', $result[0]['message']); + $this->assertEquals(1, $result[0]['level']); + $this->assertEquals('BYMONTH in RRULE must have value(s) between 1 and 12!', $result[1]['message']); + $this->assertEquals(1, $result[1]['level']); + $this->assertEquals('FREQ=YEARLY;COUNT=6;BYMONTHDAY=24;BYMONTH=3', $property->getValue()); + + } + + function testValidateValidByMonthRrule() { + + $calendar = new VCalendar(); + $property = $calendar->createProperty('RRULE', 'FREQ=YEARLY;COUNT=6;BYMONTHDAY=24;BYMONTH=2,3'); + $this->assertEquals('FREQ=YEARLY;COUNT=6;BYMONTHDAY=24;BYMONTH=2,3', $property->getValue()); + + } + + /** + * test for issue #336 + */ + function testValidateRruleBySecondZero() { + + $calendar = new VCalendar(); + $property = $calendar->createProperty('RRULE', 'FREQ=DAILY;BYHOUR=10;BYMINUTE=30;BYSECOND=0;UNTIL=20150616T153000Z'); + $result = $property->validate(Node::REPAIR); + + // There should be 0 warnings and the value should be unchanged + $this->assertEmpty($result); + $this->assertEquals('FREQ=DAILY;BYHOUR=10;BYMINUTE=30;BYSECOND=0;UNTIL=20150616T153000Z', $property->getValue()); + + } + + function testValidateValidByWeekNoWithRepair() { + + $calendar = new VCalendar(); + $property = $calendar->createProperty('RRULE', 'FREQ=YEARLY;COUNT=6;BYWEEKNO=11'); + $result = $property->validate(Node::REPAIR); + + $this->assertCount(0, $result); + $this->assertEquals('FREQ=YEARLY;COUNT=6;BYWEEKNO=11', $property->getValue()); + + } + + function testValidateInvalidByWeekNoWithRepair() { + + $calendar = new VCalendar(); + $property = $calendar->createProperty('RRULE', 'FREQ=YEARLY;COUNT=6;BYWEEKNO=55;BYDAY=WE'); + $result = $property->validate(Node::REPAIR); + + $this->assertCount(1, $result); + $this->assertEquals('BYWEEKNO in RRULE must have value(s) from -53 to -1, or 1 to 53!', $result[0]['message']); + $this->assertEquals(1, $result[0]['level']); + $this->assertEquals('FREQ=YEARLY;COUNT=6;BYDAY=WE', $property->getValue()); + + } + + function testValidateMultipleInvalidByWeekNoWithRepair() { + + $calendar = new VCalendar(); + $property = $calendar->createProperty('RRULE', 'FREQ=YEARLY;COUNT=6;BYWEEKNO=55,2,-80;BYDAY=WE'); + $result = $property->validate(Node::REPAIR); + + $this->assertCount(2, $result); + $this->assertEquals('BYWEEKNO in RRULE must have value(s) from -53 to -1, or 1 to 53!', $result[0]['message']); + $this->assertEquals(1, $result[0]['level']); + $this->assertEquals('BYWEEKNO in RRULE must have value(s) from -53 to -1, or 1 to 53!', $result[1]['message']); + $this->assertEquals(1, $result[1]['level']); + $this->assertEquals('FREQ=YEARLY;COUNT=6;BYWEEKNO=2;BYDAY=WE', $property->getValue()); + + } + + function testValidateAllInvalidByWeekNoWithRepair() { + + $calendar = new VCalendar(); + $property = $calendar->createProperty('RRULE', 'FREQ=YEARLY;COUNT=6;BYWEEKNO=55,-80;BYDAY=WE'); + $result = $property->validate(Node::REPAIR); + + $this->assertCount(2, $result); + $this->assertEquals('BYWEEKNO in RRULE must have value(s) from -53 to -1, or 1 to 53!', $result[0]['message']); + $this->assertEquals(1, $result[0]['level']); + $this->assertEquals('BYWEEKNO in RRULE must have value(s) from -53 to -1, or 1 to 53!', $result[1]['message']); + $this->assertEquals(1, $result[1]['level']); + $this->assertEquals('FREQ=YEARLY;COUNT=6;BYDAY=WE', $property->getValue()); + + } + + function testValidateInvalidByWeekNoWithoutRepair() { + + $calendar = new VCalendar(); + $property = $calendar->createProperty('RRULE', 'FREQ=YEARLY;COUNT=6;BYWEEKNO=55;BYDAY=WE'); + $result = $property->validate(); + + $this->assertCount(1, $result); + $this->assertEquals('BYWEEKNO in RRULE must have value(s) from -53 to -1, or 1 to 53!', $result[0]['message']); + $this->assertEquals(3, $result[0]['level']); + $this->assertEquals('FREQ=YEARLY;COUNT=6;BYWEEKNO=55;BYDAY=WE', $property->getValue()); + + } + + function testValidateValidByYearDayWithRepair() { + + $calendar = new VCalendar(); + $property = $calendar->createProperty('RRULE', 'FREQ=YEARLY;COUNT=6;BYYEARDAY=119'); + $result = $property->validate(Node::REPAIR); + + $this->assertCount(0, $result); + $this->assertEquals('FREQ=YEARLY;COUNT=6;BYYEARDAY=119', $property->getValue()); + + } + + function testValidateInvalidByYearDayWithRepair() { + + $calendar = new VCalendar(); + $property = $calendar->createProperty('RRULE', 'FREQ=YEARLY;COUNT=6;BYYEARDAY=367;BYDAY=WE'); + $result = $property->validate(Node::REPAIR); + + $this->assertCount(1, $result); + $this->assertEquals('BYYEARDAY in RRULE must have value(s) from -366 to -1, or 1 to 366!', $result[0]['message']); + $this->assertEquals(1, $result[0]['level']); + $this->assertEquals('FREQ=YEARLY;COUNT=6;BYDAY=WE', $property->getValue()); + + } + + function testValidateMultipleInvalidByYearDayWithRepair() { + + $calendar = new VCalendar(); + $property = $calendar->createProperty('RRULE', 'FREQ=YEARLY;COUNT=6;BYYEARDAY=380,2,-390;BYDAY=WE'); + $result = $property->validate(Node::REPAIR); + + $this->assertCount(2, $result); + $this->assertEquals('BYYEARDAY in RRULE must have value(s) from -366 to -1, or 1 to 366!', $result[0]['message']); + $this->assertEquals(1, $result[0]['level']); + $this->assertEquals('BYYEARDAY in RRULE must have value(s) from -366 to -1, or 1 to 366!', $result[1]['message']); + $this->assertEquals(1, $result[1]['level']); + $this->assertEquals('FREQ=YEARLY;COUNT=6;BYYEARDAY=2;BYDAY=WE', $property->getValue()); + + } + + function testValidateAllInvalidByYearDayWithRepair() { + + $calendar = new VCalendar(); + $property = $calendar->createProperty('RRULE', 'FREQ=YEARLY;COUNT=6;BYYEARDAY=455,-480;BYDAY=WE'); + $result = $property->validate(Node::REPAIR); + + $this->assertCount(2, $result); + $this->assertEquals('BYYEARDAY in RRULE must have value(s) from -366 to -1, or 1 to 366!', $result[0]['message']); + $this->assertEquals(1, $result[0]['level']); + $this->assertEquals('BYYEARDAY in RRULE must have value(s) from -366 to -1, or 1 to 366!', $result[1]['message']); + $this->assertEquals(1, $result[1]['level']); + $this->assertEquals('FREQ=YEARLY;COUNT=6;BYDAY=WE', $property->getValue()); + + } + + function testValidateInvalidByYearDayWithoutRepair() { + + $calendar = new VCalendar(); + $property = $calendar->createProperty('RRULE', 'FREQ=YEARLY;COUNT=6;BYYEARDAY=380;BYDAY=WE'); + $result = $property->validate(); + + $this->assertCount(1, $result); + $this->assertEquals('BYYEARDAY in RRULE must have value(s) from -366 to -1, or 1 to 366!', $result[0]['message']); + $this->assertEquals(3, $result[0]['level']); + $this->assertEquals('FREQ=YEARLY;COUNT=6;BYYEARDAY=380;BYDAY=WE', $property->getValue()); + + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/TextTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/TextTest.php new file mode 100644 index 00000000000..69ac8aaf181 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/TextTest.php @@ -0,0 +1,96 @@ + '2.1', + 'PROP' => $propValue + ], false); + + // Adding quoted-printable, because we're testing if it gets removed + // automatically. + $doc->PROP['ENCODING'] = 'QUOTED-PRINTABLE'; + $doc->PROP['P1'] = 'V1'; + + + $output = $doc->serialize(); + + + $this->assertEquals("BEGIN:VCARD\r\nVERSION:2.1\r\n$expected\r\nEND:VCARD\r\n", $output); + + } + + function testSerializeVCard21() { + + $this->assertVCard21Serialization( + 'f;oo', + 'PROP;P1=V1:f;oo' + ); + + } + + function testSerializeVCard21Array() { + + $this->assertVCard21Serialization( + ['f;oo', 'bar'], + 'PROP;P1=V1:f\;oo;bar' + ); + + } + function testSerializeVCard21Fold() { + + $this->assertVCard21Serialization( + str_repeat('x', 80), + 'PROP;P1=V1:' . str_repeat('x', 64) . "\r\n " . str_repeat('x', 16) + ); + + } + + + + function testSerializeQuotedPrintable() { + + $this->assertVCard21Serialization( + "foo\r\nbar", + 'PROP;P1=V1;ENCODING=QUOTED-PRINTABLE:foo=0D=0Abar' + ); + } + + function testSerializeQuotedPrintableFold() { + + $this->assertVCard21Serialization( + "foo\r\nbarxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "PROP;P1=V1;ENCODING=QUOTED-PRINTABLE:foo=0D=0Abarxxxxxxxxxxxxxxxxxxxxxxxxxx=\r\n xxx" + ); + + } + + function testValidateMinimumPropValue() { + + $vcard = <<assertEquals(1, count($vcard->validate())); + + $this->assertEquals(1, count($vcard->N->getParts())); + + $vcard->validate(\Sabre\VObject\Node::REPAIR); + + $this->assertEquals(5, count($vcard->N->getParts())); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/UriTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/UriTest.php new file mode 100644 index 00000000000..2c44d8888a4 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/UriTest.php @@ -0,0 +1,27 @@ +serialize(); + $this->assertContains('URL;VALUE=URI:http://example.org/', $output); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/VCard/DateAndOrTimeTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/VCard/DateAndOrTimeTest.php new file mode 100644 index 00000000000..7bc2c67a9e8 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/VCard/DateAndOrTimeTest.php @@ -0,0 +1,269 @@ +createProperty('BDAY', $input); + + $this->assertEquals([$output], $prop->getJsonValue()); + + } + + function dates() { + + return [ + [ + "19961022T140000", + "1996-10-22T14:00:00", + ], + [ + "--1022T1400", + "--10-22T14:00", + ], + [ + "---22T14", + "---22T14", + ], + [ + "19850412", + "1985-04-12", + ], + [ + "1985-04", + "1985-04", + ], + [ + "1985", + "1985", + ], + [ + "--0412", + "--04-12", + ], + [ + "T102200", + "T10:22:00", + ], + [ + "T1022", + "T10:22", + ], + [ + "T10", + "T10", + ], + [ + "T-2200", + "T-22:00", + ], + [ + "T102200Z", + "T10:22:00Z", + ], + [ + "T102200-0800", + "T10:22:00-0800", + ], + [ + "T--00", + "T--00", + ], + ]; + + } + + function testSetParts() { + + $vcard = new VObject\Component\VCard(); + + $prop = $vcard->createProperty('BDAY'); + $prop->setParts([ + new \DateTime('2014-04-02 18:37:00') + ]); + + $this->assertEquals('20140402T183700Z', $prop->getValue()); + + } + + function testSetPartsDateTimeImmutable() { + + $vcard = new VObject\Component\VCard(); + + $prop = $vcard->createProperty('BDAY'); + $prop->setParts([ + new \DateTimeImmutable('2014-04-02 18:37:00') + ]); + + $this->assertEquals('20140402T183700Z', $prop->getValue()); + + } + + /** + * @expectedException InvalidArgumentException + */ + function testSetPartsTooMany() { + + $vcard = new VObject\Component\VCard(); + + $prop = $vcard->createProperty('BDAY'); + $prop->setParts([ + 1, + 2 + ]); + + } + + function testSetPartsString() { + + $vcard = new VObject\Component\VCard(); + + $prop = $vcard->createProperty('BDAY'); + $prop->setParts([ + "20140402T183700Z" + ]); + + $this->assertEquals('20140402T183700Z', $prop->getValue()); + + } + + function testSetValueDateTime() { + + $vcard = new VObject\Component\VCard(); + + $prop = $vcard->createProperty('BDAY'); + $prop->setValue( + new \DateTime('2014-04-02 18:37:00') + ); + + $this->assertEquals('20140402T183700Z', $prop->getValue()); + + } + + function testSetValueDateTimeImmutable() { + + $vcard = new VObject\Component\VCard(); + + $prop = $vcard->createProperty('BDAY'); + $prop->setValue( + new \DateTimeImmutable('2014-04-02 18:37:00') + ); + + $this->assertEquals('20140402T183700Z', $prop->getValue()); + + } + + function testSetDateTimeOffset() { + + $vcard = new VObject\Component\VCard(); + + $prop = $vcard->createProperty('BDAY'); + $prop->setValue( + new \DateTime('2014-04-02 18:37:00', new \DateTimeZone('America/Toronto')) + ); + + $this->assertEquals('20140402T183700-0400', $prop->getValue()); + + } + + function testGetDateTime() { + + $datetime = new \DateTime('2014-04-02 18:37:00', new \DateTimeZone('America/Toronto')); + + $vcard = new VObject\Component\VCard(); + $prop = $vcard->createProperty('BDAY', $datetime); + + $dt = $prop->getDateTime(); + $this->assertEquals('2014-04-02T18:37:00-04:00', $dt->format('c'), "For some reason this one failed. Current default timezone is: " . date_default_timezone_get()); + + } + + function testGetDate() { + + $datetime = new \DateTime('2014-04-02'); + + $vcard = new VObject\Component\VCard(); + $prop = $vcard->createProperty('BDAY', $datetime, null, 'DATE'); + + $this->assertEquals('DATE', $prop->getValueType()); + $this->assertEquals('BDAY:20140402', rtrim($prop->serialize())); + + } + + function testGetDateIncomplete() { + + $datetime = '--0407'; + + $vcard = new VObject\Component\VCard(); + $prop = $vcard->add('BDAY', $datetime); + + $dt = $prop->getDateTime(); + // Note: if the year changes between the last line and the next line of + // code, this test may fail. + // + // If that happens, head outside and have a drink. + $current = new \DateTime('now'); + $year = $current->format('Y'); + + $this->assertEquals($year . '0407', $dt->format('Ymd')); + + } + + function testGetDateIncompleteFromVCard() { + + $vcard = <<BDAY; + + $dt = $prop->getDateTime(); + // Note: if the year changes between the last line and the next line of + // code, this test may fail. + // + // If that happens, head outside and have a drink. + $current = new \DateTime('now'); + $year = $current->format('Y'); + + $this->assertEquals($year . '0407', $dt->format('Ymd')); + + } + + function testValidate() { + + $datetime = '--0407'; + + $vcard = new VObject\Component\VCard(); + $prop = $vcard->add('BDAY', $datetime); + + $this->assertEquals([], $prop->validate()); + + } + + function testValidateBroken() { + + $datetime = '123'; + + $vcard = new VObject\Component\VCard(); + $prop = $vcard->add('BDAY', $datetime); + + $this->assertEquals([[ + 'level' => 3, + 'message' => 'The supplied value (123) is not a correct DATE-AND-OR-TIME property', + 'node' => $prop, + ]], $prop->validate()); + + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/VCard/LanguageTagTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/VCard/LanguageTagTest.php new file mode 100644 index 00000000000..c38b6f26438 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Property/VCard/LanguageTagTest.php @@ -0,0 +1,48 @@ +parse($input); + + $this->assertInstanceOf('Sabre\VObject\Property\VCard\LanguageTag', $result->LANG); + + $this->assertEquals('nl', $result->LANG->getValue()); + + $this->assertEquals( + $input, + $result->serialize() + ); + + } + + function testChangeAndSerialize() { + + $input = "BEGIN:VCARD\r\nVERSION:4.0\r\nLANG:nl\r\nEND:VCARD\r\n"; + $mimeDir = new VObject\Parser\MimeDir($input); + + $result = $mimeDir->parse($input); + + $this->assertInstanceOf('Sabre\VObject\Property\VCard\LanguageTag', $result->LANG); + // This replicates what the vcard converter does and triggered a bug in + // the past. + $result->LANG->setValue(['de']); + + $this->assertEquals('de', $result->LANG->getValue()); + + $expected = "BEGIN:VCARD\r\nVERSION:4.0\r\nLANG:de\r\nEND:VCARD\r\n"; + $this->assertEquals( + $expected, + $result->serialize() + ); + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/PropertyTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/PropertyTest.php new file mode 100644 index 00000000000..b6241ce8c02 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/PropertyTest.php @@ -0,0 +1,410 @@ +createProperty('propname', 'propvalue'); + $this->assertEquals('PROPNAME', $property->name); + $this->assertEquals('propvalue', $property->__toString()); + $this->assertEquals('propvalue', (string)$property); + $this->assertEquals('propvalue', $property->getValue()); + + } + + function testCreate() { + + $cal = new VCalendar(); + + $params = [ + 'param1' => 'value1', + 'param2' => 'value2', + ]; + + $property = $cal->createProperty('propname', 'propvalue', $params); + + $this->assertEquals('value1', $property['param1']->getValue()); + $this->assertEquals('value2', $property['param2']->getValue()); + + } + + function testSetValue() { + + $cal = new VCalendar(); + + $property = $cal->createProperty('propname', 'propvalue'); + $property->setValue('value2'); + + $this->assertEquals('PROPNAME', $property->name); + $this->assertEquals('value2', $property->__toString()); + + } + + function testParameterExists() { + + $cal = new VCalendar(); + $property = $cal->createProperty('propname', 'propvalue'); + $property['paramname'] = 'paramvalue'; + + $this->assertTrue(isset($property['PARAMNAME'])); + $this->assertTrue(isset($property['paramname'])); + $this->assertFalse(isset($property['foo'])); + + } + + function testParameterGet() { + + $cal = new VCalendar(); + $property = $cal->createProperty('propname', 'propvalue'); + $property['paramname'] = 'paramvalue'; + + $this->assertInstanceOf('Sabre\\VObject\\Parameter', $property['paramname']); + + } + + function testParameterNotExists() { + + $cal = new VCalendar(); + $property = $cal->createProperty('propname', 'propvalue'); + $property['paramname'] = 'paramvalue'; + + $this->assertInternalType('null', $property['foo']); + + } + + function testParameterMultiple() { + + $cal = new VCalendar(); + $property = $cal->createProperty('propname', 'propvalue'); + $property['paramname'] = 'paramvalue'; + $property->add('paramname', 'paramvalue'); + + $this->assertInstanceOf('Sabre\\VObject\\Parameter', $property['paramname']); + $this->assertEquals(2, count($property['paramname']->getParts())); + + } + + function testSetParameterAsString() { + + $cal = new VCalendar(); + $property = $cal->createProperty('propname', 'propvalue'); + $property['paramname'] = 'paramvalue'; + + $this->assertEquals(1, count($property->parameters())); + $this->assertInstanceOf('Sabre\\VObject\\Parameter', $property->parameters['PARAMNAME']); + $this->assertEquals('PARAMNAME', $property->parameters['PARAMNAME']->name); + $this->assertEquals('paramvalue', $property->parameters['PARAMNAME']->getValue()); + + } + + function testUnsetParameter() { + + $cal = new VCalendar(); + $property = $cal->createProperty('propname', 'propvalue'); + $property['paramname'] = 'paramvalue'; + + unset($property['PARAMNAME']); + $this->assertEquals(0, count($property->parameters())); + + } + + function testSerialize() { + + $cal = new VCalendar(); + $property = $cal->createProperty('propname', 'propvalue'); + + $this->assertEquals("PROPNAME:propvalue\r\n", $property->serialize()); + + } + + function testSerializeParam() { + + $cal = new VCalendar(); + $property = $cal->createProperty('propname', 'propvalue', [ + 'paramname' => 'paramvalue', + 'paramname2' => 'paramvalue2', + ]); + + $this->assertEquals("PROPNAME;PARAMNAME=paramvalue;PARAMNAME2=paramvalue2:propvalue\r\n", $property->serialize()); + + } + + function testSerializeNewLine() { + + $cal = new VCalendar(); + $property = $cal->createProperty('SUMMARY', "line1\nline2"); + + $this->assertEquals("SUMMARY:line1\\nline2\r\n", $property->serialize()); + + } + + function testSerializeLongLine() { + + $cal = new VCalendar(); + $value = str_repeat('!', 200); + $property = $cal->createProperty('propname', $value); + + $expected = "PROPNAME:" . str_repeat('!', 66) . "\r\n " . str_repeat('!', 74) . "\r\n " . str_repeat('!', 60) . "\r\n"; + + $this->assertEquals($expected, $property->serialize()); + + } + + function testSerializeUTF8LineFold() { + + $cal = new VCalendar(); + $value = str_repeat('!', 65) . "\xc3\xa4bla"; // inserted umlaut-a + $property = $cal->createProperty('propname', $value); + $expected = "PROPNAME:" . str_repeat('!', 65) . "\r\n \xc3\xa4bla\r\n"; + $this->assertEquals($expected, $property->serialize()); + + } + + function testGetIterator() { + + $cal = new VCalendar(); + $it = new ElementList([]); + $property = $cal->createProperty('propname', 'propvalue'); + $property->setIterator($it); + $this->assertEquals($it, $property->getIterator()); + + } + + + function testGetIteratorDefault() { + + $cal = new VCalendar(); + $property = $cal->createProperty('propname', 'propvalue'); + $it = $property->getIterator(); + $this->assertTrue($it instanceof ElementList); + $this->assertEquals(1, count($it)); + + } + + function testAddScalar() { + + $cal = new VCalendar(); + $property = $cal->createProperty('EMAIL'); + + $property->add('myparam', 'value'); + + $this->assertEquals(1, count($property->parameters())); + + $this->assertTrue($property->parameters['MYPARAM'] instanceof Parameter); + $this->assertEquals('MYPARAM', $property->parameters['MYPARAM']->name); + $this->assertEquals('value', $property->parameters['MYPARAM']->getValue()); + + } + + function testAddParameter() { + + $cal = new VCalendar(); + $prop = $cal->createProperty('EMAIL'); + + $prop->add('MYPARAM', 'value'); + + $this->assertEquals(1, count($prop->parameters())); + $this->assertEquals('MYPARAM', $prop['myparam']->name); + + } + + function testAddParameterTwice() { + + $cal = new VCalendar(); + $prop = $cal->createProperty('EMAIL'); + + $prop->add('MYPARAM', 'value1'); + $prop->add('MYPARAM', 'value2'); + + $this->assertEquals(1, count($prop->parameters)); + $this->assertEquals(2, count($prop->parameters['MYPARAM']->getParts())); + + $this->assertEquals('MYPARAM', $prop['MYPARAM']->name); + + } + + + function testClone() { + + $cal = new VCalendar(); + $property = $cal->createProperty('EMAIL', 'value'); + $property['FOO'] = 'BAR'; + + $property2 = clone $property; + + $property['FOO'] = 'BAZ'; + $this->assertEquals('BAR', (string)$property2['FOO']); + + } + + function testCreateParams() { + + $cal = new VCalendar(); + $property = $cal->createProperty('X-PROP', 'value', [ + 'param1' => 'value1', + 'param2' => ['value2', 'value3'] + ]); + + $this->assertEquals(1, count($property['PARAM1']->getParts())); + $this->assertEquals(2, count($property['PARAM2']->getParts())); + + } + + function testValidateNonUTF8() { + + $calendar = new VCalendar(); + $property = $calendar->createProperty('X-PROP', "Bla\x00"); + $result = $property->validate(Property::REPAIR); + + $this->assertEquals('Property contained a control character (0x00)', $result[0]['message']); + $this->assertEquals('Bla', $property->getValue()); + + } + + function testValidateControlChars() { + + $s = "chars["; + foreach ([ + 0x7F, 0x5E, 0x5C, 0x3B, 0x3A, 0x2C, 0x22, 0x20, + 0x1F, 0x1E, 0x1D, 0x1C, 0x1B, 0x1A, 0x19, 0x18, + 0x17, 0x16, 0x15, 0x14, 0x13, 0x12, 0x11, 0x10, + 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08, + 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00, + ] as $c) { + $s .= sprintf('%02X(%c)', $c, $c); + } + $s .= "]end"; + + $calendar = new VCalendar(); + $property = $calendar->createProperty('X-PROP', $s); + $result = $property->validate(Property::REPAIR); + + $this->assertEquals('Property contained a control character (0x7f)', $result[0]['message']); + $this->assertEquals("chars[7F()5E(^)5C(\\\\)3B(\\;)3A(:)2C(\\,)22(\")20( )1F()1E()1D()1C()1B()1A()19()18()17()16()15()14()13()12()11()10()0F()0E()0D()0C()0B()0A(\\n)09(\t)08()07()06()05()04()03()02()01()00()]end", $property->getRawMimeDirValue()); + + } + + function testValidateBadPropertyName() { + + $calendar = new VCalendar(); + $property = $calendar->createProperty("X_*&PROP*", "Bla"); + $result = $property->validate(Property::REPAIR); + + $this->assertEquals($result[0]['message'], 'The propertyname: X_*&PROP* contains invalid characters. Only A-Z, 0-9 and - are allowed'); + $this->assertEquals('X-PROP', $property->name); + + } + + function testGetValue() { + + $calendar = new VCalendar(); + $property = $calendar->createProperty("SUMMARY", null); + $this->assertEquals([], $property->getParts()); + $this->assertNull($property->getValue()); + + $property->setValue([]); + $this->assertEquals([], $property->getParts()); + $this->assertNull($property->getValue()); + + $property->setValue([1]); + $this->assertEquals([1], $property->getParts()); + $this->assertEquals(1, $property->getValue()); + + $property->setValue([1, 2]); + $this->assertEquals([1, 2], $property->getParts()); + $this->assertEquals('1,2', $property->getValue()); + + $property->setValue('str'); + $this->assertEquals(['str'], $property->getParts()); + $this->assertEquals('str', $property->getValue()); + } + + /** + * ElementList should reject this. + * + * @expectedException \LogicException + */ + function testArrayAccessSetInt() { + + $calendar = new VCalendar(); + $property = $calendar->createProperty("X-PROP", null); + + $calendar->add($property); + $calendar->{'X-PROP'}[0] = 'Something!'; + + } + + /** + * ElementList should reject this. + * + * @expectedException \LogicException + */ + function testArrayAccessUnsetInt() { + + $calendar = new VCalendar(); + $property = $calendar->createProperty("X-PROP", null); + + $calendar->add($property); + unset($calendar->{'X-PROP'}[0]); + + } + + function testValidateBadEncoding() { + + $document = new VCalendar(); + $property = $document->add('X-FOO', 'value'); + $property['ENCODING'] = 'invalid'; + + $result = $property->validate(); + + $this->assertEquals('ENCODING=INVALID is not valid for this document type.', $result[0]['message']); + $this->assertEquals(3, $result[0]['level']); + + } + + function testValidateBadEncodingVCard4() { + + $document = new VCard(['VERSION' => '4.0']); + $property = $document->add('X-FOO', 'value'); + $property['ENCODING'] = 'BASE64'; + + $result = $property->validate(); + + $this->assertEquals('ENCODING parameter is not valid in vCard 4.', $result[0]['message']); + $this->assertEquals(3, $result[0]['level']); + + } + + function testValidateBadEncodingVCard3() { + + $document = new VCard(['VERSION' => '3.0']); + $property = $document->add('X-FOO', 'value'); + $property['ENCODING'] = 'BASE64'; + + $result = $property->validate(); + + $this->assertEquals('ENCODING=BASE64 is not valid for this document type.', $result[0]['message']); + $this->assertEquals(3, $result[0]['level']); + + } + + function testValidateBadEncodingVCard21() { + + $document = new VCard(['VERSION' => '2.1']); + $property = $document->add('X-FOO', 'value'); + $property['ENCODING'] = 'B'; + + $result = $property->validate(); + + $this->assertEquals('ENCODING=B is not valid for this document type.', $result[0]['message']); + $this->assertEquals(3, $result[0]['level']); + + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/ReaderTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ReaderTest.php new file mode 100644 index 00000000000..7c3217b7663 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/ReaderTest.php @@ -0,0 +1,491 @@ +assertInstanceOf('Sabre\\VObject\\Component', $result); + $this->assertEquals('VCALENDAR', $result->name); + $this->assertEquals(0, count($result->children())); + + } + + function testReadStream() { + + $data = "BEGIN:VCALENDAR\r\nEND:VCALENDAR"; + + $stream = fopen('php://memory', 'r+'); + fwrite($stream, $data); + rewind($stream); + + $result = Reader::read($stream); + + $this->assertInstanceOf('Sabre\\VObject\\Component', $result); + $this->assertEquals('VCALENDAR', $result->name); + $this->assertEquals(0, count($result->children())); + + } + + function testReadComponentUnixNewLine() { + + $data = "BEGIN:VCALENDAR\nEND:VCALENDAR"; + + $result = Reader::read($data); + + $this->assertInstanceOf('Sabre\\VObject\\Component', $result); + $this->assertEquals('VCALENDAR', $result->name); + $this->assertEquals(0, count($result->children())); + + } + + function testReadComponentLineFold() { + + $data = "BEGIN:\r\n\tVCALENDAR\r\nE\r\n ND:VCALENDAR"; + + $result = Reader::read($data); + + $this->assertInstanceOf('Sabre\\VObject\\Component', $result); + $this->assertEquals('VCALENDAR', $result->name); + $this->assertEquals(0, count($result->children())); + + } + + /** + * @expectedException Sabre\VObject\ParseException + */ + function testReadCorruptComponent() { + + $data = "BEGIN:VCALENDAR\r\nEND:FOO"; + + $result = Reader::read($data); + + } + + /** + * @expectedException Sabre\VObject\ParseException + */ + function testReadCorruptSubComponent() { + + $data = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nEND:FOO\r\nEND:VCALENDAR"; + + $result = Reader::read($data); + + } + + function testReadProperty() { + + $data = "BEGIN:VCALENDAR\r\nSUMMARY:propValue\r\nEND:VCALENDAR"; + $result = Reader::read($data); + + $result = $result->SUMMARY; + $this->assertInstanceOf('Sabre\\VObject\\Property', $result); + $this->assertEquals('SUMMARY', $result->name); + $this->assertEquals('propValue', $result->getValue()); + + } + + function testReadPropertyWithNewLine() { + + $data = "BEGIN:VCALENDAR\r\nSUMMARY:Line1\\nLine2\\NLine3\\\\Not the 4th line!\r\nEND:VCALENDAR"; + $result = Reader::read($data); + + $result = $result->SUMMARY; + $this->assertInstanceOf('Sabre\\VObject\\Property', $result); + $this->assertEquals('SUMMARY', $result->name); + $this->assertEquals("Line1\nLine2\nLine3\\Not the 4th line!", $result->getValue()); + + } + + function testReadMappedProperty() { + + $data = "BEGIN:VCALENDAR\r\nDTSTART:20110529\r\nEND:VCALENDAR"; + $result = Reader::read($data); + + $result = $result->DTSTART; + $this->assertInstanceOf('Sabre\\VObject\\Property\\ICalendar\\DateTime', $result); + $this->assertEquals('DTSTART', $result->name); + $this->assertEquals('20110529', $result->getValue()); + + } + + function testReadMappedPropertyGrouped() { + + $data = "BEGIN:VCALENDAR\r\nfoo.DTSTART:20110529\r\nEND:VCALENDAR"; + $result = Reader::read($data); + + $result = $result->DTSTART; + $this->assertInstanceOf('Sabre\\VObject\\Property\\ICalendar\\DateTime', $result); + $this->assertEquals('DTSTART', $result->name); + $this->assertEquals('20110529', $result->getValue()); + + } + + /** + * @expectedException Sabre\VObject\ParseException + */ + function testReadBrokenLine() { + + $data = "BEGIN:VCALENDAR\r\nPROPNAME;propValue"; + $result = Reader::read($data); + + } + + function testReadPropertyInComponent() { + + $data = [ + "BEGIN:VCALENDAR", + "PROPNAME:propValue", + "END:VCALENDAR" + ]; + + $result = Reader::read(implode("\r\n", $data)); + + $this->assertInstanceOf('Sabre\\VObject\\Component', $result); + $this->assertEquals('VCALENDAR', $result->name); + $this->assertEquals(1, count($result->children())); + $this->assertInstanceOf('Sabre\\VObject\\Property', $result->children()[0]); + $this->assertEquals('PROPNAME', $result->children()[0]->name); + $this->assertEquals('propValue', $result->children()[0]->getValue()); + + } + + function testReadNestedComponent() { + + $data = [ + "BEGIN:VCALENDAR", + "BEGIN:VTIMEZONE", + "BEGIN:DAYLIGHT", + "END:DAYLIGHT", + "END:VTIMEZONE", + "END:VCALENDAR" + ]; + + $result = Reader::read(implode("\r\n", $data)); + + $this->assertInstanceOf('Sabre\\VObject\\Component', $result); + $this->assertEquals('VCALENDAR', $result->name); + $this->assertEquals(1, count($result->children())); + $this->assertInstanceOf('Sabre\\VObject\\Component', $result->children()[0]); + $this->assertEquals('VTIMEZONE', $result->children()[0]->name); + $this->assertEquals(1, count($result->children()[0]->children())); + $this->assertInstanceOf('Sabre\\VObject\\Component', $result->children()[0]->children()[0]); + $this->assertEquals('DAYLIGHT', $result->children()[0]->children()[0]->name); + + + } + + function testReadPropertyParameter() { + + $data = "BEGIN:VCALENDAR\r\nPROPNAME;PARAMNAME=paramvalue:propValue\r\nEND:VCALENDAR"; + $result = Reader::read($data); + + $result = $result->PROPNAME; + + $this->assertInstanceOf('Sabre\\VObject\\Property', $result); + $this->assertEquals('PROPNAME', $result->name); + $this->assertEquals('propValue', $result->getValue()); + $this->assertEquals(1, count($result->parameters())); + $this->assertEquals('PARAMNAME', $result->parameters['PARAMNAME']->name); + $this->assertEquals('paramvalue', $result->parameters['PARAMNAME']->getValue()); + + } + + function testReadPropertyRepeatingParameter() { + + $data = "BEGIN:VCALENDAR\r\nPROPNAME;N=1;N=2;N=3,4;N=\"5\",6;N=\"7,8\";N=9,10;N=^'11^':propValue\r\nEND:VCALENDAR"; + $result = Reader::read($data); + + $result = $result->PROPNAME; + + $this->assertInstanceOf('Sabre\\VObject\\Property', $result); + $this->assertEquals('PROPNAME', $result->name); + $this->assertEquals('propValue', $result->getValue()); + $this->assertEquals(1, count($result->parameters())); + $this->assertEquals('N', $result->parameters['N']->name); + $this->assertEquals('1,2,3,4,5,6,7,8,9,10,"11"', $result->parameters['N']->getValue()); + $this->assertEquals([1, 2, 3, 4, 5, 6, "7,8", 9, 10, '"11"'], $result->parameters['N']->getParts()); + + } + + function testReadPropertyRepeatingNamelessGuessedParameter() { + + $data = "BEGIN:VCALENDAR\r\nPROPNAME;WORK;VOICE;PREF:propValue\r\nEND:VCALENDAR"; + $result = Reader::read($data); + + $result = $result->PROPNAME; + + $this->assertInstanceOf('Sabre\\VObject\\Property', $result); + $this->assertEquals('PROPNAME', $result->name); + $this->assertEquals('propValue', $result->getValue()); + $this->assertEquals(1, count($result->parameters())); + $this->assertEquals('TYPE', $result->parameters['TYPE']->name); + $this->assertEquals('WORK,VOICE,PREF', $result->parameters['TYPE']->getValue()); + $this->assertEquals(['WORK', 'VOICE', 'PREF'], $result->parameters['TYPE']->getParts()); + + } + + function testReadPropertyNoName() { + + $data = "BEGIN:VCALENDAR\r\nPROPNAME;PRODIGY:propValue\r\nEND:VCALENDAR"; + $result = Reader::read($data); + + $result = $result->PROPNAME; + + $this->assertInstanceOf('Sabre\\VObject\\Property', $result); + $this->assertEquals('PROPNAME', $result->name); + $this->assertEquals('propValue', $result->getValue()); + $this->assertEquals(1, count($result->parameters())); + $this->assertEquals('TYPE', $result->parameters['TYPE']->name); + $this->assertTrue($result->parameters['TYPE']->noName); + $this->assertEquals('PRODIGY', $result->parameters['TYPE']); + + } + + function testReadPropertyParameterExtraColon() { + + $data = "BEGIN:VCALENDAR\r\nPROPNAME;PARAMNAME=paramvalue:propValue:anotherrandomstring\r\nEND:VCALENDAR"; + $result = Reader::read($data); + + $result = $result->PROPNAME; + + $this->assertInstanceOf('Sabre\\VObject\\Property', $result); + $this->assertEquals('PROPNAME', $result->name); + $this->assertEquals('propValue:anotherrandomstring', $result->getValue()); + $this->assertEquals(1, count($result->parameters())); + $this->assertEquals('PARAMNAME', $result->parameters['PARAMNAME']->name); + $this->assertEquals('paramvalue', $result->parameters['PARAMNAME']->getValue()); + + } + + function testReadProperty2Parameters() { + + $data = "BEGIN:VCALENDAR\r\nPROPNAME;PARAMNAME=paramvalue;PARAMNAME2=paramvalue2:propValue\r\nEND:VCALENDAR"; + $result = Reader::read($data); + + $result = $result->PROPNAME; + + $this->assertInstanceOf('Sabre\\VObject\\Property', $result); + $this->assertEquals('PROPNAME', $result->name); + $this->assertEquals('propValue', $result->getValue()); + $this->assertEquals(2, count($result->parameters())); + $this->assertEquals('PARAMNAME', $result->parameters['PARAMNAME']->name); + $this->assertEquals('paramvalue', $result->parameters['PARAMNAME']->getValue()); + $this->assertEquals('PARAMNAME2', $result->parameters['PARAMNAME2']->name); + $this->assertEquals('paramvalue2', $result->parameters['PARAMNAME2']->getValue()); + + } + + function testReadPropertyParameterQuoted() { + + $data = "BEGIN:VCALENDAR\r\nPROPNAME;PARAMNAME=\"paramvalue\":propValue\r\nEND:VCALENDAR"; + $result = Reader::read($data); + + $result = $result->PROPNAME; + + $this->assertInstanceOf('Sabre\\VObject\\Property', $result); + $this->assertEquals('PROPNAME', $result->name); + $this->assertEquals('propValue', $result->getValue()); + $this->assertEquals(1, count($result->parameters())); + $this->assertEquals('PARAMNAME', $result->parameters['PARAMNAME']->name); + $this->assertEquals('paramvalue', $result->parameters['PARAMNAME']->getValue()); + + } + + function testReadPropertyParameterNewLines() { + + $data = "BEGIN:VCALENDAR\r\nPROPNAME;PARAMNAME=paramvalue1^nvalue2^^nvalue3:propValue\r\nEND:VCALENDAR"; + $result = Reader::read($data); + + $result = $result->PROPNAME; + + $this->assertInstanceOf('Sabre\\VObject\\Property', $result); + $this->assertEquals('PROPNAME', $result->name); + $this->assertEquals('propValue', $result->getValue()); + + $this->assertEquals(1, count($result->parameters())); + $this->assertEquals('PARAMNAME', $result->parameters['PARAMNAME']->name); + $this->assertEquals("paramvalue1\nvalue2^nvalue3", $result->parameters['PARAMNAME']->getValue()); + + } + + function testReadPropertyParameterQuotedColon() { + + $data = "BEGIN:VCALENDAR\r\nPROPNAME;PARAMNAME=\"param:value\":propValue\r\nEND:VCALENDAR"; + $result = Reader::read($data); + $result = $result->PROPNAME; + + $this->assertInstanceOf('Sabre\\VObject\\Property', $result); + $this->assertEquals('PROPNAME', $result->name); + $this->assertEquals('propValue', $result->getValue()); + $this->assertEquals(1, count($result->parameters())); + $this->assertEquals('PARAMNAME', $result->parameters['PARAMNAME']->name); + $this->assertEquals('param:value', $result->parameters['PARAMNAME']->getValue()); + + } + + function testReadForgiving() { + + $data = [ + "BEGIN:VCALENDAR", + "X_PROP:propValue", + "END:VCALENDAR" + ]; + + $caught = false; + try { + $result = Reader::read(implode("\r\n", $data)); + } catch (ParseException $e) { + $caught = true; + } + + $this->assertEquals(true, $caught); + + $result = Reader::read(implode("\r\n", $data), Reader::OPTION_FORGIVING); + + $expected = implode("\r\n", [ + "BEGIN:VCALENDAR", + "X_PROP:propValue", + "END:VCALENDAR", + "" + ]); + + $this->assertEquals($expected, $result->serialize()); + + } + + function testReadWithInvalidLine() { + + $data = [ + "BEGIN:VCALENDAR", + "DESCRIPTION:propValue", + "Yes, we've actually seen a file with non-idented property values on multiple lines", + "END:VCALENDAR" + ]; + + $caught = false; + try { + $result = Reader::read(implode("\r\n", $data)); + } catch (ParseException $e) { + $caught = true; + } + + $this->assertEquals(true, $caught); + + $result = Reader::read(implode("\r\n", $data), Reader::OPTION_IGNORE_INVALID_LINES); + + $expected = implode("\r\n", [ + "BEGIN:VCALENDAR", + "DESCRIPTION:propValue", + "END:VCALENDAR", + "" + ]); + + $this->assertEquals($expected, $result->serialize()); + + } + + /** + * Reported as Issue 32. + * + * @expectedException \Sabre\VObject\ParseException + */ + function testReadIncompleteFile() { + + $input = <<assertInstanceOf('Sabre\\VObject\\Component', $result); + $this->assertEquals('VCALENDAR', $result->name); + $this->assertEquals(0, count($result->children())); + + } + + function testReadXMLComponent() { + + $data = << + + + + +XML; + + $result = Reader::readXML($data); + + $this->assertInstanceOf('Sabre\\VObject\\Component', $result); + $this->assertEquals('VCALENDAR', $result->name); + $this->assertEquals(0, count($result->children())); + + } + + function testReadXMLStream() { + + $data = << + + + + +XML; + + $stream = fopen('php://memory', 'r+'); + fwrite($stream, $data); + rewind($stream); + + $result = Reader::readXML($stream); + + $this->assertInstanceOf('Sabre\\VObject\\Component', $result); + $this->assertEquals('VCALENDAR', $result->name); + $this->assertEquals(0, count($result->children())); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/ByMonthInDailyTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/ByMonthInDailyTest.php new file mode 100644 index 00000000000..204dd36df0f --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/ByMonthInDailyTest.php @@ -0,0 +1,58 @@ +assertInstanceOf('Sabre\\VObject\\Component\\VCalendar', $vcal); + + $vcal = $vcal->expand(new DateTime('2013-09-28'), new DateTime('2014-09-11')); + + foreach ($vcal->VEVENT as $event) { + $dates[] = $event->DTSTART->getValue(); + } + + $expectedDates = [ + "20130929T160000Z", + "20131006T160000Z", + "20131013T160000Z", + "20131020T160000Z", + "20131027T160000Z", + "20140907T160000Z" + ]; + + $this->assertEquals($expectedDates, $dates, 'Recursed dates are restricted by month'); + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/BySetPosHangTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/BySetPosHangTest.php new file mode 100644 index 00000000000..65e38f536e8 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/BySetPosHangTest.php @@ -0,0 +1,60 @@ +assertInstanceOf('Sabre\\VObject\\Component\\VCalendar', $vcal); + + $vcal = $vcal->expand(new DateTime('2015-01-01'), new DateTime('2016-01-01')); + + foreach ($vcal->VEVENT as $event) { + $dates[] = $event->DTSTART->getValue(); + } + + $expectedDates = [ + "20150101T160000Z", + "20150122T160000Z", + "20150219T160000Z", + "20150319T160000Z", + "20150423T150000Z", + "20150521T150000Z", + "20150618T150000Z", + "20150723T150000Z", + "20150820T150000Z", + "20150917T150000Z", + "20151022T150000Z", + "20151119T160000Z", + "20151224T160000Z", + ]; + + $this->assertEquals($expectedDates, $dates); + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/ExpandFloatingTimesTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/ExpandFloatingTimesTest.php new file mode 100644 index 00000000000..3d7b6f19c82 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/ExpandFloatingTimesTest.php @@ -0,0 +1,122 @@ +assertInstanceOf('Sabre\\VObject\\Component\\VCalendar', $vcal); + + $vcal = $vcal->expand(new DateTime('2015-01-01'), new DateTime('2015-01-31')); + $output = <<assertVObjectEqualsVObject($output, $vcal); + + } + + function testExpandWithReferenceTimezone() { + + $input = <<assertInstanceOf('Sabre\\VObject\\Component\\VCalendar', $vcal); + + $vcal = $vcal->expand( + new DateTime('2015-01-01'), + new DateTime('2015-01-31'), + new DateTimeZone('Europe/Berlin') + ); + + $output = <<assertVObjectEqualsVObject($output, $vcal); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/FifthTuesdayProblemTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/FifthTuesdayProblemTest.php new file mode 100644 index 00000000000..2029ec9c5c6 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/FifthTuesdayProblemTest.php @@ -0,0 +1,54 @@ +VEVENT->UID); + + while ($it->valid()) { + $it->next(); + } + + // If we got here, it means we were successful. The bug that was in the + // system before would fail on the 5th tuesday of the month, if the 5th + // tuesday did not exist. + $this->assertTrue(true); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/HandleRDateExpandTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/HandleRDateExpandTest.php new file mode 100644 index 00000000000..32dcf9330f7 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/HandleRDateExpandTest.php @@ -0,0 +1,60 @@ +assertInstanceOf('Sabre\\VObject\\Component\\VCalendar', $vcal); + + $vcal = $vcal->expand(new DateTime('2015-01-01'), new DateTime('2015-12-01')); + + $result = iterator_to_array($vcal->VEVENT); + + $this->assertEquals(5, count($result)); + + $utc = new DateTimeZone('UTC'); + $expected = [ + new DateTimeImmutable("2015-10-12", $utc), + new DateTimeImmutable("2015-10-15", $utc), + new DateTimeImmutable("2015-10-17", $utc), + new DateTimeImmutable("2015-10-18", $utc), + new DateTimeImmutable("2015-10-20", $utc), + ]; + + $result = array_map(function($ev) {return $ev->DTSTART->getDateTime();}, $result); + $this->assertEquals($expected, $result); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/IncorrectExpandTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/IncorrectExpandTest.php new file mode 100644 index 00000000000..82278293d61 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/IncorrectExpandTest.php @@ -0,0 +1,62 @@ +assertInstanceOf('Sabre\\VObject\\Component\\VCalendar', $vcal); + + $vcal = $vcal->expand(new DateTime('2011-01-01'), new DateTime('2014-01-01')); + + $output = <<assertVObjectEqualsVObject($output, $vcal); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/InfiniteLoopProblemTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/InfiniteLoopProblemTest.php new file mode 100644 index 00000000000..491b0e87970 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/InfiniteLoopProblemTest.php @@ -0,0 +1,98 @@ +vcal = new VCalendar(); + + } + + /** + * This bug came from a Fruux customer. This would result in a never-ending + * request. + */ + function testFastForwardTooFar() { + + $ev = $this->vcal->createComponent('VEVENT'); + $ev->UID = 'foobar'; + $ev->DTSTART = '20090420T180000Z'; + $ev->RRULE = 'FREQ=WEEKLY;BYDAY=MO;UNTIL=20090704T205959Z;INTERVAL=1'; + + $this->assertFalse($ev->isInTimeRange(new DateTimeImmutable('2012-01-01 12:00:00'), new DateTimeImmutable('3000-01-01 00:00:00'))); + + } + + /** + * Different bug, also likely an infinite loop. + */ + function testYearlyByMonthLoop() { + + $ev = $this->vcal->createComponent('VEVENT'); + $ev->UID = 'uuid'; + $ev->DTSTART = '20120101T154500'; + $ev->DTSTART['TZID'] = 'Europe/Berlin'; + $ev->RRULE = 'FREQ=YEARLY;INTERVAL=1;UNTIL=20120203T225959Z;BYMONTH=2;BYSETPOS=1;BYDAY=SU,MO,TU,WE,TH,FR,SA'; + $ev->DTEND = '20120101T164500'; + $ev->DTEND['TZID'] = 'Europe/Berlin'; + + // This recurrence rule by itself is a yearly rule that should happen + // every february. + // + // The BYDAY part expands this to every day of the month, but the + // BYSETPOS limits this to only the 1st day of the month. Very crazy + // way to specify this, and could have certainly been a lot easier. + $this->vcal->add($ev); + + $it = new Recur\EventIterator($this->vcal, 'uuid'); + $it->fastForward(new DateTimeImmutable('2012-01-29 23:00:00', new DateTimeZone('UTC'))); + + $collect = []; + + while ($it->valid()) { + $collect[] = $it->getDtStart(); + if ($it->getDtStart() > new DateTimeImmutable('2013-02-05 22:59:59', new DateTimeZone('UTC'))) { + break; + } + $it->next(); + + } + + $this->assertEquals( + [new DateTimeImmutable('2012-02-01 15:45:00', new DateTimeZone('Europe/Berlin'))], + $collect + ); + + } + + /** + * Something, somewhere produced an ics with an interval set to 0. Because + * this means we increase the current day (or week, month) by 0, this also + * results in an infinite loop. + * + * @expectedException \Sabre\VObject\InvalidDataException + * @return void + */ + function testZeroInterval() { + + $ev = $this->vcal->createComponent('VEVENT'); + $ev->UID = 'uuid'; + $ev->DTSTART = '20120824T145700Z'; + $ev->RRULE = 'FREQ=YEARLY;INTERVAL=0'; + $this->vcal->add($ev); + + $it = new Recur\EventIterator($this->vcal, 'uuid'); + $it->fastForward(new DateTimeImmutable('2013-01-01 23:00:00', new DateTimeZone('UTC'))); + + // if we got this far.. it means we are no longer infinitely looping + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/Issue26Test.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/Issue26Test.php new file mode 100644 index 00000000000..df8619da579 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/Issue26Test.php @@ -0,0 +1,34 @@ +assertInstanceOf('Sabre\\VObject\\Component\\VCalendar', $vcal); + + $it = new EventIterator($vcal, 'bae5d57a98'); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/Issue48Test.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/Issue48Test.php new file mode 100644 index 00000000000..179da816dda --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/Issue48Test.php @@ -0,0 +1,48 @@ +assertInstanceOf('Sabre\\VObject\\Component\\VCalendar', $vcal); + + $it = new Recur\EventIterator($vcal, 'foo'); + + $result = iterator_to_array($it); + + $tz = new DateTimeZone('Europe/Moscow'); + + $expected = [ + new DateTimeImmutable('2013-07-10 11:00:00', $tz), + new DateTimeImmutable('2013-07-12 11:00:00', $tz), + new DateTimeImmutable('2013-07-13 11:00:00', $tz), + ]; + + $this->assertEquals($expected, $result); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/Issue50Test.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/Issue50Test.php new file mode 100644 index 00000000000..193bdd878de --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/Issue50Test.php @@ -0,0 +1,127 @@ +assertInstanceOf('Sabre\\VObject\\Component\\VCalendar', $vcal); + + $it = new Recur\EventIterator($vcal, '1aef0b27-3d92-4581-829a-11999dd36724'); + + $result = []; + foreach ($it as $instance) { + + $result[] = $instance; + + } + + $tz = new DateTimeZone('Europe/Brussels'); + + $this->assertEquals([ + new DateTimeImmutable('2013-07-15 09:00:00', $tz), + new DateTimeImmutable('2013-07-16 07:00:00', $tz), + new DateTimeImmutable('2013-07-17 07:00:00', $tz), + new DateTimeImmutable('2013-07-18 09:00:00', $tz), + new DateTimeImmutable('2013-07-19 07:00:00', $tz), + ], $result); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/MainTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/MainTest.php new file mode 100644 index 00000000000..0d8c5b188c6 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/MainTest.php @@ -0,0 +1,1452 @@ +createComponent('VEVENT'); + $ev->UID = 'bla'; + $ev->RRULE = 'FREQ=DAILY;BYHOUR=10;BYMINUTE=5;BYSECOND=16;BYWEEKNO=32;BYYEARDAY=100,200'; + $dtStart = $vcal->createProperty('DTSTART'); + $dtStart->setDateTime(new DateTimeImmutable('2011-10-07')); + + $ev->add($dtStart); + + $vcal->add($ev); + + $it = new EventIterator($vcal, (string)$ev->UID); + + $this->assertTrue($it->isInfinite()); + + } + + /** + * @expectedException \Sabre\VObject\InvalidDataException + * @depends testValues + */ + function testInvalidFreq() { + + $vcal = new VCalendar(); + $ev = $vcal->createComponent('VEVENT'); + $ev->RRULE = 'FREQ=SMONTHLY;INTERVAL=3;UNTIL=20111025T000000Z'; + $ev->UID = 'foo'; + $dtStart = $vcal->createProperty('DTSTART'); + $dtStart->setDateTime(new DateTimeImmutable('2011-10-07', new DateTimeZone('UTC'))); + + $ev->add($dtStart); + $vcal->add($ev); + + $it = new EventIterator($vcal, (string)$ev->UID); + + } + + /** + * @expectedException InvalidArgumentException + */ + function testVCalendarNoUID() { + + $vcal = new VCalendar(); + $it = new EventIterator($vcal); + + } + + /** + * @expectedException InvalidArgumentException + */ + function testVCalendarInvalidUID() { + + $vcal = new VCalendar(); + $it = new EventIterator($vcal, 'foo'); + + } + + /** + * @depends testValues + */ + function testHourly() { + + $vcal = new VCalendar(); + $ev = $vcal->createComponent('VEVENT'); + + $ev->UID = 'bla'; + $ev->RRULE = 'FREQ=HOURLY;INTERVAL=3;UNTIL=20111025T000000Z'; + $dtStart = $vcal->createProperty('DTSTART'); + $dtStart->setDateTime(new DateTimeImmutable('2011-10-07 12:00:00', new DateTimeZone('UTC'))); + + $ev->add($dtStart); + $vcal->add($ev); + + $it = new EventIterator($vcal, $ev->UID); + + // Max is to prevent overflow + $max = 12; + $result = []; + foreach ($it as $item) { + + $result[] = $item; + $max--; + + if (!$max) break; + + } + + $tz = new DateTimeZone('UTC'); + + $this->assertEquals( + [ + new DateTimeImmutable('2011-10-07 12:00:00', $tz), + new DateTimeImmutable('2011-10-07 15:00:00', $tz), + new DateTimeImmutable('2011-10-07 18:00:00', $tz), + new DateTimeImmutable('2011-10-07 21:00:00', $tz), + new DateTimeImmutable('2011-10-08 00:00:00', $tz), + new DateTimeImmutable('2011-10-08 03:00:00', $tz), + new DateTimeImmutable('2011-10-08 06:00:00', $tz), + new DateTimeImmutable('2011-10-08 09:00:00', $tz), + new DateTimeImmutable('2011-10-08 12:00:00', $tz), + new DateTimeImmutable('2011-10-08 15:00:00', $tz), + new DateTimeImmutable('2011-10-08 18:00:00', $tz), + new DateTimeImmutable('2011-10-08 21:00:00', $tz), + ], + $result + ); + + } + + /** + * @depends testValues + */ + function testDaily() { + + $vcal = new VCalendar(); + $ev = $vcal->createComponent('VEVENT'); + + $ev->UID = 'bla'; + $ev->RRULE = 'FREQ=DAILY;INTERVAL=3;UNTIL=20111025T000000Z'; + $dtStart = $vcal->createProperty('DTSTART'); + $dtStart->setDateTime(new DateTimeImmutable('2011-10-07', new DateTimeZone('UTC'))); + + $ev->add($dtStart); + + $vcal->add($ev); + + $it = new EventIterator($vcal, $ev->UID); + + // Max is to prevent overflow + $max = 12; + $result = []; + foreach ($it as $item) { + + $result[] = $item; + $max--; + + if (!$max) break; + + } + + $tz = new DateTimeZone('UTC'); + + $this->assertEquals( + [ + new DateTimeImmutable('2011-10-07', $tz), + new DateTimeImmutable('2011-10-10', $tz), + new DateTimeImmutable('2011-10-13', $tz), + new DateTimeImmutable('2011-10-16', $tz), + new DateTimeImmutable('2011-10-19', $tz), + new DateTimeImmutable('2011-10-22', $tz), + new DateTimeImmutable('2011-10-25', $tz), + ], + $result + ); + + } + + /** + * @depends testValues + */ + function testNoRRULE() { + + $vcal = new VCalendar(); + $ev = $vcal->createComponent('VEVENT'); + + $ev->UID = 'bla'; + $dtStart = $vcal->createProperty('DTSTART'); + $dtStart->setDateTime(new DateTimeImmutable('2011-10-07', new DateTimeZone('UTC'))); + + $ev->add($dtStart); + + $vcal->add($ev); + + $it = new EventIterator($vcal, $ev->UID); + + // Max is to prevent overflow + $max = 12; + $result = []; + foreach ($it as $item) { + + $result[] = $item; + $max--; + + if (!$max) break; + + } + + $tz = new DateTimeZone('UTC'); + + $this->assertEquals( + [ + new DateTimeImmutable('2011-10-07', $tz), + ], + $result + ); + + } + + /** + * @depends testValues + */ + function testDailyByDayByHour() { + + $vcal = new VCalendar(); + $ev = $vcal->createComponent('VEVENT'); + + $ev->UID = 'bla'; + $ev->RRULE = 'FREQ=DAILY;BYDAY=SA,SU;BYHOUR=6,7'; + $dtStart = $vcal->createProperty('DTSTART'); + $dtStart->setDateTime(new DateTimeImmutable('2011-10-08 06:00:00', new DateTimeZone('UTC'))); + + $ev->add($dtStart); + + $vcal->add($ev); + + $it = new EventIterator($vcal, (string)$ev->UID); + + // Grabbing the next 12 items + $max = 12; + $result = []; + foreach ($it as $item) { + + $result[] = $item; + $max--; + + if (!$max) break; + + } + + $tz = new DateTimeZone('UTC'); + + $this->assertEquals( + [ + new DateTimeImmutable('2011-10-08 06:00:00', $tz), + new DateTimeImmutable('2011-10-08 07:00:00', $tz), + new DateTimeImmutable('2011-10-09 06:00:00', $tz), + new DateTimeImmutable('2011-10-09 07:00:00', $tz), + new DateTimeImmutable('2011-10-15 06:00:00', $tz), + new DateTimeImmutable('2011-10-15 07:00:00', $tz), + new DateTimeImmutable('2011-10-16 06:00:00', $tz), + new DateTimeImmutable('2011-10-16 07:00:00', $tz), + new DateTimeImmutable('2011-10-22 06:00:00', $tz), + new DateTimeImmutable('2011-10-22 07:00:00', $tz), + new DateTimeImmutable('2011-10-23 06:00:00', $tz), + new DateTimeImmutable('2011-10-23 07:00:00', $tz), + ], + $result + ); + + } + + /** + * @depends testValues + */ + function testDailyByHour() { + + $vcal = new VCalendar(); + $ev = $vcal->createComponent('VEVENT'); + + $ev->UID = 'bla'; + $ev->RRULE = 'FREQ=DAILY;INTERVAL=2;BYHOUR=10,11,12,13,14,15'; + $dtStart = $vcal->createProperty('DTSTART'); + $dtStart->setDateTime(new DateTimeImmutable('2012-10-11 12:00:00', new DateTimeZone('UTC'))); + + $ev->add($dtStart); + + $vcal->add($ev); + + $it = new EventIterator($vcal, (string)$ev->UID); + + // Grabbing the next 12 items + $max = 12; + $result = []; + foreach ($it as $item) { + + $result[] = $item; + $max--; + + if (!$max) break; + + } + + $tz = new DateTimeZone('UTC'); + + $this->assertEquals( + [ + new DateTimeImmutable('2012-10-11 12:00:00', $tz), + new DateTimeImmutable('2012-10-11 13:00:00', $tz), + new DateTimeImmutable('2012-10-11 14:00:00', $tz), + new DateTimeImmutable('2012-10-11 15:00:00', $tz), + new DateTimeImmutable('2012-10-13 10:00:00', $tz), + new DateTimeImmutable('2012-10-13 11:00:00', $tz), + new DateTimeImmutable('2012-10-13 12:00:00', $tz), + new DateTimeImmutable('2012-10-13 13:00:00', $tz), + new DateTimeImmutable('2012-10-13 14:00:00', $tz), + new DateTimeImmutable('2012-10-13 15:00:00', $tz), + new DateTimeImmutable('2012-10-15 10:00:00', $tz), + new DateTimeImmutable('2012-10-15 11:00:00', $tz), + ], + $result + ); + + } + + /** + * @depends testValues + */ + function testDailyByDay() { + + $vcal = new VCalendar(); + $ev = $vcal->createComponent('VEVENT'); + + $ev->UID = 'bla'; + $ev->RRULE = 'FREQ=DAILY;INTERVAL=2;BYDAY=TU,WE,FR'; + $dtStart = $vcal->createProperty('DTSTART'); + $dtStart->setDateTime(new DateTimeImmutable('2011-10-07', new DateTimeZone('UTC'))); + + $ev->add($dtStart); + + $vcal->add($ev); + + $it = new EventIterator($vcal, (string)$ev->UID); + + // Grabbing the next 12 items + $max = 12; + $result = []; + foreach ($it as $item) { + + $result[] = $item; + $max--; + + if (!$max) break; + + } + + $tz = new DateTimeZone('UTC'); + + $this->assertEquals( + [ + new DateTimeImmutable('2011-10-07', $tz), + new DateTimeImmutable('2011-10-11', $tz), + new DateTimeImmutable('2011-10-19', $tz), + new DateTimeImmutable('2011-10-21', $tz), + new DateTimeImmutable('2011-10-25', $tz), + new DateTimeImmutable('2011-11-02', $tz), + new DateTimeImmutable('2011-11-04', $tz), + new DateTimeImmutable('2011-11-08', $tz), + new DateTimeImmutable('2011-11-16', $tz), + new DateTimeImmutable('2011-11-18', $tz), + new DateTimeImmutable('2011-11-22', $tz), + new DateTimeImmutable('2011-11-30', $tz), + ], + $result + ); + + } + + /** + * @depends testValues + */ + function testWeekly() { + + $vcal = new VCalendar(); + $ev = $vcal->createComponent('VEVENT'); + + $ev->UID = 'bla'; + $ev->RRULE = 'FREQ=WEEKLY;INTERVAL=2;COUNT=10'; + $dtStart = $vcal->createProperty('DTSTART'); + $dtStart->setDateTime(new DateTimeImmutable('2011-10-07', new DateTimeZone('UTC'))); + + $ev->add($dtStart); + + $vcal->add($ev); + + $it = new EventIterator($vcal, (string)$ev->UID); + + // Max is to prevent overflow + $max = 12; + $result = []; + foreach ($it as $item) { + + $result[] = $item; + $max--; + + if (!$max) break; + + } + + $tz = new DateTimeZone('UTC'); + + $this->assertEquals( + [ + new DateTimeImmutable('2011-10-07', $tz), + new DateTimeImmutable('2011-10-21', $tz), + new DateTimeImmutable('2011-11-04', $tz), + new DateTimeImmutable('2011-11-18', $tz), + new DateTimeImmutable('2011-12-02', $tz), + new DateTimeImmutable('2011-12-16', $tz), + new DateTimeImmutable('2011-12-30', $tz), + new DateTimeImmutable('2012-01-13', $tz), + new DateTimeImmutable('2012-01-27', $tz), + new DateTimeImmutable('2012-02-10', $tz), + ], + $result + ); + + } + + /** + * @depends testValues + */ + function testWeeklyByDayByHour() { + + $vcal = new VCalendar(); + $ev = $vcal->createComponent('VEVENT'); + + $ev->UID = 'bla'; + $ev->RRULE = 'FREQ=WEEKLY;INTERVAL=2;BYDAY=TU,WE,FR;WKST=MO;BYHOUR=8,9,10'; + $dtStart = $vcal->createProperty('DTSTART'); + $dtStart->setDateTime(new DateTimeImmutable('2011-10-07 08:00:00', new DateTimeZone('UTC'))); + + $ev->add($dtStart); + + $vcal->add($ev); + + $it = new EventIterator($vcal, (string)$ev->UID); + + // Grabbing the next 12 items + $max = 15; + $result = []; + foreach ($it as $item) { + + $result[] = $item; + $max--; + + if (!$max) break; + + } + + $tz = new DateTimeZone('UTC'); + + $this->assertEquals( + [ + new DateTimeImmutable('2011-10-07 08:00:00', $tz), + new DateTimeImmutable('2011-10-07 09:00:00', $tz), + new DateTimeImmutable('2011-10-07 10:00:00', $tz), + new DateTimeImmutable('2011-10-18 08:00:00', $tz), + new DateTimeImmutable('2011-10-18 09:00:00', $tz), + new DateTimeImmutable('2011-10-18 10:00:00', $tz), + new DateTimeImmutable('2011-10-19 08:00:00', $tz), + new DateTimeImmutable('2011-10-19 09:00:00', $tz), + new DateTimeImmutable('2011-10-19 10:00:00', $tz), + new DateTimeImmutable('2011-10-21 08:00:00', $tz), + new DateTimeImmutable('2011-10-21 09:00:00', $tz), + new DateTimeImmutable('2011-10-21 10:00:00', $tz), + new DateTimeImmutable('2011-11-01 08:00:00', $tz), + new DateTimeImmutable('2011-11-01 09:00:00', $tz), + new DateTimeImmutable('2011-11-01 10:00:00', $tz), + ], + $result + ); + + } + + /** + * @depends testValues + */ + function testWeeklyByDaySpecificHour() { + + $vcal = new VCalendar(); + $ev = $vcal->createComponent('VEVENT'); + + $ev->UID = 'bla'; + $ev->RRULE = 'FREQ=WEEKLY;INTERVAL=2;BYDAY=TU,WE,FR;WKST=SU'; + $dtStart = $vcal->createProperty('DTSTART'); + $dtStart->setDateTime(new DateTimeImmutable('2011-10-07 18:00:00', new DateTimeZone('UTC'))); + + $ev->add($dtStart); + + $vcal->add($ev); + + $it = new EventIterator($vcal, (string)$ev->UID); + + // Grabbing the next 12 items + $max = 12; + $result = []; + foreach ($it as $item) { + + $result[] = $item; + $max--; + + if (!$max) break; + + } + + $tz = new DateTimeZone('UTC'); + + $this->assertEquals( + [ + new DateTimeImmutable('2011-10-07 18:00:00', $tz), + new DateTimeImmutable('2011-10-18 18:00:00', $tz), + new DateTimeImmutable('2011-10-19 18:00:00', $tz), + new DateTimeImmutable('2011-10-21 18:00:00', $tz), + new DateTimeImmutable('2011-11-01 18:00:00', $tz), + new DateTimeImmutable('2011-11-02 18:00:00', $tz), + new DateTimeImmutable('2011-11-04 18:00:00', $tz), + new DateTimeImmutable('2011-11-15 18:00:00', $tz), + new DateTimeImmutable('2011-11-16 18:00:00', $tz), + new DateTimeImmutable('2011-11-18 18:00:00', $tz), + new DateTimeImmutable('2011-11-29 18:00:00', $tz), + new DateTimeImmutable('2011-11-30 18:00:00', $tz), + ], + $result + ); + + } + + /** + * @depends testValues + */ + function testWeeklyByDay() { + + $vcal = new VCalendar(); + $ev = $vcal->createComponent('VEVENT'); + + $ev->UID = 'bla'; + $ev->RRULE = 'FREQ=WEEKLY;INTERVAL=2;BYDAY=TU,WE,FR;WKST=SU'; + $dtStart = $vcal->createProperty('DTSTART'); + $dtStart->setDateTime(new DateTimeImmutable('2011-10-07', new DateTimeZone('UTC'))); + + $ev->add($dtStart); + + $vcal->add($ev); + + $it = new EventIterator($vcal, (string)$ev->UID); + + // Grabbing the next 12 items + $max = 12; + $result = []; + foreach ($it as $item) { + + $result[] = $item; + $max--; + + if (!$max) break; + + } + + $tz = new DateTimeZone('UTC'); + + $this->assertEquals( + [ + new DateTimeImmutable('2011-10-07', $tz), + new DateTimeImmutable('2011-10-18', $tz), + new DateTimeImmutable('2011-10-19', $tz), + new DateTimeImmutable('2011-10-21', $tz), + new DateTimeImmutable('2011-11-01', $tz), + new DateTimeImmutable('2011-11-02', $tz), + new DateTimeImmutable('2011-11-04', $tz), + new DateTimeImmutable('2011-11-15', $tz), + new DateTimeImmutable('2011-11-16', $tz), + new DateTimeImmutable('2011-11-18', $tz), + new DateTimeImmutable('2011-11-29', $tz), + new DateTimeImmutable('2011-11-30', $tz), + ], + $result + ); + + } + + /** + * @depends testValues + */ + function testMonthly() { + + $vcal = new VCalendar(); + $ev = $vcal->createComponent('VEVENT'); + + $ev->UID = 'bla'; + $ev->RRULE = 'FREQ=MONTHLY;INTERVAL=3;COUNT=5'; + $dtStart = $vcal->createProperty('DTSTART'); + $dtStart->setDateTime(new DateTimeImmutable('2011-12-05', new DateTimeZone('UTC'))); + + $ev->add($dtStart); + + $vcal->add($ev); + + $it = new EventIterator($vcal, (string)$ev->UID); + + $max = 14; + $result = []; + foreach ($it as $item) { + + $result[] = $item; + $max--; + + if (!$max) break; + + } + + $tz = new DateTimeZone('UTC'); + + $this->assertEquals( + [ + new DateTimeImmutable('2011-12-05', $tz), + new DateTimeImmutable('2012-03-05', $tz), + new DateTimeImmutable('2012-06-05', $tz), + new DateTimeImmutable('2012-09-05', $tz), + new DateTimeImmutable('2012-12-05', $tz), + ], + $result + ); + + + } + + /** + * @depends testValues + */ + function testMonthlyEndOfMonth() { + + $vcal = new VCalendar(); + $ev = $vcal->createComponent('VEVENT'); + + $ev->UID = 'bla'; + $ev->RRULE = 'FREQ=MONTHLY;INTERVAL=2;COUNT=12'; + $dtStart = $vcal->createProperty('DTSTART'); + $dtStart->setDateTime(new DateTimeImmutable('2011-12-31', new DateTimeZone('UTC'))); + + $ev->add($dtStart); + + $vcal->add($ev); + + $it = new EventIterator($vcal, (string)$ev->UID); + + $max = 14; + $result = []; + foreach ($it as $item) { + + $result[] = $item; + $max--; + + if (!$max) break; + + } + + $tz = new DateTimeZone('UTC'); + + $this->assertEquals( + [ + new DateTimeImmutable('2011-12-31', $tz), + new DateTimeImmutable('2012-08-31', $tz), + new DateTimeImmutable('2012-10-31', $tz), + new DateTimeImmutable('2012-12-31', $tz), + new DateTimeImmutable('2013-08-31', $tz), + new DateTimeImmutable('2013-10-31', $tz), + new DateTimeImmutable('2013-12-31', $tz), + new DateTimeImmutable('2014-08-31', $tz), + new DateTimeImmutable('2014-10-31', $tz), + new DateTimeImmutable('2014-12-31', $tz), + new DateTimeImmutable('2015-08-31', $tz), + new DateTimeImmutable('2015-10-31', $tz), + ], + $result + ); + + + } + + /** + * @depends testValues + */ + function testMonthlyByMonthDay() { + + $vcal = new VCalendar(); + $ev = $vcal->createComponent('VEVENT'); + + $ev->UID = 'bla'; + $ev->RRULE = 'FREQ=MONTHLY;INTERVAL=5;COUNT=9;BYMONTHDAY=1,31,-7'; + $dtStart = $vcal->createProperty('DTSTART'); + $dtStart->setDateTime(new DateTimeImmutable('2011-01-01', new DateTimeZone('UTC'))); + + $ev->add($dtStart); + + $vcal->add($ev); + + $it = new EventIterator($vcal, (string)$ev->UID); + + $max = 14; + $result = []; + foreach ($it as $item) { + + $result[] = $item; + $max--; + + if (!$max) break; + + } + + $tz = new DateTimeZone('UTC'); + + $this->assertEquals( + [ + new DateTimeImmutable('2011-01-01', $tz), + new DateTimeImmutable('2011-01-25', $tz), + new DateTimeImmutable('2011-01-31', $tz), + new DateTimeImmutable('2011-06-01', $tz), + new DateTimeImmutable('2011-06-24', $tz), + new DateTimeImmutable('2011-11-01', $tz), + new DateTimeImmutable('2011-11-24', $tz), + new DateTimeImmutable('2012-04-01', $tz), + new DateTimeImmutable('2012-04-24', $tz), + ], + $result + ); + + } + + /** + * A pretty slow test. Had to be marked as 'medium' for phpunit to not die + * after 1 second. Would be good to optimize later. + * + * @depends testValues + * @medium + */ + function testMonthlyByDay() { + + $vcal = new VCalendar(); + $ev = $vcal->createComponent('VEVENT'); + + $ev->UID = 'bla'; + $ev->RRULE = 'FREQ=MONTHLY;INTERVAL=2;COUNT=16;BYDAY=MO,-2TU,+1WE,3TH'; + $dtStart = $vcal->createProperty('DTSTART'); + $dtStart->setDateTime(new DateTimeImmutable('2011-01-03', new DateTimeZone('UTC'))); + + $ev->add($dtStart); + + $vcal->add($ev); + + $it = new EventIterator($vcal, (string)$ev->UID); + + $max = 20; + $result = []; + foreach ($it as $item) { + + $result[] = $item; + $max--; + + if (!$max) break; + + } + + $tz = new DateTimeZone('UTC'); + + $this->assertEquals( + [ + new DateTimeImmutable('2011-01-03', $tz), + new DateTimeImmutable('2011-01-05', $tz), + new DateTimeImmutable('2011-01-10', $tz), + new DateTimeImmutable('2011-01-17', $tz), + new DateTimeImmutable('2011-01-18', $tz), + new DateTimeImmutable('2011-01-20', $tz), + new DateTimeImmutable('2011-01-24', $tz), + new DateTimeImmutable('2011-01-31', $tz), + new DateTimeImmutable('2011-03-02', $tz), + new DateTimeImmutable('2011-03-07', $tz), + new DateTimeImmutable('2011-03-14', $tz), + new DateTimeImmutable('2011-03-17', $tz), + new DateTimeImmutable('2011-03-21', $tz), + new DateTimeImmutable('2011-03-22', $tz), + new DateTimeImmutable('2011-03-28', $tz), + new DateTimeImmutable('2011-05-02', $tz), + ], + $result + ); + + } + + /** + * @depends testValues + */ + function testMonthlyByDayByMonthDay() { + + $vcal = new VCalendar(); + $ev = $vcal->createComponent('VEVENT'); + + $ev->UID = 'bla'; + $ev->RRULE = 'FREQ=MONTHLY;COUNT=10;BYDAY=MO;BYMONTHDAY=1'; + $dtStart = $vcal->createProperty('DTSTART'); + $dtStart->setDateTime(new DateTimeImmutable('2011-08-01', new DateTimeZone('UTC'))); + + $ev->add($dtStart); + + $vcal->add($ev); + + $it = new EventIterator($vcal, (string)$ev->UID); + + $max = 20; + $result = []; + foreach ($it as $item) { + + $result[] = $item; + $max--; + + if (!$max) break; + + } + + $tz = new DateTimeZone('UTC'); + + $this->assertEquals( + [ + new DateTimeImmutable('2011-08-01', $tz), + new DateTimeImmutable('2012-10-01', $tz), + new DateTimeImmutable('2013-04-01', $tz), + new DateTimeImmutable('2013-07-01', $tz), + new DateTimeImmutable('2014-09-01', $tz), + new DateTimeImmutable('2014-12-01', $tz), + new DateTimeImmutable('2015-06-01', $tz), + new DateTimeImmutable('2016-02-01', $tz), + new DateTimeImmutable('2016-08-01', $tz), + new DateTimeImmutable('2017-05-01', $tz), + ], + $result + ); + + } + + /** + * @depends testValues + */ + function testMonthlyByDayBySetPos() { + + $vcal = new VCalendar(); + $ev = $vcal->createComponent('VEVENT'); + + $ev->UID = 'bla'; + $ev->RRULE = 'FREQ=MONTHLY;COUNT=10;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=1,-1'; + $dtStart = $vcal->createProperty('DTSTART'); + $dtStart->setDateTime(new DateTimeImmutable('2011-01-03', new DateTimeZone('UTC'))); + + $ev->add($dtStart); + + $vcal->add($ev); + + $it = new EventIterator($vcal, (string)$ev->UID); + + $max = 20; + $result = []; + foreach ($it as $item) { + + $result[] = $item; + $max--; + + if (!$max) break; + + } + + $tz = new DateTimeZone('UTC'); + + $this->assertEquals( + [ + new DateTimeImmutable('2011-01-03', $tz), + new DateTimeImmutable('2011-01-31', $tz), + new DateTimeImmutable('2011-02-01', $tz), + new DateTimeImmutable('2011-02-28', $tz), + new DateTimeImmutable('2011-03-01', $tz), + new DateTimeImmutable('2011-03-31', $tz), + new DateTimeImmutable('2011-04-01', $tz), + new DateTimeImmutable('2011-04-29', $tz), + new DateTimeImmutable('2011-05-02', $tz), + new DateTimeImmutable('2011-05-31', $tz), + ], + $result + ); + + } + + /** + * @depends testValues + */ + function testYearly() { + + $vcal = new VCalendar(); + $ev = $vcal->createComponent('VEVENT'); + + $ev->UID = 'bla'; + $ev->RRULE = 'FREQ=YEARLY;COUNT=10;INTERVAL=3'; + $dtStart = $vcal->createProperty('DTSTART'); + $dtStart->setDateTime(new DateTimeImmutable('2011-01-01', new DateTimeZone('UTC'))); + + $ev->add($dtStart); + + $vcal->add($ev); + + $it = new EventIterator($vcal, (string)$ev->UID); + + $max = 20; + $result = []; + foreach ($it as $item) { + + $result[] = $item; + $max--; + + if (!$max) break; + + } + + $tz = new DateTimeZone('UTC'); + + $this->assertEquals( + [ + new DateTimeImmutable('2011-01-01', $tz), + new DateTimeImmutable('2014-01-01', $tz), + new DateTimeImmutable('2017-01-01', $tz), + new DateTimeImmutable('2020-01-01', $tz), + new DateTimeImmutable('2023-01-01', $tz), + new DateTimeImmutable('2026-01-01', $tz), + new DateTimeImmutable('2029-01-01', $tz), + new DateTimeImmutable('2032-01-01', $tz), + new DateTimeImmutable('2035-01-01', $tz), + new DateTimeImmutable('2038-01-01', $tz), + ], + $result + ); + + } + + /** + * @depends testValues + */ + function testYearlyLeapYear() { + + $vcal = new VCalendar(); + $ev = $vcal->createComponent('VEVENT'); + + $ev->UID = 'bla'; + $ev->RRULE = 'FREQ=YEARLY;COUNT=3'; + $dtStart = $vcal->createProperty('DTSTART'); + $dtStart->setDateTime(new DateTimeImmutable('2012-02-29', new DateTimeZone('UTC'))); + + $ev->add($dtStart); + + $vcal->add($ev); + + $it = new EventIterator($vcal, (string)$ev->UID); + + $max = 20; + $result = []; + foreach ($it as $item) { + + $result[] = $item; + $max--; + + if (!$max) break; + + } + + $tz = new DateTimeZone('UTC'); + + $this->assertEquals( + [ + new DateTimeImmutable('2012-02-29', $tz), + new DateTimeImmutable('2016-02-29', $tz), + new DateTimeImmutable('2020-02-29', $tz), + ], + $result + ); + + } + + /** + * @depends testValues + */ + function testYearlyByMonth() { + + $vcal = new VCalendar(); + $ev = $vcal->createComponent('VEVENT'); + + $ev->UID = 'bla'; + $ev->RRULE = 'FREQ=YEARLY;COUNT=8;INTERVAL=4;BYMONTH=4,10'; + $dtStart = $vcal->createProperty('DTSTART'); + $dtStart->setDateTime(new DateTimeImmutable('2011-04-07', new DateTimeZone('UTC'))); + + $ev->add($dtStart); + + $vcal->add($ev); + + $it = new EventIterator($vcal, (string)$ev->UID); + + $max = 20; + $result = []; + foreach ($it as $item) { + + $result[] = $item; + $max--; + + if (!$max) break; + + } + + $tz = new DateTimeZone('UTC'); + + $this->assertEquals( + [ + new DateTimeImmutable('2011-04-07', $tz), + new DateTimeImmutable('2011-10-07', $tz), + new DateTimeImmutable('2015-04-07', $tz), + new DateTimeImmutable('2015-10-07', $tz), + new DateTimeImmutable('2019-04-07', $tz), + new DateTimeImmutable('2019-10-07', $tz), + new DateTimeImmutable('2023-04-07', $tz), + new DateTimeImmutable('2023-10-07', $tz), + ], + $result + ); + + } + + /** + * @depends testValues + */ + function testYearlyByMonthByDay() { + + $vcal = new VCalendar(); + $ev = $vcal->createComponent('VEVENT'); + + $ev->UID = 'bla'; + $ev->RRULE = 'FREQ=YEARLY;COUNT=8;INTERVAL=5;BYMONTH=4,10;BYDAY=1MO,-1SU'; + $dtStart = $vcal->createProperty('DTSTART'); + $dtStart->setDateTime(new DateTimeImmutable('2011-04-04', new DateTimeZone('UTC'))); + + $ev->add($dtStart); + + $vcal->add($ev); + + $it = new EventIterator($vcal, (string)$ev->UID); + + $max = 20; + $result = []; + foreach ($it as $item) { + + $result[] = $item; + $max--; + + if (!$max) break; + + } + + $tz = new DateTimeZone('UTC'); + + $this->assertEquals( + [ + new DateTimeImmutable('2011-04-04', $tz), + new DateTimeImmutable('2011-04-24', $tz), + new DateTimeImmutable('2011-10-03', $tz), + new DateTimeImmutable('2011-10-30', $tz), + new DateTimeImmutable('2016-04-04', $tz), + new DateTimeImmutable('2016-04-24', $tz), + new DateTimeImmutable('2016-10-03', $tz), + new DateTimeImmutable('2016-10-30', $tz), + ], + $result + ); + + } + + /** + * @depends testValues + */ + function testFastForward() { + + $vcal = new VCalendar(); + $ev = $vcal->createComponent('VEVENT'); + + $ev->UID = 'bla'; + $ev->RRULE = 'FREQ=YEARLY;COUNT=8;INTERVAL=5;BYMONTH=4,10;BYDAY=1MO,-1SU'; + $dtStart = $vcal->createProperty('DTSTART'); + $dtStart->setDateTime(new DateTimeImmutable('2011-04-04', new DateTimeZone('UTC'))); + + $ev->add($dtStart); + + $vcal->add($ev); + + $it = new EventIterator($vcal, (string)$ev->UID); + + // The idea is that we're fast-forwarding too far in the future, so + // there will be no results left. + $it->fastForward(new DateTimeImmutable('2020-05-05', new DateTimeZone('UTC'))); + + $max = 20; + $result = []; + while ($item = $it->current()) { + + $result[] = $item; + $max--; + + if (!$max) break; + $it->next(); + + } + + $this->assertEquals([], $result); + + } + + /** + * @depends testValues + */ + function testFastForwardAllDayEventThatStopAtTheStartTime() { + $vcal = new VCalendar(); + $ev = $vcal->createComponent('VEVENT'); + + $ev->UID = 'bla'; + $ev->RRULE = 'FREQ=DAILY'; + + $dtStart = $vcal->createProperty('DTSTART'); + $dtStart->setDateTime(new DateTimeImmutable('2011-04-04', new DateTimeZone('UTC'))); + $ev->add($dtStart); + + $dtEnd = $vcal->createProperty('DTSTART'); + $dtEnd->setDateTime(new DateTimeImmutable('2011-04-05', new DateTimeZone('UTC'))); + $ev->add($dtEnd); + + $vcal->add($ev); + + $it = new EventIterator($vcal, (string)$ev->UID); + + $it->fastForward(new DateTimeImmutable('2011-04-05T000000', new DateTimeZone('UTC'))); + + $this->assertEquals(new DateTimeImmutable('2011-04-06'), $it->getDTStart()); + } + + /** + * @depends testValues + */ + function testComplexExclusions() { + + $vcal = new VCalendar(); + $ev = $vcal->createComponent('VEVENT'); + + $ev->UID = 'bla'; + $ev->RRULE = 'FREQ=YEARLY;COUNT=10'; + $dtStart = $vcal->createProperty('DTSTART'); + + $tz = new DateTimeZone('Canada/Eastern'); + $dtStart->setDateTime(new DateTimeImmutable('2011-01-01 13:50:20', $tz)); + + $exDate1 = $vcal->createProperty('EXDATE'); + $exDate1->setDateTimes([new DateTimeImmutable('2012-01-01 13:50:20', $tz), new DateTimeImmutable('2014-01-01 13:50:20', $tz)]); + $exDate2 = $vcal->createProperty('EXDATE'); + $exDate2->setDateTimes([new DateTimeImmutable('2016-01-01 13:50:20', $tz)]); + + $ev->add($dtStart); + $ev->add($exDate1); + $ev->add($exDate2); + + $vcal->add($ev); + + $it = new EventIterator($vcal, (string)$ev->UID); + + $max = 20; + $result = []; + foreach ($it as $item) { + + $result[] = $item; + $max--; + + if (!$max) break; + + } + + $this->assertEquals( + [ + new DateTimeImmutable('2011-01-01 13:50:20', $tz), + new DateTimeImmutable('2013-01-01 13:50:20', $tz), + new DateTimeImmutable('2015-01-01 13:50:20', $tz), + new DateTimeImmutable('2017-01-01 13:50:20', $tz), + new DateTimeImmutable('2018-01-01 13:50:20', $tz), + new DateTimeImmutable('2019-01-01 13:50:20', $tz), + new DateTimeImmutable('2020-01-01 13:50:20', $tz), + ], + $result + ); + + } + + /** + * @depends testValues + */ + function testOverridenEvent() { + + $vcal = new VCalendar(); + + $ev1 = $vcal->createComponent('VEVENT'); + $ev1->UID = 'overridden'; + $ev1->RRULE = 'FREQ=DAILY;COUNT=10'; + $ev1->DTSTART = '20120107T120000Z'; + $ev1->SUMMARY = 'baseEvent'; + + $vcal->add($ev1); + + // ev2 overrides an event, and puts it on 2pm instead. + $ev2 = $vcal->createComponent('VEVENT'); + $ev2->UID = 'overridden'; + $ev2->{'RECURRENCE-ID'} = '20120110T120000Z'; + $ev2->DTSTART = '20120110T140000Z'; + $ev2->SUMMARY = 'Event 2'; + + $vcal->add($ev2); + + // ev3 overrides an event, and puts it 2 days and 2 hours later + $ev3 = $vcal->createComponent('VEVENT'); + $ev3->UID = 'overridden'; + $ev3->{'RECURRENCE-ID'} = '20120113T120000Z'; + $ev3->DTSTART = '20120115T140000Z'; + $ev3->SUMMARY = 'Event 3'; + + $vcal->add($ev3); + + $it = new EventIterator($vcal, 'overridden'); + + $dates = []; + $summaries = []; + while ($it->valid()) { + + $dates[] = $it->getDTStart(); + $summaries[] = (string)$it->getEventObject()->SUMMARY; + $it->next(); + + } + + $tz = new DateTimeZone('UTC'); + $this->assertEquals([ + new DateTimeImmutable('2012-01-07 12:00:00', $tz), + new DateTimeImmutable('2012-01-08 12:00:00', $tz), + new DateTimeImmutable('2012-01-09 12:00:00', $tz), + new DateTimeImmutable('2012-01-10 14:00:00', $tz), + new DateTimeImmutable('2012-01-11 12:00:00', $tz), + new DateTimeImmutable('2012-01-12 12:00:00', $tz), + new DateTimeImmutable('2012-01-14 12:00:00', $tz), + new DateTimeImmutable('2012-01-15 12:00:00', $tz), + new DateTimeImmutable('2012-01-15 14:00:00', $tz), + new DateTimeImmutable('2012-01-16 12:00:00', $tz), + ], $dates); + + $this->assertEquals([ + 'baseEvent', + 'baseEvent', + 'baseEvent', + 'Event 2', + 'baseEvent', + 'baseEvent', + 'baseEvent', + 'baseEvent', + 'Event 3', + 'baseEvent', + ], $summaries); + + } + + /** + * @depends testValues + */ + function testOverridenEvent2() { + + $vcal = new VCalendar(); + + $ev1 = $vcal->createComponent('VEVENT'); + $ev1->UID = 'overridden'; + $ev1->RRULE = 'FREQ=WEEKLY;COUNT=3'; + $ev1->DTSTART = '20120112T120000Z'; + $ev1->SUMMARY = 'baseEvent'; + + $vcal->add($ev1); + + // ev2 overrides an event, and puts it 6 days earlier instead. + $ev2 = $vcal->createComponent('VEVENT'); + $ev2->UID = 'overridden'; + $ev2->{'RECURRENCE-ID'} = '20120119T120000Z'; + $ev2->DTSTART = '20120113T120000Z'; + $ev2->SUMMARY = 'Override!'; + + $vcal->add($ev2); + + $it = new EventIterator($vcal, 'overridden'); + + $dates = []; + $summaries = []; + while ($it->valid()) { + + $dates[] = $it->getDTStart(); + $summaries[] = (string)$it->getEventObject()->SUMMARY; + $it->next(); + + } + + $tz = new DateTimeZone('UTC'); + $this->assertEquals([ + new DateTimeImmutable('2012-01-12 12:00:00', $tz), + new DateTimeImmutable('2012-01-13 12:00:00', $tz), + new DateTimeImmutable('2012-01-26 12:00:00', $tz), + + ], $dates); + + $this->assertEquals([ + 'baseEvent', + 'Override!', + 'baseEvent', + ], $summaries); + + } + + /** + * @depends testValues + */ + function testOverridenEventNoValuesExpected() { + + $vcal = new VCalendar(); + $ev1 = $vcal->createComponent('VEVENT'); + + $ev1->UID = 'overridden'; + $ev1->RRULE = 'FREQ=WEEKLY;COUNT=3'; + $ev1->DTSTART = '20120124T120000Z'; + $ev1->SUMMARY = 'baseEvent'; + + $vcal->add($ev1); + + // ev2 overrides an event, and puts it 6 days earlier instead. + $ev2 = $vcal->createComponent('VEVENT'); + $ev2->UID = 'overridden'; + $ev2->{'RECURRENCE-ID'} = '20120131T120000Z'; + $ev2->DTSTART = '20120125T120000Z'; + $ev2->SUMMARY = 'Override!'; + + $vcal->add($ev2); + + $it = new EventIterator($vcal, 'overridden'); + + $dates = []; + $summaries = []; + + // The reported problem was specifically related to the VCALENDAR + // expansion. In this parcitular case, we had to forward to the 28th of + // january. + $it->fastForward(new DateTimeImmutable('2012-01-28 23:00:00')); + + // We stop the loop when it hits the 6th of februari. Normally this + // iterator would hit 24, 25 (overriden from 31) and 7 feb but because + // we 'filter' from the 28th till the 6th, we should get 0 results. + while ($it->valid() && $it->getDTStart() < new DateTimeImmutable('2012-02-06 23:00:00')) { + + $dates[] = $it->getDTStart(); + $summaries[] = (string)$it->getEventObject()->SUMMARY; + $it->next(); + + } + + $this->assertEquals([], $dates); + $this->assertEquals([], $summaries); + + } + + /** + * @depends testValues + */ + function testRDATE() { + + $vcal = new VCalendar(); + $ev = $vcal->createComponent('VEVENT'); + + $ev->UID = 'bla'; + $ev->RDATE = [ + new DateTimeImmutable('2014-08-07', new DateTimeZone('UTC')), + new DateTimeImmutable('2014-08-08', new DateTimeZone('UTC')), + ]; + $dtStart = $vcal->createProperty('DTSTART'); + $dtStart->setDateTime(new DateTimeImmutable('2011-10-07', new DateTimeZone('UTC'))); + + $ev->add($dtStart); + + $vcal->add($ev); + + $it = new EventIterator($vcal, $ev->UID); + + // Max is to prevent overflow + $max = 12; + $result = []; + foreach ($it as $item) { + + $result[] = $item; + $max--; + + if (!$max) break; + + } + + $tz = new DateTimeZone('UTC'); + + $this->assertEquals( + [ + new DateTimeImmutable('2011-10-07', $tz), + new DateTimeImmutable('2014-08-07', $tz), + new DateTimeImmutable('2014-08-08', $tz), + ], + $result + ); + + } + + /** + * @depends testValues + * @expectedException \InvalidArgumentException + */ + function testNoMasterBadUID() { + + $vcal = new VCalendar(); + // ev2 overrides an event, and puts it on 2pm instead. + $ev2 = $vcal->createComponent('VEVENT'); + $ev2->UID = 'overridden'; + $ev2->{'RECURRENCE-ID'} = '20120110T120000Z'; + $ev2->DTSTART = '20120110T140000Z'; + $ev2->SUMMARY = 'Event 2'; + + $vcal->add($ev2); + + // ev3 overrides an event, and puts it 2 days and 2 hours later + $ev3 = $vcal->createComponent('VEVENT'); + $ev3->UID = 'overridden'; + $ev3->{'RECURRENCE-ID'} = '20120113T120000Z'; + $ev3->DTSTART = '20120115T140000Z'; + $ev3->SUMMARY = 'Event 3'; + + $vcal->add($ev3); + + $it = new EventIterator($vcal, 'broken'); + + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/MaxInstancesTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/MaxInstancesTest.php new file mode 100644 index 00000000000..1e94032a308 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/MaxInstancesTest.php @@ -0,0 +1,41 @@ +expand(new DateTime('2014-08-01'), new DateTime('2014-09-01')); + + } finally { + Settings::$maxRecurrences = $temp; + } + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/MissingOverriddenTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/MissingOverriddenTest.php new file mode 100644 index 00000000000..fc8c518dcda --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/MissingOverriddenTest.php @@ -0,0 +1,62 @@ +assertInstanceOf('Sabre\\VObject\\Component\\VCalendar', $vcal); + + $vcal = $vcal->expand(new DateTime('2011-01-01'), new DateTime('2015-01-01')); + + $output = <<assertVObjectEqualsVObject($output, $vcal); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/NoInstancesTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/NoInstancesTest.php new file mode 100644 index 00000000000..84c7ec13e61 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/NoInstancesTest.php @@ -0,0 +1,40 @@ +assertInstanceOf('Sabre\\VObject\\Component\\VCalendar', $vcal); + + $it = new EventIterator($vcal, 'foo'); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/OverrideFirstEventTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/OverrideFirstEventTest.php new file mode 100644 index 00000000000..78c0782c877 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/OverrideFirstEventTest.php @@ -0,0 +1,121 @@ +expand(new DateTime('2014-08-01'), new DateTime('2014-09-01')); + + $expected = <<assertVObjectEqualsVObject( + $expected, + $vcal + ); + + + } + + function testRemoveFirstEvent() { + + $input = <<expand(new DateTime('2014-08-01'), new DateTime('2014-08-19')); + + $expected = <<assertVObjectEqualsVObject( + $expected, + $vcal + ); + + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/SameDateForRecurringEventsTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/SameDateForRecurringEventsTest.php new file mode 100644 index 00000000000..b89f25aaa49 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/EventIterator/SameDateForRecurringEventsTest.php @@ -0,0 +1,55 @@ +getComponents()); + + $this->assertEquals(4, iterator_count($eventIterator), 'in ICS 4 events'); + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/RDateIteratorTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/RDateIteratorTest.php new file mode 100644 index 00000000000..e2852dc6986 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/RDateIteratorTest.php @@ -0,0 +1,78 @@ +assertEquals( + $expected, + iterator_to_array($it) + ); + + $this->assertFalse($it->isInfinite()); + + } + + function testTimezone() { + + $tz = new DateTimeZone('Europe/Berlin'); + $it = new RDateIterator('20140901T000000,20141001T000000', new DateTimeImmutable('2014-08-01 00:00:00', $tz)); + + $expected = [ + new DateTimeImmutable('2014-08-01 00:00:00', $tz), + new DateTimeImmutable('2014-09-01 00:00:00', $tz), + new DateTimeImmutable('2014-10-01 00:00:00', $tz), + ]; + + $this->assertEquals( + $expected, + iterator_to_array($it) + ); + + + $this->assertFalse($it->isInfinite()); + + } + + + function testFastForward() { + + $utc = new DateTimeZone('UTC'); + $it = new RDateIterator('20140901T000000Z,20141001T000000Z', new DateTimeImmutable('2014-08-01 00:00:00', $utc)); + + $it->fastForward(new DateTimeImmutable('2014-08-15 00:00:00')); + + $result = []; + while ($it->valid()) { + $result[] = $it->current(); + $it->next(); + } + + $expected = [ + new DateTimeImmutable('2014-09-01 00:00:00', $utc), + new DateTimeImmutable('2014-10-01 00:00:00', $utc), + ]; + + $this->assertEquals( + $expected, + $result + ); + + $this->assertFalse($it->isInfinite()); + + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/RRuleIteratorTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/RRuleIteratorTest.php new file mode 100644 index 00000000000..84649f41f37 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Recur/RRuleIteratorTest.php @@ -0,0 +1,995 @@ +parse( + 'FREQ=HOURLY;INTERVAL=3;COUNT=12', + '2011-10-07 12:00:00', + [ + '2011-10-07 12:00:00', + '2011-10-07 15:00:00', + '2011-10-07 18:00:00', + '2011-10-07 21:00:00', + '2011-10-08 00:00:00', + '2011-10-08 03:00:00', + '2011-10-08 06:00:00', + '2011-10-08 09:00:00', + '2011-10-08 12:00:00', + '2011-10-08 15:00:00', + '2011-10-08 18:00:00', + '2011-10-08 21:00:00', + ] + ); + + } + + function testDaily() { + + $this->parse( + 'FREQ=DAILY;INTERVAL=3;UNTIL=20111025T000000Z', + '2011-10-07', + [ + '2011-10-07 00:00:00', + '2011-10-10 00:00:00', + '2011-10-13 00:00:00', + '2011-10-16 00:00:00', + '2011-10-19 00:00:00', + '2011-10-22 00:00:00', + '2011-10-25 00:00:00', + ] + ); + + } + + function testDailyByDayByHour() { + + $this->parse( + 'FREQ=DAILY;BYDAY=SA,SU;BYHOUR=6,7', + '2011-10-08 06:00:00', + [ + '2011-10-08 06:00:00', + '2011-10-08 07:00:00', + '2011-10-09 06:00:00', + '2011-10-09 07:00:00', + '2011-10-15 06:00:00', + '2011-10-15 07:00:00', + '2011-10-16 06:00:00', + '2011-10-16 07:00:00', + '2011-10-22 06:00:00', + '2011-10-22 07:00:00', + '2011-10-23 06:00:00', + '2011-10-23 07:00:00', + ] + ); + + } + + function testDailyByHour() { + + $this->parse( + 'FREQ=DAILY;INTERVAL=2;BYHOUR=10,11,12,13,14,15', + '2012-10-11 12:00:00', + [ + '2012-10-11 12:00:00', + '2012-10-11 13:00:00', + '2012-10-11 14:00:00', + '2012-10-11 15:00:00', + '2012-10-13 10:00:00', + '2012-10-13 11:00:00', + '2012-10-13 12:00:00', + '2012-10-13 13:00:00', + '2012-10-13 14:00:00', + '2012-10-13 15:00:00', + '2012-10-15 10:00:00', + '2012-10-15 11:00:00', + ] + ); + + } + + function testDailyByDay() { + + $this->parse( + 'FREQ=DAILY;INTERVAL=2;BYDAY=TU,WE,FR', + '2011-10-07 12:00:00', + [ + '2011-10-07 12:00:00', + '2011-10-11 12:00:00', + '2011-10-19 12:00:00', + '2011-10-21 12:00:00', + '2011-10-25 12:00:00', + '2011-11-02 12:00:00', + '2011-11-04 12:00:00', + '2011-11-08 12:00:00', + '2011-11-16 12:00:00', + '2011-11-18 12:00:00', + '2011-11-22 12:00:00', + '2011-11-30 12:00:00', + ] + ); + + } + + function testDailyCount() { + + $this->parse( + 'FREQ=DAILY;COUNT=5', + '2014-08-01 18:03:00', + [ + '2014-08-01 18:03:00', + '2014-08-02 18:03:00', + '2014-08-03 18:03:00', + '2014-08-04 18:03:00', + '2014-08-05 18:03:00', + ] + ); + + } + + function testDailyByMonth() { + + $this->parse( + 'FREQ=DAILY;BYMONTH=9,10;BYDAY=SU', + '2007-10-04 16:00:00', + [ + '2013-09-29 16:00:00', + '2013-10-06 16:00:00', + '2013-10-13 16:00:00', + '2013-10-20 16:00:00', + '2013-10-27 16:00:00', + '2014-09-07 16:00:00' + ], + '2013-09-28' + ); + + } + + function testWeekly() { + + $this->parse( + 'FREQ=WEEKLY;INTERVAL=2;COUNT=10', + '2011-10-07 00:00:00', + [ + '2011-10-07 00:00:00', + '2011-10-21 00:00:00', + '2011-11-04 00:00:00', + '2011-11-18 00:00:00', + '2011-12-02 00:00:00', + '2011-12-16 00:00:00', + '2011-12-30 00:00:00', + '2012-01-13 00:00:00', + '2012-01-27 00:00:00', + '2012-02-10 00:00:00', + ] + ); + + } + + function testWeeklyByDay() { + + $this->parse( + 'FREQ=WEEKLY;INTERVAL=1;COUNT=4;BYDAY=MO;WKST=SA', + '2014-08-01 00:00:00', + [ + '2014-08-01 00:00:00', + '2014-08-04 00:00:00', + '2014-08-11 00:00:00', + '2014-08-18 00:00:00', + ] + ); + + } + + function testWeeklyByDay2() { + + $this->parse( + 'FREQ=WEEKLY;INTERVAL=2;BYDAY=TU,WE,FR;WKST=SU', + '2011-10-07 00:00:00', + [ + '2011-10-07 00:00:00', + '2011-10-18 00:00:00', + '2011-10-19 00:00:00', + '2011-10-21 00:00:00', + '2011-11-01 00:00:00', + '2011-11-02 00:00:00', + '2011-11-04 00:00:00', + '2011-11-15 00:00:00', + '2011-11-16 00:00:00', + '2011-11-18 00:00:00', + '2011-11-29 00:00:00', + '2011-11-30 00:00:00', + ] + ); + + } + + function testWeeklyByDayByHour() { + + $this->parse( + 'FREQ=WEEKLY;INTERVAL=2;BYDAY=TU,WE,FR;WKST=MO;BYHOUR=8,9,10', + '2011-10-07 08:00:00', + [ + '2011-10-07 08:00:00', + '2011-10-07 09:00:00', + '2011-10-07 10:00:00', + '2011-10-18 08:00:00', + '2011-10-18 09:00:00', + '2011-10-18 10:00:00', + '2011-10-19 08:00:00', + '2011-10-19 09:00:00', + '2011-10-19 10:00:00', + '2011-10-21 08:00:00', + '2011-10-21 09:00:00', + '2011-10-21 10:00:00', + '2011-11-01 08:00:00', + '2011-11-01 09:00:00', + '2011-11-01 10:00:00', + ] + ); + + } + + function testWeeklyByDaySpecificHour() { + + $this->parse( + 'FREQ=WEEKLY;INTERVAL=2;BYDAY=TU,WE,FR;WKST=SU', + '2011-10-07 18:00:00', + [ + '2011-10-07 18:00:00', + '2011-10-18 18:00:00', + '2011-10-19 18:00:00', + '2011-10-21 18:00:00', + '2011-11-01 18:00:00', + '2011-11-02 18:00:00', + '2011-11-04 18:00:00', + '2011-11-15 18:00:00', + '2011-11-16 18:00:00', + '2011-11-18 18:00:00', + '2011-11-29 18:00:00', + '2011-11-30 18:00:00', + ] + ); + + } + + function testMonthly() { + + $this->parse( + 'FREQ=MONTHLY;INTERVAL=3;COUNT=5', + '2011-12-05 00:00:00', + [ + '2011-12-05 00:00:00', + '2012-03-05 00:00:00', + '2012-06-05 00:00:00', + '2012-09-05 00:00:00', + '2012-12-05 00:00:00', + ] + ); + + } + + function testMonlthyEndOfMonth() { + + $this->parse( + 'FREQ=MONTHLY;INTERVAL=2;COUNT=12', + '2011-12-31 00:00:00', + [ + '2011-12-31 00:00:00', + '2012-08-31 00:00:00', + '2012-10-31 00:00:00', + '2012-12-31 00:00:00', + '2013-08-31 00:00:00', + '2013-10-31 00:00:00', + '2013-12-31 00:00:00', + '2014-08-31 00:00:00', + '2014-10-31 00:00:00', + '2014-12-31 00:00:00', + '2015-08-31 00:00:00', + '2015-10-31 00:00:00', + ] + ); + + } + + function testMonthlyByMonthDay() { + + $this->parse( + 'FREQ=MONTHLY;INTERVAL=5;COUNT=9;BYMONTHDAY=1,31,-7', + '2011-01-01 00:00:00', + [ + '2011-01-01 00:00:00', + '2011-01-25 00:00:00', + '2011-01-31 00:00:00', + '2011-06-01 00:00:00', + '2011-06-24 00:00:00', + '2011-11-01 00:00:00', + '2011-11-24 00:00:00', + '2012-04-01 00:00:00', + '2012-04-24 00:00:00', + ] + ); + + } + + function testMonthlyByDay() { + + $this->parse( + 'FREQ=MONTHLY;INTERVAL=2;COUNT=16;BYDAY=MO,-2TU,+1WE,3TH', + '2011-01-03 00:00:00', + [ + '2011-01-03 00:00:00', + '2011-01-05 00:00:00', + '2011-01-10 00:00:00', + '2011-01-17 00:00:00', + '2011-01-18 00:00:00', + '2011-01-20 00:00:00', + '2011-01-24 00:00:00', + '2011-01-31 00:00:00', + '2011-03-02 00:00:00', + '2011-03-07 00:00:00', + '2011-03-14 00:00:00', + '2011-03-17 00:00:00', + '2011-03-21 00:00:00', + '2011-03-22 00:00:00', + '2011-03-28 00:00:00', + '2011-05-02 00:00:00', + ] + ); + + } + + function testMonthlyByDayByMonthDay() { + + $this->parse( + 'FREQ=MONTHLY;COUNT=10;BYDAY=MO;BYMONTHDAY=1', + '2011-08-01 00:00:00', + [ + '2011-08-01 00:00:00', + '2012-10-01 00:00:00', + '2013-04-01 00:00:00', + '2013-07-01 00:00:00', + '2014-09-01 00:00:00', + '2014-12-01 00:00:00', + '2015-06-01 00:00:00', + '2016-02-01 00:00:00', + '2016-08-01 00:00:00', + '2017-05-01 00:00:00', + ] + ); + + } + + function testMonthlyByDayBySetPos() { + + $this->parse( + 'FREQ=MONTHLY;COUNT=10;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=1,-1', + '2011-01-03 00:00:00', + [ + '2011-01-03 00:00:00', + '2011-01-31 00:00:00', + '2011-02-01 00:00:00', + '2011-02-28 00:00:00', + '2011-03-01 00:00:00', + '2011-03-31 00:00:00', + '2011-04-01 00:00:00', + '2011-04-29 00:00:00', + '2011-05-02 00:00:00', + '2011-05-31 00:00:00', + ] + ); + + } + + function testYearly() { + + $this->parse( + 'FREQ=YEARLY;COUNT=10;INTERVAL=3', + '2011-01-01 00:00:00', + [ + '2011-01-01 00:00:00', + '2014-01-01 00:00:00', + '2017-01-01 00:00:00', + '2020-01-01 00:00:00', + '2023-01-01 00:00:00', + '2026-01-01 00:00:00', + '2029-01-01 00:00:00', + '2032-01-01 00:00:00', + '2035-01-01 00:00:00', + '2038-01-01 00:00:00', + ] + ); + } + + function testYearlyLeapYear() { + + $this->parse( + 'FREQ=YEARLY;COUNT=3', + '2012-02-29 00:00:00', + [ + '2012-02-29 00:00:00', + '2016-02-29 00:00:00', + '2020-02-29 00:00:00', + ] + ); + } + + function testYearlyByMonth() { + + $this->parse( + 'FREQ=YEARLY;COUNT=8;INTERVAL=4;BYMONTH=4,10', + '2011-04-07 00:00:00', + [ + '2011-04-07 00:00:00', + '2011-10-07 00:00:00', + '2015-04-07 00:00:00', + '2015-10-07 00:00:00', + '2019-04-07 00:00:00', + '2019-10-07 00:00:00', + '2023-04-07 00:00:00', + '2023-10-07 00:00:00', + ] + ); + + } + + /** + * @expectedException \Sabre\VObject\InvalidDataException + */ + function testYearlyByMonthInvalidValue1() { + + $this->parse( + 'FREQ=YEARLY;COUNT=6;BYMONTHDAY=24;BYMONTH=0', + '2011-04-07 00:00:00', + [] + ); + + } + + /** + * @expectedException \Sabre\VObject\InvalidDataException + */ + function testYearlyByMonthInvalidValue2() { + + $this->parse( + 'FREQ=YEARLY;COUNT=6;BYMONTHDAY=24;BYMONTH=bla', + '2011-04-07 00:00:00', + [] + ); + + } + + /** + * @expectedException \Sabre\VObject\InvalidDataException + */ + function testYearlyByMonthManyInvalidValues() { + + $this->parse( + 'FREQ=YEARLY;COUNT=6;BYMONTHDAY=24;BYMONTH=0,bla', + '2011-04-07 00:00:00', + [] + ); + + } + + /** + * @expectedException \Sabre\VObject\InvalidDataException + */ + function testYearlyByMonthEmptyValue() { + + $this->parse( + 'FREQ=YEARLY;COUNT=6;BYMONTHDAY=24;BYMONTH=', + '2011-04-07 00:00:00', + [] + ); + + } + + function testYearlyByMonthByDay() { + + $this->parse( + 'FREQ=YEARLY;COUNT=8;INTERVAL=5;BYMONTH=4,10;BYDAY=1MO,-1SU', + '2011-04-04 00:00:00', + [ + '2011-04-04 00:00:00', + '2011-04-24 00:00:00', + '2011-10-03 00:00:00', + '2011-10-30 00:00:00', + '2016-04-04 00:00:00', + '2016-04-24 00:00:00', + '2016-10-03 00:00:00', + '2016-10-30 00:00:00', + ] + ); + + } + + function testYearlyByYearDay() { + + $this->parse( + 'FREQ=YEARLY;COUNT=7;INTERVAL=2;BYYEARDAY=190', + '2011-07-10 03:07:00', + [ + '2011-07-10 03:07:00', + '2013-07-10 03:07:00', + '2015-07-10 03:07:00', + '2017-07-10 03:07:00', + '2019-07-10 03:07:00', + '2021-07-10 03:07:00', + '2023-07-10 03:07:00', + ] + ); + + } + + function testYearlyByYearDayMultiple() { + + $this->parse( + 'FREQ=YEARLY;COUNT=8;INTERVAL=3;BYYEARDAY=190,301', + '2011-07-10 14:53:11', + [ + '2011-07-10 14:53:11', + '2011-10-29 14:53:11', + '2014-07-10 14:53:11', + '2014-10-29 14:53:11', + '2017-07-10 14:53:11', + '2017-10-29 14:53:11', + '2020-07-09 14:53:11', + '2020-10-28 14:53:11', + ] + ); + + } + + function testYearlyByYearDayByDay() { + + $this->parse( + 'FREQ=YEARLY;COUNT=6;BYYEARDAY=97;BYDAY=SA', + '2001-04-07 14:53:11', + [ + '2001-04-07 14:53:11', + '2006-04-08 14:53:11', + '2012-04-07 14:53:11', + '2017-04-08 14:53:11', + '2023-04-08 14:53:11', + '2034-04-08 14:53:11', + ] + ); + + } + + function testYearlyByYearDayNegative() { + + $this->parse( + 'FREQ=YEARLY;COUNT=8;BYYEARDAY=-97,-5', + '2001-09-26 14:53:11', + [ + '2001-09-26 14:53:11', + '2001-12-27 14:53:11', + '2002-09-26 14:53:11', + '2002-12-27 14:53:11', + '2003-09-26 14:53:11', + '2003-12-27 14:53:11', + '2004-09-26 14:53:11', + '2004-12-27 14:53:11', + ] + ); + + } + + /** + * @expectedException \Sabre\VObject\InvalidDataException + */ + function testYearlyByYearDayInvalid390() { + + $this->parse( + 'FREQ=YEARLY;COUNT=8;INTERVAL=4;BYYEARDAY=390', + '2011-04-07 00:00:00', + [ + ] + ); + + } + + /** + * @expectedException \Sabre\VObject\InvalidDataException + */ + function testYearlyByYearDayInvalid0() { + + $this->parse( + 'FREQ=YEARLY;COUNT=8;INTERVAL=4;BYYEARDAY=0', + '2011-04-07 00:00:00', + [ + ] + ); + + } + + function testFastForward() { + + // The idea is that we're fast-forwarding too far in the future, so + // there will be no results left. + $this->parse( + 'FREQ=YEARLY;COUNT=8;INTERVAL=5;BYMONTH=4,10;BYDAY=1MO,-1SU', + '2011-04-04 00:00:00', + [], + '2020-05-05 00:00:00' + ); + + } + + /** + * The bug that was in the + * system before would fail on the 5th tuesday of the month, if the 5th + * tuesday did not exist. + * + * A pretty slow test. Had to be marked as 'medium' for phpunit to not die + * after 1 second. Would be good to optimize later. + * + * @medium + */ + function testFifthTuesdayProblem() { + + $this->parse( + 'FREQ=MONTHLY;INTERVAL=1;UNTIL=20071030T035959Z;BYDAY=5TU', + '2007-10-04 14:46:42', + [ + '2007-10-04 14:46:42', + ] + ); + + } + + /** + * This bug came from a Fruux customer. This would result in a never-ending + * request. + */ + function testFastFowardTooFar() { + + $this->parse( + 'FREQ=WEEKLY;BYDAY=MO;UNTIL=20090704T205959Z;INTERVAL=1', + '2009-04-20 18:00:00', + [ + '2009-04-20 18:00:00', + '2009-04-27 18:00:00', + '2009-05-04 18:00:00', + '2009-05-11 18:00:00', + '2009-05-18 18:00:00', + '2009-05-25 18:00:00', + '2009-06-01 18:00:00', + '2009-06-08 18:00:00', + '2009-06-15 18:00:00', + '2009-06-22 18:00:00', + '2009-06-29 18:00:00', + ] + ); + + } + + function testValidByWeekNo() { + + $this->parse( + 'FREQ=YEARLY;BYWEEKNO=20;BYDAY=TU', + '2011-02-07 00:00:00', + [ + '2011-02-07 00:00:00', + '2011-05-17 00:00:00', + '2012-05-15 00:00:00', + '2013-05-14 00:00:00', + '2014-05-13 00:00:00', + '2015-05-12 00:00:00', + '2016-05-17 00:00:00', + '2017-05-16 00:00:00', + '2018-05-15 00:00:00', + '2019-05-14 00:00:00', + '2020-05-12 00:00:00', + '2021-05-18 00:00:00', + ] + ); + + } + + function testNegativeValidByWeekNo() { + + $this->parse( + 'FREQ=YEARLY;BYWEEKNO=-20;BYDAY=TU,FR', + '2011-09-02 00:00:00', + [ + '2011-09-02 00:00:00', + '2012-08-07 00:00:00', + '2012-08-10 00:00:00', + '2013-08-06 00:00:00', + '2013-08-09 00:00:00', + '2014-08-05 00:00:00', + '2014-08-08 00:00:00', + '2015-08-11 00:00:00', + '2015-08-14 00:00:00', + '2016-08-09 00:00:00', + '2016-08-12 00:00:00', + '2017-08-08 00:00:00', + ] + ); + + } + + function testTwoValidByWeekNo() { + + $this->parse( + 'FREQ=YEARLY;BYWEEKNO=20;BYDAY=TU,FR', + '2011-09-07 09:00:00', + [ + '2011-09-07 09:00:00', + '2012-05-15 09:00:00', + '2012-05-18 09:00:00', + '2013-05-14 09:00:00', + '2013-05-17 09:00:00', + '2014-05-13 09:00:00', + '2014-05-16 09:00:00', + '2015-05-12 09:00:00', + '2015-05-15 09:00:00', + '2016-05-17 09:00:00', + '2016-05-20 09:00:00', + '2017-05-16 09:00:00', + ] + ); + + } + + function testValidByWeekNoByDayDefault() { + + $this->parse( + 'FREQ=YEARLY;BYWEEKNO=20', + '2011-05-16 00:00:00', + [ + '2011-05-16 00:00:00', + '2012-05-14 00:00:00', + '2013-05-13 00:00:00', + '2014-05-12 00:00:00', + '2015-05-11 00:00:00', + '2016-05-16 00:00:00', + '2017-05-15 00:00:00', + '2018-05-14 00:00:00', + '2019-05-13 00:00:00', + '2020-05-11 00:00:00', + '2021-05-17 00:00:00', + '2022-05-16 00:00:00', + ] + ); + + } + + function testMultipleValidByWeekNo() { + + $this->parse( + 'FREQ=YEARLY;BYWEEKNO=20,50;BYDAY=TU,FR', + '2011-01-16 00:00:00', + [ + '2011-01-16 00:00:00', + '2011-05-17 00:00:00', + '2011-05-20 00:00:00', + '2011-12-13 00:00:00', + '2011-12-16 00:00:00', + '2012-05-15 00:00:00', + '2012-05-18 00:00:00', + '2012-12-11 00:00:00', + '2012-12-14 00:00:00', + '2013-05-14 00:00:00', + '2013-05-17 00:00:00', + '2013-12-10 00:00:00', + ] + ); + + } + + /** + * @expectedException \Sabre\VObject\InvalidDataException + */ + function testInvalidByWeekNo() { + + $this->parse( + 'FREQ=YEARLY;BYWEEKNO=54', + '2011-05-16 00:00:00', + [ + ] + ); + + } + + /** + * This also at one point caused an infinite loop. We're keeping the test. + */ + function testYearlyByMonthLoop() { + + $this->parse( + 'FREQ=YEARLY;INTERVAL=1;UNTIL=20120203T225959Z;BYMONTH=2;BYSETPOS=1;BYDAY=SU,MO,TU,WE,TH,FR,SA', + '2012-01-01 15:45:00', + [ + '2012-02-01 15:45:00', + ], + '2012-01-29 23:00:00' + ); + + + } + + /** + * Something, somewhere produced an ics with an interval set to 0. Because + * this means we increase the current day (or week, month) by 0, this also + * results in an infinite loop. + * + * @expectedException \Sabre\VObject\InvalidDataException + */ + function testZeroInterval() { + + $this->parse( + 'FREQ=YEARLY;INTERVAL=0', + '2012-08-24 14:57:00', + [], + '2013-01-01 23:00:00' + ); + + } + + /** + * @expectedException \Sabre\VObject\InvalidDataException + */ + function testInvalidFreq() { + + $this->parse( + 'FREQ=SMONTHLY;INTERVAL=3;UNTIL=20111025T000000Z', + '2011-10-07', + [] + ); + + } + + /** + * @expectedException \Sabre\VObject\InvalidDataException + */ + function testByDayBadOffset() { + + $this->parse( + 'FREQ=WEEKLY;INTERVAL=1;COUNT=4;BYDAY=0MO;WKST=SA', + '2014-08-01 00:00:00', + [] + ); + + } + + function testUntilBeginHasTimezone() { + + $this->parse( + 'FREQ=WEEKLY;UNTIL=20131118T183000', + '2013-09-23 18:30:00', + [ + '2013-09-23 18:30:00', + '2013-09-30 18:30:00', + '2013-10-07 18:30:00', + '2013-10-14 18:30:00', + '2013-10-21 18:30:00', + '2013-10-28 18:30:00', + '2013-11-04 18:30:00', + '2013-11-11 18:30:00', + '2013-11-18 18:30:00', + ], + null, + 'America/New_York' + ); + + } + + function testUntilBeforeDtStart() { + + $this->parse( + 'FREQ=DAILY;UNTIL=20140101T000000Z', + '2014-08-02 00:15:00', + [ + '2014-08-02 00:15:00', + ] + ); + + } + + function testIgnoredStuff() { + + $this->parse( + 'FREQ=DAILY;BYSECOND=1;BYMINUTE=1;BYYEARDAY=1;BYWEEKNO=1;COUNT=2', + '2014-08-02 00:15:00', + [ + '2014-08-02 00:15:00', + '2014-08-03 00:15:00', + ] + ); + + } + + function testMinusFifthThursday() { + + $this->parse( + 'FREQ=MONTHLY;BYDAY=-4TH,-5TH;COUNT=4', + '2015-01-01 00:15:00', + [ + '2015-01-01 00:15:00', + '2015-01-08 00:15:00', + '2015-02-05 00:15:00', + '2015-03-05 00:15:00' + ] + ); + + } + + /** + * @expectedException \Sabre\VObject\InvalidDataException + */ + function testUnsupportedPart() { + + $this->parse( + 'FREQ=DAILY;BYWODAN=1', + '2014-08-02 00:15:00', + [] + ); + + } + + function testIteratorFunctions() { + + $parser = new RRuleIterator('FREQ=DAILY', new DateTime('2014-08-02 00:00:13')); + $parser->next(); + $this->assertEquals( + new DateTime('2014-08-03 00:00:13'), + $parser->current() + ); + $this->assertEquals( + 1, + $parser->key() + ); + + $parser->rewind(); + + $this->assertEquals( + new DateTime('2014-08-02 00:00:13'), + $parser->current() + ); + $this->assertEquals( + 0, + $parser->key() + ); + + } + + function parse($rule, $start, $expected, $fastForward = null, $tz = 'UTC') { + + $dt = new DateTime($start, new DateTimeZone($tz)); + $parser = new RRuleIterator($rule, $dt); + + if ($fastForward) { + $parser->fastForward(new DateTime($fastForward)); + } + + $result = []; + while ($parser->valid()) { + + $item = $parser->current(); + $result[] = $item->format('Y-m-d H:i:s'); + + if ($parser->isInfinite() && count($result) >= count($expected)) { + break; + } + $parser->next(); + + } + + $this->assertEquals( + $expected, + $result + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/RecurrenceIterator/UntilRespectsTimezoneTest.ics b/htdocs/includes/sabre/sabre/vobject/tests/VObject/RecurrenceIterator/UntilRespectsTimezoneTest.ics new file mode 100644 index 00000000000..1663c783d84 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/RecurrenceIterator/UntilRespectsTimezoneTest.ics @@ -0,0 +1,39 @@ +BEGIN:VCALENDAR +VERSION:2.0 +X-WR-TIMEZONE:America/New_York +PRODID:-//www.churchcommunitybuilder.com//Church Community Builder//EN +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:Test Event +BEGIN:VTIMEZONE +TZID:America/New_York +X-LIC-LOCATION:America/New_York +BEGIN:DAYLIGHT +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +TZNAME:EDT +DTSTART:19700308T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +TZNAME:EST +DTSTART:19701101T020000 +RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +UID:10621-1440@ccbchurch.com +DTSTART;TZID=America/New_York:20130923T183000 +DTEND;TZID=America/New_York:20130923T203000 +DTSTAMP:20131216T170211 +RRULE:FREQ=WEEKLY;UNTIL=20131118T183000 +CREATED:20130423T161111 +DESCRIPTION:Test Event ending November 11, 2013 +LAST-MODIFIED:20131126T163428 +SEQUENCE:1387231331 +SUMMARY:Test +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/SlashRTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/SlashRTest.php new file mode 100644 index 00000000000..8e9389de162 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/SlashRTest.php @@ -0,0 +1,20 @@ +add('test', "abc\r\ndef"); + $this->assertEquals("TEST:abc\\ndef\r\n", $prop->serialize()); + + } + + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Splitter/ICalendarTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Splitter/ICalendarTest.php new file mode 100644 index 00000000000..ccbd5c88191 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Splitter/ICalendarTest.php @@ -0,0 +1,325 @@ +version = VObject\Version::VERSION; + } + + function createStream($data) { + + $stream = fopen('php://memory', 'r+'); + fwrite($stream, $data); + rewind($stream); + return $stream; + + } + + function testICalendarImportValidEvent() { + + $data = <<createStream($data); + + $objects = new ICalendar($tempFile); + + $return = ""; + while ($object = $objects->getNext()) { + $return .= $object->serialize(); + } + $this->assertEquals([], VObject\Reader::read($return)->validate()); + } + + /** + * @expectedException Sabre\VObject\ParseException + */ + function testICalendarImportWrongType() { + + $data = <<createStream($data); + + $objects = new ICalendar($tempFile); + + } + + function testICalendarImportEndOfData() { + $data = <<createStream($data); + + $objects = new ICalendar($tempFile); + + $return = ""; + while ($object = $objects->getNext()) { + $return .= $object->serialize(); + } + $this->assertNull($object = $objects->getNext()); + } + + /** + * @expectedException Sabre\VObject\ParseException + */ + function testICalendarImportInvalidEvent() { + $data = <<createStream($data); + $objects = new ICalendar($tempFile); + + } + + function testICalendarImportMultipleValidEvents() { + + $event[] = <<createStream($data); + + $objects = new ICalendar($tempFile); + + $return = ""; + $i = 0; + while ($object = $objects->getNext()) { + + $expected = <<version//EN +CALSCALE:GREGORIAN +$event[$i] +END:VCALENDAR + +EOT; + + $return .= $object->serialize(); + $expected = str_replace("\n", "\r\n", $expected); + $this->assertEquals($expected, $object->serialize()); + $i++; + } + $this->assertEquals([], VObject\Reader::read($return)->validate()); + } + + function testICalendarImportEventWithoutUID() { + + $data = <<version//EN +CALSCALE:GREGORIAN +BEGIN:VEVENT +DTSTART:20140101T040000Z +DTSTAMP:20140122T233226Z +END:VEVENT +END:VCALENDAR + +EOT; + $tempFile = $this->createStream($data); + + $objects = new ICalendar($tempFile); + + $return = ""; + while ($object = $objects->getNext()) { + $return .= $object->serialize(); + } + + $messages = VObject\Reader::read($return)->validate(); + + if ($messages) { + $messages = array_map( + function($item) { return $item['message']; }, + $messages + ); + $this->fail('Validation errors: ' . implode("\n", $messages)); + } else { + $this->assertEquals([], $messages); + } + } + + function testICalendarImportMultipleVTIMEZONESAndMultipleValidEvents() { + + $timezones = <<createStream($data); + + $objects = new ICalendar($tempFile); + + $return = ""; + $i = 0; + while ($object = $objects->getNext()) { + + $expected = <<version//EN +CALSCALE:GREGORIAN +$timezones +$event[$i] +END:VCALENDAR + +EOT; + $expected = str_replace("\n", "\r\n", $expected); + + $this->assertEquals($expected, $object->serialize()); + $return .= $object->serialize(); + $i++; + + } + + $this->assertEquals([], VObject\Reader::read($return)->validate()); + } + + function testICalendarImportWithOutVTIMEZONES() { + + $data = <<createStream($data); + + $objects = new ICalendar($tempFile); + + $return = ""; + while ($object = $objects->getNext()) { + $return .= $object->serialize(); + } + + $messages = VObject\Reader::read($return)->validate(); + $this->assertEquals([], $messages); + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/Splitter/VCardTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Splitter/VCardTest.php new file mode 100644 index 00000000000..e19e2d8205e --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/Splitter/VCardTest.php @@ -0,0 +1,193 @@ +createStream($data); + + $objects = new VCard($tempFile); + + $count = 0; + while ($objects->getNext()) { + $count++; + } + $this->assertEquals(1, $count); + + } + + /** + * @expectedException Sabre\VObject\ParseException + */ + function testVCardImportWrongType() { + $event[] = <<createStream($data); + + $splitter = new VCard($tempFile); + + while ($object = $splitter->getNext()) { + } + + } + + function testVCardImportValidVCardsWithCategories() { + $data = <<createStream($data); + + $splitter = new VCard($tempFile); + + $count = 0; + while ($object = $splitter->getNext()) { + $count++; + } + $this->assertEquals(4, $count); + + } + + function testVCardImportEndOfData() { + $data = <<createStream($data); + + $objects = new VCard($tempFile); + $object = $objects->getNext(); + + $this->assertNull($objects->getNext()); + + + } + + /** + * @expectedException \Sabre\VObject\ParseException + */ + function testVCardImportCheckInvalidArgumentException() { + $data = <<createStream($data); + + $objects = new VCard($tempFile); + while ($objects->getNext()) { } + + } + + function testVCardImportMultipleValidVCards() { + $data = <<createStream($data); + + $objects = new VCard($tempFile); + + $count = 0; + while ($objects->getNext()) { + $count++; + } + $this->assertEquals(2, $count); + + } + + function testImportMultipleSeparatedWithNewLines() { + $data = <<createStream($data); + $objects = new VCard($tempFile); + + $count = 0; + while ($objects->getNext()) { + $count++; + } + $this->assertEquals(2, $count); + } + + function testVCardImportVCardWithoutUID() { + $data = <<createStream($data); + + $objects = new VCard($tempFile); + + $count = 0; + while ($objects->getNext()) { + $count++; + } + + $this->assertEquals(1, $count); + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/StringUtilTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/StringUtilTest.php new file mode 100644 index 00000000000..8e0bc483d15 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/StringUtilTest.php @@ -0,0 +1,55 @@ +assertEquals(false, $string); + + } + + function testIsUTF8() { + + $string = StringUtil::isUTF8('I 💚 SabreDAV'); + + $this->assertEquals(true, $string); + + } + + function testUTF8ControlChar() { + + $string = StringUtil::isUTF8(chr(0x00)); + + $this->assertEquals(false, $string); + + } + + function testConvertToUTF8nonUTF8() { + + $string = StringUtil::convertToUTF8(chr(0xbf)); + + $this->assertEquals(utf8_encode(chr(0xbf)), $string); + + } + + function testConvertToUTF8IsUTF8() { + + $string = StringUtil::convertToUTF8('I 💚 SabreDAV'); + + $this->assertEquals('I 💚 SabreDAV', $string); + + } + + function testConvertToUTF8ControlChar() { + + $string = StringUtil::convertToUTF8(chr(0x00)); + + $this->assertEquals('', $string); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/TimeZoneUtilTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/TimeZoneUtilTest.php new file mode 100644 index 00000000000..8d8357dc7c2 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/TimeZoneUtilTest.php @@ -0,0 +1,377 @@ +assertInstanceOf('DateTimeZone', $tz); + } catch (\Exception $e) { + if (strpos($e->getMessage(), "Unknown or bad timezone") !== false) { + $this->markTestSkipped($timezoneName . ' is not (yet) supported in this PHP version. Update pecl/timezonedb'); + } else { + throw $e; + } + + } + + } + + function getMapping() { + + TimeZoneUtil::loadTzMaps(); + + // PHPUNit requires an array of arrays + return array_map( + function($value) { + return [$value]; + }, + TimeZoneUtil::$map + ); + + } + + function testExchangeMap() { + + $vobj = <<assertEquals($ex->getName(), $tz->getName()); + + } + + function testWetherMicrosoftIsStillInsane() { + + $vobj = <<assertEquals($ex->getName(), $tz->getName()); + + } + + function testUnknownExchangeId() { + + $vobj = <<assertEquals($ex->getName(), $tz->getName()); + + } + + function testWindowsTimeZone() { + + $tz = TimeZoneUtil::getTimeZone('Eastern Standard Time'); + $ex = new \DateTimeZone('America/New_York'); + $this->assertEquals($ex->getName(), $tz->getName()); + + } + + /** + * @dataProvider getPHPTimeZoneIdentifiers + */ + function testTimeZoneIdentifiers($tzid) { + + $tz = TimeZoneUtil::getTimeZone($tzid); + $ex = new \DateTimeZone($tzid); + + $this->assertEquals($ex->getName(), $tz->getName()); + + } + + /** + * @dataProvider getPHPTimeZoneBCIdentifiers + */ + function testTimeZoneBCIdentifiers($tzid) { + + $tz = TimeZoneUtil::getTimeZone($tzid); + $ex = new \DateTimeZone($tzid); + + $this->assertEquals($ex->getName(), $tz->getName()); + + } + + function getPHPTimeZoneIdentifiers() { + + // PHPUNit requires an array of arrays + return array_map( + function($value) { + return [$value]; + }, + \DateTimeZone::listIdentifiers() + ); + + } + + function getPHPTimeZoneBCIdentifiers() { + + // PHPUNit requires an array of arrays + return array_map( + function($value) { + return [$value]; + }, + TimeZoneUtil::getIdentifiersBC() + ); + + } + + function testTimezoneOffset() { + + $tz = TimeZoneUtil::getTimeZone('GMT-0400', null, true); + + if (version_compare(PHP_VERSION, '5.5.10', '>=') && !defined('HHVM_VERSION')) { + $ex = new \DateTimeZone('-04:00'); + } else { + $ex = new \DateTimeZone('Etc/GMT-4'); + } + $this->assertEquals($ex->getName(), $tz->getName()); + + } + + /** + * @expectedException InvalidArgumentException + */ + function testTimezoneFail() { + + $tz = TimeZoneUtil::getTimeZone('FooBar', null, true); + + } + + function testFallBack() { + + $vobj = <<assertEquals($ex->getName(), $tz->getName()); + + } + + function testLjubljanaBug() { + + $vobj = <<assertEquals($ex->getName(), $tz->getName()); + + } + + function testWeirdSystemVLICs() { + +$vobj = <<assertEquals($ex->getName(), $tz->getName()); + + } + + + function testPrefixedOffsetExchangeIdentifier() + { + $tz = TimeZoneUtil::getTimeZone('(UTC-05:00) Eastern Time (US & Canada)'); + $ex = new \DateTimeZone('America/New_York'); + $this->assertEquals($ex->getName(), $tz->getName()); + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/UUIDUtilTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/UUIDUtilTest.php new file mode 100644 index 00000000000..d33a8794607 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/UUIDUtilTest.php @@ -0,0 +1,37 @@ +assertTrue( + UUIDUtil::validateUUID('11111111-2222-3333-4444-555555555555') + ); + $this->assertFalse( + UUIDUtil::validateUUID(' 11111111-2222-3333-4444-555555555555') + ); + $this->assertTrue( + UUIDUtil::validateUUID('ffffffff-2222-3333-4444-555555555555') + ); + $this->assertFalse( + UUIDUtil::validateUUID('fffffffg-2222-3333-4444-555555555555') + ); + + } + + /** + * @depends testValidateUUID + */ + function testGetUUID() { + + $this->assertTrue( + UUIDUtil::validateUUID( + UUIDUtil::getUUID() + ) + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/VCard21Test.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/VCard21Test.php new file mode 100644 index 00000000000..cede1eac59f --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/VCard21Test.php @@ -0,0 +1,52 @@ +serialize(); + + $this->assertEquals($input, $output); + + } + + function testPropertyPadValueCount() { + + $input = <<serialize(); + + $expected = <<assertEquals($expected, $output); + + } +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/VCardConverterTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/VCardConverterTest.php new file mode 100644 index 00000000000..77fc37d700f --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/VCardConverterTest.php @@ -0,0 +1,533 @@ +convert(Document::VCARD40); + + $this->assertVObjectEqualsVObject( + $output, + $vcard + ); + + } + + function testConvert40to40() { + + $input = <<convert(Document::VCARD40); + + $this->assertVObjectEqualsVObject( + $output, + $vcard + ); + + } + + function testConvert21to40() { + + $input = <<convert(Document::VCARD40); + + $this->assertVObjectEqualsVObject( + $output, + $vcard + ); + + } + + function testConvert30to30() { + + $input = <<convert(Document::VCARD30); + + $this->assertVObjectEqualsVObject( + $output, + $vcard + ); + + } + + function testConvert40to30() { + + $input = <<convert(Document::VCARD30); + + $this->assertVObjectEqualsVObject( + $output, + $vcard + ); + + } + + function testConvertGroupCard() { + + $input = <<convert(Document::VCARD40); + + $this->assertVObjectEqualsVObject( + $output, + $vcard + ); + + $input = $output; + $output = <<convert(Document::VCARD30); + + $this->assertVObjectEqualsVObject( + $output, + $vcard + ); + + } + + function testBDAYConversion() { + + $input = <<convert(Document::VCARD40); + + $this->assertVObjectEqualsVObject( + $output, + $vcard + ); + + $input = $output; + $output = <<convert(Document::VCARD30); + + $this->assertVObjectEqualsVObject( + $output, + $vcard + ); + + } + + /** + * @expectedException InvalidArgumentException + */ + function testUnknownSourceVCardVersion() { + + $input = <<convert(Document::VCARD40); + + } + + /** + * @expectedException InvalidArgumentException + */ + function testUnknownTargetVCardVersion() { + + $input = <<convert(Document::VCARD21); + + } + + function testConvertIndividualCard() { + + $input = <<convert(Document::VCARD30); + + $this->assertVObjectEqualsVObject( + $output, + $vcard + ); + + $input = $output; + $output = <<convert(Document::VCARD40); + + $this->assertVObjectEqualsVObject( + $output, + $vcard + ); + + } + + function testAnniversary() { + + $input = <<!$_ +ITEM1.X-ANNIVERSARY;VALUE=DATE-AND-OR-TIME:20081210 +END:VCARD + +OUT; + + $vcard = Reader::read($input); + $vcard = $vcard->convert(Document::VCARD30); + + $this->assertVObjectEqualsVObject( + $output, + $vcard + ); + + // Swapping input and output + list( + $input, + $output + ) = [ + $output, + $input + ]; + + $vcard = Reader::read($input); + $vcard = $vcard->convert(Document::VCARD40); + + $this->assertVObjectEqualsVObject( + $output, + $vcard + ); + + } + + function testMultipleAnniversaries() { + + $input = <<!$_ +ITEM1.X-ANNIVERSARY;VALUE=DATE-AND-OR-TIME:20081210 +ITEM2.X-ABDATE;VALUE=DATE-AND-OR-TIME:20091210 +ITEM2.X-ABLABEL:_$!!$_ +ITEM2.X-ANNIVERSARY;VALUE=DATE-AND-OR-TIME:20091210 +ITEM3.X-ABDATE;VALUE=DATE-AND-OR-TIME:20101210 +ITEM3.X-ABLABEL:_$!!$_ +ITEM3.X-ANNIVERSARY;VALUE=DATE-AND-OR-TIME:20101210 +END:VCARD + +OUT; + + $vcard = Reader::read($input); + $vcard = $vcard->convert(Document::VCARD30); + + $this->assertVObjectEqualsVObject( + $output, + $vcard + ); + + // Swapping input and output + list( + $input, + $output + ) = [ + $output, + $input + ]; + + $vcard = Reader::read($input); + $vcard = $vcard->convert(Document::VCARD40); + + $this->assertVObjectEqualsVObject( + $output, + $vcard + ); + + } + + function testNoLabel() { + + $input = <<assertInstanceOf('Sabre\\VObject\\Component\\VCard', $vcard); + $vcard = $vcard->convert(Document::VCARD40); + $vcard = $vcard->serialize(); + + $converted = Reader::read($vcard); + $converted->validate(); + + $version = Version::VERSION; + + $expected = <<assertEquals($expected, str_replace("\r", "", $vcard)); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/VersionTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/VersionTest.php new file mode 100644 index 00000000000..956479bf2e6 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/VersionTest.php @@ -0,0 +1,14 @@ +assertEquals(-1, version_compare('2.0.0', $v)); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/WriterTest.php b/htdocs/includes/sabre/sabre/vobject/tests/VObject/WriterTest.php new file mode 100644 index 00000000000..800e13dd054 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/WriterTest.php @@ -0,0 +1,41 @@ +getComponent()); + $this->assertEquals("BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n", $result); + + } + + function testWriteToJson() { + + $result = Writer::writeJson($this->getComponent()); + $this->assertEquals('["vcalendar",[],[]]', $result); + + } + + function testWriteToXml() { + + $result = Writer::writeXml($this->getComponent()); + $this->assertEquals( + '' . "\n" . + '' . "\n" . + ' ' . "\n" . + '' . "\n", + $result + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/issue153.vcf b/htdocs/includes/sabre/sabre/vobject/tests/VObject/issue153.vcf new file mode 100644 index 00000000000..180949c5eb1 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/issue153.vcf @@ -0,0 +1,352 @@ +BEGIN:VCARD +VERSION:3.0 +N:Benutzer;Test;;; +FN:Test Benutzer +PHOTO;BASE64: + /9j/4AAQSkZJRgABAQAAAQABAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQA + AAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAABQKADAAQAAAABAAABQAAAAAD/2wBD + AAIBAQIBAQICAQICAgICAwUDAwMDAwYEBAMFBwYHBwcGBgYHCAsJBwgKCAYGCQ0JCgsLDAwMBwkN + Dg0MDgsMDAv/2wBDAQICAgMCAwUDAwULCAYICwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsL + CwsLCwsLCwsLCwsLCwsLCwsLCwv/wAARCAFAAUADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAA + AAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKB + kaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZn + aGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT + 1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcI + CQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAV + YnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6 + goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk + 5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD8J7JbO8tYo1tIFCDLOVG5qfdaVZRwmSOFWzyA + F4H1rLt5WViMhdp6HgmtKK8O3B+4Rhx6fSgBI9FtjaNN5aErwRjilSys7lFAt41xyTtqc2yJCVlY + 7eqgGqv2jyLcebjZnGPWncdzT0+w0u5eQXtrGiBcIyoPmNMXwpb/AGMTSRRbH6YAyPwqK21GKdfL + BAVfu+1SQX4jnjKFsp03dPypCKN9oEaKSkC7R0bGKpnSlSPdHErZOORXV3Ouy337sCLB6kpx+FY0 + t+VfyrgcbuCB1oAfoMemrcImq2sZX+I7ATXS618PdK1DRlvvDEaMq5LoV2nisx4LVrUfu5BOePau + m8EQS6PY3HmFXjljKhTzjOf1oA4mz8OxvMrLbW5RD8wbByKg1LRrRriRYY408w/KAMba1pRaWt/H + a6a7CVm2u7N8lUPEujzaRekzSK6tgqVNAGNBZJauY5Yon92GTRJp0ROY0Un0A4q3c2odkaYOMjii + KL7NIDGcj1NDAZBplmmWv1xnoFHStfS/DFpewqYoYm3DutZ8lv8AapdyOqk8EVteEbSe3KBSrDrQ + BT8S+HbawiiWGCAPjsuMnPesqHS4JSFlSMP7DitbXbvfrkkM2eGw3p+FMfTh5X+hr8w7t3oAhOhW + u8MkMZUY3fL0Heo9UsrN5FFrbxKmMBgoG41fWFra0Acjpzg9aoXjtgRoo29vagCoun27kbY059qn + bwykskYjRArdTT7GEl2UqMr2q/JtVU27iR15NADdK8DC/wBPle2iicxNg5ALH6Umm6FZ/a3ttQt4 + g2Cqnb0PbJ+tamn3j6ZCW0nILfeBORWVfO4dhLw7fMW7560AZuqeHf7MuTFcRpv6qVGVx70q2Eci + QwyW0SsPvOqjJrUtb6S9tHQKGeMZYuM8VUs7gRxbrncy9mWgB1x4QtTHvsQWkHJVhhax3tkhugHh + UkfeAXIFdPZ3v2uxkQ9G4jI6/j+tYun3r2Fy6yxeb2Py5IoAqXenJ5xaGNNvXH/1qcLSGeBdkSg9 + CcdaswC3be0pfexOMnpn2qaS1KQkQASKoydvLCgDNi09RKTNCuO2BxVjSobc6gqXMERQHkleDUsc + u9VADbG6qOWAp11bLbptkjlCkZRsde9AFi5sbO3kKfZYTnkHaOlVbuO2F5thtYcADjaKXUpHj8ku + Co2VDFL5wLeg696YFwQ2z7Qtlb8HJO0c1Zsr7T7a9kL6XazZ4CmMFRWfHdkEgjGRjPpU9raP5LSP + j5h2pAWdQ0+z1KdG+y21qvcRqBn8qXSvC+iTu63ssqyE/IAuR+NQwSrGm1g+c8E9qiSQW9wPNYYP + OR2oAW68GNa28k3lwGNHwvzDJGfSqM9nHBgm3j59QMVdmma4zIjsUBHy5OKp6o8s2BJjZjjAoAro + /nysbgYY9zWmLPCR+WQQwyaz4k2F/Pbft/GtKxvUeFN+B2x+NAEptsWpZSdo9etZe8su2X7pPFdU + LeOazKqVwevNYt7pw5EA5HIxQBQA8tAIeGz1NWIJvJlhW5OQBzjrUMR/eN9pwoXjB4qQ3ERJeYcy + 9P8AZoA0jf8AmybVxsHAFS6jp63ixmwjIwOfrWfaou12GcDpmt/w5qJhXc6hh2GM0AZkHiRpblVl + G0RjGMdxXQ+H/E0Rm+bjdw1crqEHm3EksY4Y9PTmq0cskc42qUOfpmgDovHOhLBOZ9O+aEnIUdRW + QZft1sgum/1Ywua3fDfiFDL5WoEPEwxzzirPizwTFPZC60kYUjcAp4NAHPSq91EoRS3061DHD9nb + 94Mkfw020v57GbcCRt4IIqzNcedIH2jc3JyOaAIYrRZmJxtNdB4fkGn2hluBgBR+NZ2n2X9ozAQD + 5qvaxGbKIRXkuFU4C96AMDxBKZdQkuEUkStuUegpNM1eWScAkqpHTHNPlwbjMzExZ4Pal1PS/s6+ + dY/6vuwPSgC9G8c0A+1xEknrnpUVxaeXNm2dVUfjVazvEZAEkMrccZzV1YYyBIhJP8SZ6fhQBSmV + 4JfMVT+96UJdSQdcMO4A6fjVmTUoJiqTOMJ/q+elRyQs0TtaxF0PVhzmgCzpd55r7YI2HHPTmrV0 + sDTF7gnJXGO4OKyNKgn80NbFhjoBzWjqdg6SISPmIBOaAKVnI1leyhsMJOD7CqOqRtZqotjiFulW + rhsSMshKH1ogsZbmF475TKifdf0oApabevHIAhCYOdxp0t59luS0I+995uxqpdRyWsrqmXGeCR/K + rVlZfaogqv8AvD/CaAIY42kV3K5zzn1p9jNLp6u/A80YPNWWsJNPAVpC4JAZT2HfFWJoVmVVjhVk + HTPrQBPoi2wsoo4APtBHL+tP1mS5uVEFxgJGNqH15plp5WmyBriMRsowM8UybXTNdbrpd6A/KKAD + xbJAGs44FIPlnd9c/wD16ynt/LiDW2SR2qa5vP7RnMs6BNuQMd6jhkAUb2K8+tADYp0fhj8w6itC + yQ3CFYeAOoqi8Uew+UMuf4u9T2NwIW+UgMetO4FmS6RJ1ik6HqxHAqC+gimUiA8DvjrU0kcE8ieY + itu+8c0+bShaWxksSZoM4b0SkBTgha0cq33Cuc1SvrrLFV6jpWqbuGe1HnnDdAKy7i3WSY7OT2NN + AMulWSV8ZDNzxV7SlbaFjClx69Kpww7W3ct7jpUtnNJHd5UjZnt1NIDdt7h7NQ7qGfpt7VR1XVEh + dhEpP94/4VpafexTy7ZlbBGDVHxFbQh1j04HaOTkdKAM5ZVlYso3E+tVp4w8gx0Bqd7QxNu+6D6V + DIoVySxAx2NAFyNmli2pjYBz61paW3lWrFS3BwP8/hWJbTBFJy2D6HgfWtiTWPsqxraBHyOeBg0A + RSoLSTdIepzz0606exTWyQGMXljORTNT1B7+ECZR5fHzDqapfbHjbFkTsIwSTQA43ptyyS44Paun + 8N64Z7Bre4YlZBtU5+7XLTQbjwN4Pb+IfWn2lw9uyrIw2Z5HpQBv3GirHc7LxWVZOVI71FNp7WDg + QYlIIGD6VvaPdi+tljb5yeAzcn8DT9YtbPSpVhDM87jJ3Htjnn6UAUIrJreD7Si7MDoKhv8AUxqt + pGt5GqIOr9zRfLM8ZFgZGtex2nGe4zWKN8rsDhYx2JpJ3Atx+HxcRSzWcpcL/CRwaj0zW1sQy3cS + nsFPSoYJpbIl7dm8tT8wzV7+0hqEO1Y4lQ9cqMn9KoCp9kW7kaaxU+Yx+5j5etWrb/RGxfr5bkdu + lW7KFILpfspDbVyc1fjNnrLtHqOYWP8AFjGfxpAc/e6Ql/GzW4AfqBWfpupS6Xer5vPlHmMjg10V + 5pp0u4JhYNGvAYHrUn2WLWrVo41AvSMRZAC/8CPr1oAvafdWOuNG+lqDekY+zg8MPXPX/wDXWZrF + tcWNw0erKElB4Rf4R6c1BpqyaBdbrnEcwyAc4x06H0rQS9a9jUTgOXPzMwycexoAw7u1jYb3zkU3 + Srtgdk54PFamv2C2pDQbWjcfKCeSa56aJld23YA6ZOKFqBrXGjjULuOKxKuZOTn+H/OKwr/ztOvs + uCrg7RgVLYapPbXAEW4EkHJNdBNBH4gtgyhFmXuw60AVpbT7VpiPJ94jLetQWsDRSIYz8mec1c0+ + 1nexdrw7GjJXk/epsFtDPG0bOdw+b5SaAKWsXA+14Y71FQi5S4RvlAC8A0y5hHmHarhvQ9BVGSQx + sUXPHX3oAmDCJ8rzgHg96gQ+ZGWbg9vahNRG7EnalkkF6hEXyD270MCWF3aEhdue1OsmNnMAih/r + VaBgAUY8561PaubdnMxJXseuKANhIY5Assp2v12itZtAgubEi2nb5xuKYHWubstQaO6SVzujTqpP + X8K2rXWLRF8xZJPMfjAzgUAcxcNiaRSpUocc96sW+yNgZCMVF4lvJdRvTOYkj52jbgZ98D6VWmlY + 2qCUnJOKaVwCzviibANwYc8Utkdl7tbKhjxmpUspvm8tgn16ipigSEG4G4pxu9TSA27GeFbRlGGm + P3cdhUN8GEP2hV3JjafrWfpU/wBmuAcZLA4/Sr1trkarJHcRmSEZO3uTQBmrcbZCLoDZ2x1qOHSi + yebJIAPQipp4kmbzI1EQJ6GtCxsoHP8Ap91GB2yDQBlSWO+M/ZsBHHzZ71XkfMIWNgGU9vSt3U9N + t9m21uonz0Iz/hVCfRkjg82FhtHDGgCuZ8EMjDZjBzSZ8pAwU7XbGT0pWtEjjAZgV4PFOml2QKqk + OoOcU1qBNYRSrdkrhw3BIrah8KwXoV/m3PyVzyDWNp999kccgZq/ea7PFAGgZlJ6EUgN23thpdi4 + V1Eucr7ev9K53V/ER1a/MkuWdBtG04zioLrXJ5wDK2XAxmqVqmZ2YPtHJ/GgDsvC3i0ppr2d2ish + yFAHIz706bRLNdOPnErKw4y3NcvZ3pjA8o4kB61o3OpSX9nbx3QIkU/MwoAj/sGaPzFjlWSJjk46 + ioYYwqssjIHHAHpWm4ESN9nYDIFZV+I7uVI1wrY5b1oAtafcvb3W4MM9Nx6U/VZpNRys54ToU4zW + KXaDKrJuC8cVdtpi1gzs43HNAD9N195bdYtRIUR4wD1NX2KuA9uThuSQelcsZwzq9xyzfezV/SdX + e3m8pXJhkPKkUAdYZk8RywjVVJES7U2cE/WtA+HDHohuY3Uxg7RF/GeaPBlxaawMW6rHKnAU9SOO + lX/FFv8A2bpzTQk+cpAAz93nrQBx+r4c5CODEOA3Y+wrKu5V1C1GFKznkk9K6Wzv49fs8Xf7y7DY + MhGNgrmtX0s2t66WknnKvUp0/WgCnbrJFdot0NwJxkDFdDYp86oMjjIArJivxbR7LuMyEjKitS21 + MW8auuW44H93/PFAG15aXdr5Uv7uULkA/wCFc+Yvstw0at8+eoq/p+rm6vRJMNwIx9KranYySXSy + WEZZHOCw7UARXFyj5STAk7ntWVf2gALLyfUVoataLbfLO2SO/Ws2c+VwhLK3QDpQBmz2xAyCG56d + 6uWPlnCkFcjoTzUBkMc/3cZpwn8oZkDFs8HsKALN1apDIHOeaiLkRkMOtSXE6yxAsRUcdxldswIJ + HANMCuJW8xQgOP51oacWPPGAeRUUOIZQzDhecd6mbIcbPusM0gLmq6bHPohlhDeZuH4c1zzF1+Rs + HByDXTae0s0IhjjZg3GPWqOs+HpLCTbNGyb+cHrQBZitjPEzW/LL97vinw2v2m2aORec9AKXQbsw + ygBBiX72TWxfaS8kiGFQAwz8vWkncDlbqNraT5cjb/n+lMGckx8kjOa1tU2TxkPkMpxyKyrhJ4Wa + KIDbTAkgvIp7URzgBwe/BpZYrd4vmZWNZ81x5cgBXDdzVlIvtUOGIBHpQA2aEROpR8DsB2q3bvG9 + iySzEsTkLnrVMqViCZzt7nrT7GBVuQRnODQA6Q+Sx80A4HApEJB3BAR9K19EmhkvCJ0ZsKe3tUc8 + Mc1yy7cpn6YoAzoUiclnYYY8AHpUl8zRxqpPy9qtC2tULgSMAvQ460lzIl9b7YiDt4GaAKMMQlJ5 + z9Kj8gIW5yKnS3Crlzhh6d6k0mbyZT565Q5z60ANtrRpPmhzWhbwy7DJcDhhwMdKlt7aK+gb+z33 + yKdxVuMCqaz5cqGYfWgB6yu8rBB8o6Gs/UpjGQXBGPTvVmSfyImyepqrqjbIw3WgCDz1ib9yOTg4 + NbVlNBJYvlVBHt1rBaPzQWU4IHSn2FwRJslJxQA6e3M0O4oAzdB6VXR2iKGQENGOK0ms1eAkFjF/ + BjrVGaAo371smgC7pety2kwl06Vo5AOWXmuwm+Itv4g8Ota30aWlySAJQfmkP/1zXIeG4Y5SVBB3 + evamXGly2tydwG0nKkHpQBZ86fRbpBLI252y4PGRWhO8Ml1IbJhHn+BTnNU9O1oRwvDqqhB2lHJP + 4U6awb+z4JdKbzdh5ZurDHtQBat5LaRHiaOP7QejEZKD/Oauy+FI7W3Bsroyhxkq3QH8q5a7ujM8 + nWOQnBqTR9burCT98xdR60AbbaHc6ZG3ymJsZC/3hVnw/fNIXt7hygHzZp2oeIBqCxzqfmCgEe3+ + RVdrmLVAEtf3bxfOW/ve36UAV7+7DXMu5Q4/Os2e3eRWkiAGOijtWrPodxfQmeNVAPOPWsppJIpi + JxsKcY9aAMwRyTSbpflx68VOYvOXb97OKtXAiZdzkqT0AGc037BIIRLHjsR60AVprZrZwGj4qTY0 + xyRj3PUVMJDduFfqvFRzxJCzrCzEr60ALEu+YI53c4qeGB7lGCnBU4FUopTBLvfk1at9R2sAMjNA + GtaXsnhy2FzPHvC46jgnNQ33imTXrkz3oVFAwo9Kfrtq03hAzEfJ5gyc81hWM5hhKrhgT0NPcByS + P5g2uVI98Vp6X4uuNGlyzCQIQR0bI7/1rNQxqW+05J7Y4qK5ZYUP2ZCW9TSA7SR9M8V30X9nMFZw + WfcNi5qPWPDtjo0pE7O03U/Mf055rmtFmN9E0DEox+atPWbiW7lSO8Ja4jQbcDC4A9PXFADYtM0+ + 6nc3u7aOm3IP6Vnak9tYt/xL/M445zTIbieOdmWNsE46cip42EkyC4hYx469KAFsrT7XEJgFPOT6 + 1s+H9PD3XlzxnL/MDtqn9pghgb7GjL/eJORWqfEnmrA9oFRoxjJ5BoAp6NqDW2pzRXtuyIAw3FMf + rVS4iF08pydmeCDxWvqeuC+Ro9qglcMw71mwReXD5aAlFJPPU0AZ0cEsbkSZKH15FD2xJJiJVj6c + VfnzLGEXAA71PFpDPaebE6/KOh60AYVws8TBgrFe57CmHUG25RVJA7AVozzSLbNvX5T1AHNY/m/Z + nPlqwDetAEtvqzJNu3FZBwQBjI96vPqkd3mRtokH31UYx+VZqWruxaFl+frkZxT1tvs1ujJgEH5m + PR/pQAXl2S371XAHI+Wkaf7VD8hGR2arKySylRccQ98DmiS0jifdsdgeODQBQd9x3IBx1xTYlBm3 + En86sXUAwPswKg9QeaBErIEj6nrQC0NHRtUjt0K3AHzDABGcVW1fTzJL51jyOpz0NVooispebBI4 + wK2YFEthk8qR07igDAgJil+TKtnnHFaP2h5yI3ZsgdSfaqd2P3im3BGM9aktsjmRgCOaAJZrMwR7 + 3A5PT0pdMvZtOning+byzuVDyh/A8VHczSzDPy7RwOKgiuHEewjKeoFAzp7TUNM8XXEw8RhYNQmP + 7ny18uNeOM7cCtMfDiS8uY0tDEYghyynjPbn864htP8ANhLIehzWzovxDvtFsDB9+PI4I/rQI0r3 + wNc6DO0N2VaQqW2q24YxmqFhYRgE/vkkDfMGBBP4GrSeJ7tZd6SxvIfmK4yQP84p0XiyC71gS65G + 00zAKGX5Qv4UAbFpd28WnIsBLsDzmub1+AXt1LJEoQqfu4xu+lbWsWgs4/NsCXjPIbqK5+5kklmE + rDD54BFAGb5cjybCrAnnB6ipEvXil2sM4GMVpFY7m4UNmNyOWJ4qteaM0BISVZe+RQBFHC2/zISg + B69KlIVhIHA3HuR70lqotlBulY5P4Vcls44k3u6N5oyoHb60wM6O1SRir5LemOKv2vhuW4iLg7VA + 6k4FTR2ax4aaVIwR3HWqGua5PcQm1WRBH6jqaQFzWbE2nhzynuIi+8HaHyKweJSEQEN6jpVcKyOw + cMVznOeKmtZvOPDKuOKAJbi0JYFf4eue9IW8sncfvdqnlvVFyFyu09abI0bysMZx0oArC4eCTcgb + juK2dNvE1N1M0ohljGQzc5A7cfSs6aweWAk7kTuapQysIT9mOSvG49aAOkvzLMxk06QNuG1l7j3r + PlnnJAuGJij+nNQ6XqT7wEYqyn5v9utLULaW7j321uiEjLqMkKKAIotbghb/AI8hKGPIBHNXLG6t + 7uzk3RLbKG/iP+Fc+8f2d1eFztzyD2q5p2oCFWRoxOX52nPFAGgLyC2lyZFKdB70r69buxRJBHjr + nvWVdeXLE7xE8fwnoPpVKZUnQPkBhwRmgDq7a9tLyARWiiWYngL1qG4gurJ28+NowO2a5a3v3smD + aa5WUd1HNbC6zI0KSX13JO7D5lbHFAE4V7pi0b5x1GazdUtXSM7v4iPw5rQ0/XrcXX75FgUdxzuq + /qFrp+sWRe3uDkc4BFAHLRDY42ycd6uPOXiiV+RGPlWnXOg3IQvEmIB/Ft6/jUUEZmMcgydvzECg + C1G2+Ly3YAvyM9qY88kaFcmmp807uwPJ4FS3do+Fzn5ulAFVrjbgS8Z4yah2C03SMffNWZdPknVA + iluQOnHWmX9pILvyY13HHK46UAVre7LSyOCTmtjSiy7VijLeZ0IqO08OzPIUiTI74Ga6bRP7O01F + h1KYJOv3V4BoA4zU1lExMrkbOAvpVcSifhjgrzmtjxPp7pO7SggOcqfUViy25hG5fSgC8rrLAojb + d7d6SexlEgwpRfTNV7e5LFBbKAwPNWHeX7TguxI7GmBPBExhaNVIJ6egqOVknO1fkx1J61aj1gLC + UEKlk4LVWvozC67kCFxkD1pAQ24e3uDLC3z9CR3H/wCqrczJdOGiOxvYc5/CocMYhtUBj3xU8Qjk + XbKPIZOjqclvzoAu2HiO60xPKvd7wY/1fGBWnJo8WuW6y6XIPMYZEAzuH9KxISonAuzuRzgk9qtR + 79KmMuhTt5cRyxznFADLzS2tMw6pAY5OoDEZ/Sm20TQQ74YwVQckGtMatB4kUpqreVIRw5+8aqXF + jc6bAsbD9yThWz94UAOmmjvrRCMJjOQRVS0sD9pLyABM5Of6Vdtrdn+RUGcZqO6uRBG0MuFI79KA + MfV7r7ZqDI7kohAVT6U2eJNimJQOuTnpSXFussrMvBz1pJov3YUsR9O9ABblRncQ3bAqY2EUwIiA + Vqr20ojfYqZx3q9bSKAGcYJPIoAoq7OCEQBffrRDGEcleM8nNPjuGkhHmbB74ApvmxltsuTnuDQA + +SFEjDwu5buD0qpLL5vMg2kEdOlXECMAyZGOMMePyprQRI5N0rt3BXO326UAV4b0Wt0pC5HrXS2W + qq9zE7jcO+OhFc81kbg7iMqeAFHSpLa8eymaNOUIwD6UAavjPQYYybq1bBmXcF9O39Kw4iXdDKcE + DAxW3q7NdWELISdiYIz71kz6ZNZNHI0cjqQfujIFAEtzAtu/7vODzmqlyzNyAo9vWp7uWSWJd+AM + jjGGqOWCSWRVVW2+uKAKskpWU5TP0p8c+ExsPPNTmCVD+5U/QrzRJHJGymeOQc45HFAFczh497KR + jirWlEsAudvII9znitEeBp7yAPZvEVPJUsP5ZqCO3j0yYDUNwliI6dOPpQBt/wDCR3Wj6eHFujvI + do3DIX9KoHXoL6J11CJYZAONlaWueIYtY8Nwx6ZHu2MdxVeTXKG0eaXKRuCeuBQB0mn+HRe2Yeze + MqRkFmwfyra0rwsIrRmvZICcgDLVw7xXFuFd2uEQfeAJAxUkkjSxh4J7gjPAErf40Abvjq1i0y4S + KByCdrfL+FUI7SR4Wc+WzMOCW5qhf3Mt9cCV2ZiihRk5qpdTSBgRI+R2DnFAFw2k6AqJZMjuD1qn + cxzyyAkPuiP3ieT/AJzV+01R7a2RpMZPVmGQ1WVuTqLDCptcfMBwRQBEkst/YMCSTH8vJqtJaoYQ + JPv1o+ZDZKAo+UnBpmrCBpRNp4/0crgZ9f8A9dAzCdGgkOynxSus2xjkj+L1qW5/fxYj+8D+NRWz + R4fzCd2O9Ai0lzI6mPaMOcZqW4uI7rbtJ3IMc1XScKqncQT0olPlKWfBz6UATKjSDcmdoFWtPCyR + kzckHiqUV0623lKVIPzHHWp7Ic/vSRz0zQBcCqdyT4J7YqC3uZdKv1a2UupO7B6H2NMglMUsmcnd + 0Lc4q3BmaMBiDjr60AWJRBfyb9P2RueWJ6KfQVLHqMdtcEysxJXayN0x0yKyWihWQBdwTOSdxHNb + zWEF5ErXhX7QQAMNge2f0oAnhs4rq2kksHwirkg9SfauXnJnmL3AbL9jXSRWh0N28x1cEfMqtnA/ + Cs+70+O9/fWRIb+76fhSTuBimbyyyKDgnipLk7AML1pZbCWO7Hnjn26U6ZykRL+veqAryuvm/Jwf + Sk3mo2AyHyCT6Ux5pLU5Gwg88gGkBPNAILUO3KmooyjL8ueegzTvPMsRjG4qBwKrW1sxJZzsIPGa + AJbmfp5q7MZx71NZawEi8qZSyHg4NRGLzCPtB3eme1R3Nutocodyd8UAaVtqEUDlI8/N3PaqV2Ht + X2x4lIOSwHFSWkEFyo+cD1BpbmNbNdkh20AMh1UiJ1c9RzWj/wAJa1vYiK1RmRvvetY5gDENxgnp + UlhN5TiI4O4845oAmu51lXzFDGQ8jnpTra4uJkBAOQavXvhG8tIhPawvJAfmY9gKE1COwgIiAZiO + 3rQBV866T52Qsw6YrXguZNTs0WSJ8IPnHr9KwZNamNumZSpPU4pbPxBeRy/uJjtXqfWgDodMtnXK + QjYeo3VnalpiXjMzXMKS9O9VV1ydCXkmLY/SorWwTVJTmQEt81AHTeCY49Mik+0SJKmOg71W1bxH + HLdgaXaSRNnjdzWapGlBBG2ec4GKtQ6yZD5hjLMvbIzQBfutWC2ajV4ywwN2OM/Sql/JY2kKGzU/ + McnBBqlf3Lam5e8lKMv3Yz2FU4VjgzsGQ3WgDa0ya0u7kxzgqCCcn1q43hizkEjRkOoXcAOua5Ka + 6Mc3ygEVb0nW57ac/ZC4Xuo5zQBBeZjcwuMxRn5fUUmnySx6kv2cgg98deK1LjT31pTLpymSVuWi + Xqv17U2GzFgFBUCVOo7igCTT7cnTp/ty5ZnyCvGOKz2uwimOY7geQB0FWY7tzu8xiqk8A96qOvmy + MSowOc0AVpkkgk3uAiP39KkjtonYtnO4cKOP1q1Z3K+X5V2N6OeM8gfWiewaxiKhDsAyJB2oAk0u + 1juAwniYshwoB61FLZfaJDv/AHWexpulXRNwpjkP7s8nu1Wd4uC7zfezxQBTjxZTHzlMigbdy8Up + YXEv7nPvk1aNqbhDhgARnFZMCvbzuWZgc/nQBo2l6qs63AJA6VIsiG4DI4jXP8XeqcbrK5JH3xkH + 0pWhWVR52CF6UAa8kUd7H8rD5f1p5txHAfNPasWRCjgh8D0BrV0a+DgCdfM3DaB9RigCml/JFPyB + 159xV+C/wfNHAbtUN9orxO3k5dhycfw1XmT7JarIjb1k6U2BcuNSVGDSAPu6be1QTXcO0CVSwbPA + 7VRtpftEmxW2Mx6HvUv2V1J2jkdaQBFJB5jBVYemetRyW6SqTKCfTFNllCHBX5vWkLBPvk4NADTG + 0ePKB5qdLN5NjycqvNQIpZAFVj71LsaJQBuGaAH3aCVwycKODUMsZgJjxv8AXIzUs0DpHhmBycjm + gOd37wdRjNAFETeTcARAbSeTViApfrhjufHXNJNCsUu18Z61Xit3Q5JxQBdW0MYKyn5hSf2BPIjS + 24I29T6f5xUMMrs5HOF71ooVmtMyu3ynAAzQBqeCfG7aaPsmuYkiYFG3HseKq67YQW2rSNpLCS0l + GQ5GSh74xWZc2SyxK4OZl5x7d/0rV0K+j+xPFOu4Pwpx0oAo3OnFreM7AR9Kp/2eYpxtyCx6VoXd + g2nSlQzMh6UxJdjqSpKgfN6mgCOLSZGkKyYw/wCn+c1YltRodoWA+Y8Z+taPhWz866DQqxLdmq34 + x0ZbS23yY3NgkUAcZcSyrjcc7zw3YU62meOeTazdOhrZ07TYLkYvSFVfmqveQWkDj7CW9zg0AZs9 + 8wbO3L8ZpvmGRsyZQDsO9WLu0EwZojwMc1DJCrsA5we1AFmGVZLc7Y1bA6nvU1gIyNzgxtnoKr7I + NgHO8dx0pJ3AYG3UnHegDRS+NpL5lsxh3dQverj38OtL/pKCKSPhWU/f+tYEt98xMnC9qgludrrJ + GzFl7DvQBq6pYNGdzHGO3aqS33kEBhlSME0+01z7OcXGXRupJ5H0q5fafFqNuJLLnofmGDRsBmJe + DzMEZGevpW7o8sN/bzLqTBML8oB71k/2YYh83FQRqbdtr7sDv60AX7jSo4ZsiVo067hj9anuNHey + jVizMj8gkdaqQyi+UxjO7O0A96tXDz6rEFucp5HygUANGEQKjDJGaqzWbzgyn5QOPY1p2xZtOaGN + VMo5BPoKqxa1NHHtmij+Q4xkUAUraZFiYScMOgNMf76CIZHf2q5KRq8arEjK4OTsGaki0oKwAEhP + uDmgCohEsqq/O6rrMNMj3AEdgfQmn3tqUgEcaYz1JFMtLdn0wpFGxYHhjQBa026M0XM2WQ/NnHzU + 6Yw6tCPt6rbpH0CdvzrPtrZ45ceU4cHk9qtzW6XLOjqwY9+1AEa+HWun8zR28xU5LAZx+VLaGSV9 + jrkr145amvEY4hGkjKMg5XoPY/571vaHFDr95HHqDMkoU4C9G+uKAOevoo5iSBjBxVYwLdRkL1Xt + XSeK/CdzpkjRMqyJ95SjbsD3rmJbUwoeuGOCfSgC9eWc9rcbbdA0KHPmhcq39Ka8e9DkBS5zk1X0 + /wAR3dvEtuTm3AwVzW/D4w0xIEivbOaSTAVWBAH40AYMu6CZDkFcHcTz6UrtkYlwVHIwOtb91olr + qtuRZSL5h5EX8VY97pc1jKAqZ2jB/wA/nQBRJhubjE4YOOnNMC+S+DzmrMkIA819wPTbjmqwfzcM + 4w3vQA9mbYwgIz/ENvSm2t+6jZsYKeTkVYjn/eqwGAOp9aeW+2sdkgVf5UAQLKY5MHGferNv+6IM + XT07CmyaeZIS1vtmkUdQKbZ+akOZoyqMe45oAvRzjUJPLLgSds8/zqyPDzwETagy4U8YwARWMbcw + NuDDePenPrbXEfkTn5hwrdqAO709LPSbbzlZdvqD0Ncnr/iufX793uWQrGdmFGBjpmstdQeFRHKx + 2Nn5f73+f61E7iLCxDnrjvQBaubtNypAxyRzg0q263DMsJIzzyc1mwyDeSD82e9XIGUIrSyBNw+X + 2+tAD3tSpcFvufrVZbdL2XbnDdjnGKnhs2nkYtcIEJ6461HMiJIApBVe5HWgB8mmtpzDzSrrkZYU + 65mRGYoBgirEkCStiJlC7c5IqjLNsYhtu0d6AKkshbAZcAdc81Gdwb5SD6cVZjYy5WXBVu/pWppn + h63urfdLdxR47MDk0AYjnhehxntVq11OVANuTj8q2/8AhBZ7mwkm00CYKQBtHXrWe+kTWS7J4zE+ + OQ1ACQX/ANrkC3DD0wODV280KQwM0jxheueKdZWcCrvkjYYHUHvRe6jFLapHtLKeDjg0AVrDQ5xd + xuhIUEMHx8pH1roZtH+2W+dPIbHDMOcms+81YNoqWltlFKhQD1HNP0e5udHsHFkcyMRkDoaALUPh + aa1n8yUgqRgjPOO/eq+reDkvHzoQYIB85JzzW5HBLqWmCSWQJM3UEdB3/Sk0S3uNPmIkBlgJyXAw + o/Ci4EHh3QYfDsfm3mHklGGLdFqS91HSYpvMw0jjkhTx/KqXjLUg8hihYiMn746H6Vg+QYxuV9vH + 1oA3xrem38TNe28rqp+VUyD+gpbTU7O6ylvEYoEBPzjDAjp2HeuUk1aeyfNqMH+8BTrvVhqEAMuP + O7n1oA3X1Q3U0klp5S7OGHFZt7rj4DwxlTJ6riqMTiDZsHTn6/WpbfU5EP8AxMVMqdFIOMfWgCZb + lpEO/GDgn9K6bwZpktjcC7lUsAMYPvj/AArBi0lrpc2sqbZsHbjkV20SvDp8UUZBcDp60AY+ueIZ + dIu3Frh0lbD+YNxAPXBPSqLrpuunyNPBSSM7mZyQpJ/KtWQ2uqvNDcjypQjAFjnJx0rhNYhntbvy + 7jcucgIe9AEUMOy5ImYgg4xViVVa4UFSoToc9a6DxZoEdqv2rTsHzDlx/dFcujFpG27vlPGe9AEi + anPpV359o7b143jqo/yP0rWs/FSavF9l1JltlB3tOerd+axl3XGfMXC9896iu7UbtyYIxg0AdTc2 + Vrqe3+zZxIF4Uj+I1S1Hwpexu0kts8aL7Vg2t9JZ8REjJ+UD+Guh0TxjeaW3/EwAuFAxh260AY8y + ujfLkBOCOuabHcqgCxYAbrz0rsbSysfHdzks1rO33Y0AwTWd4h+D2r6M5mmt0ER5D85P1oAxLfWZ + LSYrbnAb5eKnudVnyELFkHOcCqUmjzRzBWyD9K6W38JtLo6TtkLzmgDHtryGZiZUDZqDU1Vl3wp8 + g+9jsf8AOKmGnw2cpE8jFR1I7VdGjRXMQa0kdoSPmHrQBn6bYnWz5NydjgZVgORWeztBK8ZBJQld + x6nFdZ4ZtoNI1QPI7O+OB7VX8faO9rdC7ESrC4BJHqaAOcgUTtuORiraW0M9yiXLAIeoPc+1RWar + u6Haxq7e6ekEZkBGzGVz1ptgVprUw3ku3iJDgDPUYFEzAwZRN2CDgUw3JEkezD7+xolvytwn2pVV + RkADv060gLVlMk4aLIDHp7+1Vbu1+yzgThiHOOelElyIZl8v5CDkVtxWkGtaYs0bMblCcr/KgDCe + 3LzsN20L2HepUQJnHI9KsX+gT29pHKCd79qWw0u4aPcwU4796AL+meIr2G1aDSbiWHOMhR1qxZXz + xXBl1n/iYBBlg/FR6VZW1nciS9mdJADgYGO1Q3pIOOu5hz60AO1vxLDqluP7Pt47eJSQ2KzvtiSg + eWuPpU89gsfzH5cc+1ZaSpbXRZT8tAGjjz237gNuPwrc0O48uUPOM4GBXORXC3HmJD1bB/QVZivZ + fLwp+71oA6fVfEiwXC+UBGjfKTj14qZbi7gtJWjkY2zx5C9s4rnbCRdZiaOUkFQTke3P9KbYa1c6 + XcBARLEWxhzwBU2AotqzH5Ls5YdFPOKmiu1KgxfvCOqHrXTL4EXxLbl9MO6bGRkYzXPal4TuNLu2 + ju/3csfUD9KoDO19yChhO3OcqO1VoZEUbHVckZL9x3q09s8a5uDkZxUDWX2i4OzgHvQBLCwkwyEF + c4z6VNDZm7utkROCfwqCzAhuGRhhV/WtR5okjjkQ7ST2oAlSRtMdUjHzR1p2OuOI2Ly4kHQViS3K + iYBMsW5zSNF9klEjPnPSgC1dzm4uVKSMZd4JP41oeJPD8+r6ZHLbwmW5H3yCMqvr/Os6xu/tDfvU + CqSOfWuj0yf7OxLO2CAG9x6UAZs6vcIqSiVw3GQMisR7RVvpFkGFU46e1dN4c1hYmCXm0quDIO9c + 54quVl16drdDHGzZX6UAV5bTzWIi4Ws6/DQEoQSpI5q9BfywxkS7WU9OOlMa3F8hG7bj5sn86AKc + ErggKVA96lFwLcYHX3NQPAHnYD5e26pAnluA/JoAu6JevFqsEqs4YN0HQV39p8aL+CJVnWKWOP5c + OAf6VwCzrbxAIMMefpT48zEFD9RQB6hZ+PNE8YqsfiJFt5GOC0abcH6ioPF+i2/hiGK50xmuLOQ4 + AjO9s/T8a8wlzLIdxKkHIwcc1s6R43vdJi2xurxsdriQbto9RnpQBal1C1urtzcIVjfqu3FRMNM8 + zbpplViehyAKnuU0/X4N+ixtFdR/67e2fN+g4xzWPcWzWFyDL8gP3Qw+9+NAGhqulSWzpJHt/wBn + Bzj2NejeHLG28f8Ahox6/HsmA2DHBGO9eTrrksUTKSOD0Par+n/EnVdMRVsZYgpHIK9u9KwEvjn4 + eTeF9UY2Jie3HI+bJFc6b6eMkt909j2rsrTxpYa7bGHWYpXlc8Ord/yrOu/B8gEjQul3Ao6RjLL9 + cGhaAcu0skr7mK8HtTjEAcMMk881Zm0l7JXxg7uQBywqqzysygDBPr1qgHSWqzANL6UunXjWBOxW + KsaZcggbu4HSlindrf5ANxNIDqblPteiWrESNC2fujJ7Vd0bRY7KLfZswWYZYSdT2/pWJ4Q8ST21 + 1b2krIYj8pBFdd4k024ht0nsdpjA4AHNAHO6npkSs2SwPase6ieJcSYdenB+atGbWykgF9G2cHvi + qGqMxiWW0GFyCSRnFAFeSN4yGiLE9we1QXYEhzMo+bnAqaC9YzbpSGY8CoL/ACwDQ80AV1mxdJwQ + q9h1qd71WHU/QdqgDO0gJAyevFE4WI8dW60AafhzUHt5v3ZAzxVzXNFku/38Odg9KwbK4ELA4z+N + ddourgQKJsMv92gCr4Y8Qy6VGUmkdLcDjn5/8a6vS5tM8SWTG3kkaZeP3xIyfxrmPEuk/ZXF9akG + CY/LHj7tZy38tvcxSwnYw7DpQB0viLwrIigwhcHqAeKxDpbmcgJtKjOfStXRPHgjlEeuAzZ6bf4e + lajX+navE4gZIyQcFmxQBxd5ZPG+9iuDxmqitHGR5oO09M+tdDqmjNsDl90YPBHSsJ4N7uH7dOOt + MByxj+EkE/d5qwYGkUNu+VetUgxVz6gVNAryx7Y84J5PpSAeZWjG8A/Lg1sabqn2hF8wnniqPkK6 + qk/z/TilaEWo/cgqKANPSbRba8zM6MXGDzVPxHYPPOzOOVPy471R03XmSRXlQEHv6VstqaakgJKh + h0X1oA5jBjYrP8uTkA9TQ0qoxLHqPyrQ1+z6TMu104x65/8A1ViSsVc5GdwoAseWbkDyQWC01QVv + S+5WGcbe9OguTFZqIjhxnPHWnWTCO6LyKjPnpQBDfs4n3sMc8Y7VPBKWT922498U7X0RCjRnJmAL + KP4aq2rtA/ycBu5HXFAGkYg0GT8rY5J5qIw5jyMORxU28zwAou5jxj1pnktAzCUlT1xQBHFP/Z8w + dpNsg6ccj8a6jQPFNjqdqbfxJbvPM/yxTE/LF9c1zsNsJ1U3EYIP8VPe1iicCORsnnHTBoAtat4Z + mS92Wn79WBK7aw0ia3uXW4jdChxkjvW/Z+KLjTZFd4hKwyAc44qy+nwazpxEOPNdvMdx1UdTQBzb + AbSNyqGPf+lWvDPiW58IXDtZzOIpRiVVON4qS/0ePcG04/aYV4Z8YwaoPGJrgq2AqnAPY0AdVdww + eJLX7XoxSKfbnyRwzn61zGooyMzsreYpwQTyn+P/ANap9NvX0S4DQtzu7dhW/rel2viWzWfRiPtC + L88a/wAfuaAOQEvyDepIOOamtbFJZWKzrH7Gpk02QRBLgYYHkDtSTaf5LBgM7u1AEVxbS2aiSNfm + xw3St7RfiTLFZi2vUe4VRt44xWJDczTzoLoFgvO096bMomlkaJfI5ztFAG7Jqdlrcm2WNYHA+82C + KidbiCAoVLWzfKoHOawo1dyGO4bQcc9frWppOvSwQLDcDzQSOvbmgCjcWBQsqDYwOTmo44BdAZfG + OeuK1NYdZLjzCdu8dAKzpLYQt+6OKAK88ciXREQ3AY5/Ckmt3dlMoznPSrMU2zJxgD2zSSRmX5kY + gdiO9AFWO3KSDgqMjrXQ6fYuUAjG3HO7rWRawNeSDLYKnHPeunVG0bR4ruTnc20g96AHxn7ZbNA7 + qzgcVzup2s2mzOl0CAT8jYzvrb1TxpZ3tgr6fBFFL/EUqpp+pJqpxeqJAPulucfSgDDfcjgxAqSP + mB60xXXlZFBPXpV2+tms5W2oTnpk1nht0uZCAfTFAG9oOvCJBb6jueJj8qj+Grer6XFCqvHMvHTA + zmuajlMUmWHznoKvQ6tLDEPtKeZnsT0oAkaBVLGX7x54qOG6NvkEEA/rV2dYLi08y3fMhH3e4rMR + mkDLOMkHg9KALcN7vXI4Iq9ZyG5jw7An1rFuWMWMAopxTzqMkIxZAuOpINAD7ZAcg9F6VqaXdRFg + pX5h92sPzRbfKQdvr61c0+4MjDyxsYHkkUAdA2lvdQ+ZcDIPGOuawNY0wWNywjwVbocdK2E1ubTF + +T5gw5yM1Lc2kOqaX5kXMxG4nPT8KAOSUSKu5VGM03aZmRo22k9Tird26Fgp+6hwcVAZfNmCnBVu + mKAJp7N71FDcuOI8d6pJlLlt+d44PoK0dTZLKCI2HmCZQCd33c+1R6iqXKpJBu34+bPQGmBNpzND + bgH7zHjPapLiXMhEvzMRwarQXG+ILcfMP7w7VZjdHj+QgMOmaQCRF7AsVBZO2am2G5t2kIAJ9O1V + 2vzM21l+UU9Cjj5M8eh4NAAIXjUeRl8/pUa6k1hGFtWyG6n+lWYX25Y8dsUs9t5tkVkK7Tz7+tAE + 9l4hAj8q/RUf+Db0P1qZ/DUWrTO0paK9cfLGg+Qn61zc0SeYc53DgVr+HNfk0u623LgwSDaxHLY9 + QaYFa80a60G58vU1VmbqF5AFWdC1k6PqaTW6qyEbSD+FdRJd2s8IikZJbO46MTmRB7nr2/WsrxD4 + QjtohLo+9kHXPb0pAd6uh6Lrekm6hkkQSRgNtQfK/p+dc1f/AAsuGUnSWSVScgynbisHQfGFxpki + RKw8tRyD0z/nNWPFHji/1lFihkCxKMAocUAaNt8NNSt3bzYrYsnT5xTLvwZYQTIuqzlLh/vqigqP + xrk/7QuIwRHcXG4jnMpP9ary3kzhvtUkrSH7p3E0AdXqPgvT1vI47K4kfcCcYAx0/wAar2ngu2uW + ZIJX3pnjHFc3DqUikfPIGHU5PFb2ka3PDe7dPZGGzGW7/wCc0AX7LRLSzcxb3eXrhhxVG78JeVcA + bvvcVfEgudqaoyrOrbiV9Pwpmo311pMnmWmySH3w1AGRrXh6TRfLMq8yfcHGPxqxZ6fpmnmNddml + jlk5+RQRx/8ArqO51ptT3vMwWU9iOF/CsOZHnkIkYu3YnmgDo7qPTtPszcWTu5LcAr1ycVl6p4hk + 1BRbsCEXkCqEGqz20wEWGEZGAeRxVy+vRqV2JpUVJiACQMAUAZ0+mvaNuuz88hwAOmaktbt7C4Ub + c8jvW5rGkp/YUEsRM0nLSf7PFYogSWEF/lJ6CgDWcjXyuMhwOAO9Y09hLbSyKy9+pqzpM9xo90Jr + co2OMMM5ropr2PxBYGK7VVXBbIXG4jnrQByUI8xSADs6HPWpPLIjGxssvr3pxQmcqx+VGwFHenJI + gOF5oAW0jZB5nQnnH6Usnzjrg0rW2/8AeISD1x2pWR5VySNo60AQBX2EzHIXpSQJ5kjOOFpLgrtI + iLFvWi2Y3CFYuoNAEt4myTBBQ46Gq6OyHKjGTzSyyyXUm+/cnHc0+PY42RtuDcDigDS03UzdQlHG + WHFSw3/2CX99lo+hA64NUorOeyG9FJA68VJFaLqNu0hkIlXkgelAF3VtEjvNMF1pKOctyPTFc/bw + tGVeMfMRzW54f119M8yJ2IjlGzk9B/k1p6f4fsmi2xXsUmeP88U7gYV5Et3aQlWCsox+NR2eUnWG + 7bdvrZ1TRY7FXjuQsatzHJ7VkyeXbxnz38xl6NmkBFfiXR3MDKQjHI9xUMV0ijMnNdBZWbeJbUcC + SZU+U454rFu/DF7byNJcW0qxqeeOtAE0EcbI+4nax49qnKNY7CCG46Vjw3DRHO1gtaNrqPnBRKu1 + R0Y80AXYDHPAzlPmzzTWG2Evn8KafMMWIsFfamKxcAyjAHbNAFSeRJpOBg0xrXykVjyp6VLqFv5b + AqwTI6dal02ZZ5VjuMNGentQBJZxXFtFuUZDcitDSPFrwOYrkFkfj6Vl30l7p87RpKRDn92eoIqG + 31gRxk3qMzqRnmgC/wCJtIa2uzLYfMjgEj2rNs70woyIMjPLHtW7Y3y38gkUnGBke1R6p4dS/mNx + obeZgfvIVH3Pf3oAz7W3EmGzgrSSRqszF13+4/hqOOLdGSrk5HO0d6WCUxYaUMYhw4HegCM6TLcy + Ztkd0wckd6jtZZbPiI+aqnlem2tTStXNvcbYZyiSA4QcdMf41Y8Taf8A2dZieGMR7sAkc7s8H+dA + GVJqTT3AKtjIxtrStNVy/kyLuUj1rAlhG4NtKqOc/wB+l+2SpP8AcKMn3s07gdJdeHPtLRS2zpCr + csD171laro72bGSFWZRwzHpQdUe8hTDEMg5xU0N7Pcx7GVpIf4lzSAwlk2yAoevUDpWpa2hvYeTg + 0mo2UM8w8lPs4HUDvRpsFz9oYW6NKB07U0BbjvptGhkgJDRMu01VLRyyIYQSgA3HstVdVMiSlZyx + bPKiksbyS1hdWUmKQ5K0gJpt8UgAw69iKn0/UyJdrdOmKIPIvW/cyLEqj7p4zUEUIEr+blHXJBx1 + oAk1O28q6VoSFVhk1GbZQ25TzUlvcfakIucKAcAnqaWK1cyFkQlB70AJvJdNq5I4+tBcbCnCjv71 + LIVcAowVhxj0qO2t9zkXHKt0bsKAIpbPIHlKWUjk06wgaNiqIBzViF/kKKwBHA9aguI5oX3REk9j + TQErWypGPOGc/pTLTy47gMFyob5fetB7EmcG3G6N8hSTjNWRpgsws/y7ouWB70gKd5dGSRcfKnIP + HFXrHSYL61e4kfyVVcYA61lC7OrxurAKxbIHtUtxfC2sTDA/A49KAEazRmkEw+TqG9as+H7YSTeX + bvu7ccYrIt7qRdobPLc59K6jw9pf2KUXcJBVjuI/z9aALF88MsJh1AiRoPl54Iqt5GmXUG3ABx1x + 0/WneMbGfTryO8VB5d2N6qfTJHP5VBoNtFqUb/b28uU/d2d6AJLPV4dGtP8AQyokHGKgu/Fwu9wl + PXgj0pmpaSmnOxmYEdu5rOht2knZ4FX3oAimiju3AtlAznrVWSAW7OC2HQ/d7VdNjLaMjurbSeMC + s+4WS41BjyEB5zQBcgnk2ARnJbqKZcydmZt3fFVxB+9DRkjHfNWLh/KKGTp/6FQBGLg3C5PzFeBT + LeT5yEzlB0p1zb7wGtzt9RTNhWVQOHPWgDc0iUajbPbTgM5GE9aydTtPKk8sKcDrk9adZX5+0FLc + FZM/K1dPpmgReJLR2nOyZDhQT1z60AYWgXYtrvy5cFXBXA9+OtGpLceH9YIsZ3BwGI4+YHsaNR09 + 9C1ERTFTMjBgE6YyO9S+IoDqHlag5++RGPfGKALelpb+IbtA+Ldk+ZkXofxqHxFpn2Vpv7OXdGOW + 56Vk3GpCBQB8pB429a0bHXN8kX2gKY1ILju1AGakfmFfJXLN0/z+VdZYQG503yda5xyPp/8AqqXw + 2LKJJvsqbjIdwDL936Viarq8u9nhA8sNg88/TFAGrdeFbeWBHscSL/AM9DWRqnhObyS7KUYdfetH + wkx1Gdnm3rECAB6Vu674psYbIRxeZuHBJHWgDzZw2nybQMluDVnT9T2PsJK56Ve1OS1vJ/OhOfXj + pWVdWctu/mJhgTxQBeYrOS0xAxTojJHKHspCQ3GPSqaXCTuqpnf+lTQIJ5XRXwy0AaN7YxzWzT3I + /fSHp6VnS2LI8Yt13kj5ucAU17me4hYbvkHXJ5qvJfDMYDNlevqeaAJTAVJGBuHPFSWuoMN32iNW + UgjOelVo5vNUvg8HGKVollOIG4HNAGhb6dHewhrVy8gPK4qaFTZZRssT1GKzLWd7C5zDlS1a9rq5 + vU2uFAIznuaAK93po2GSIEjqefu1C8QZApc+uBxWnbQpeyCG1OB1cnjmi5sUuTlxgpTQFBAYCWEQ + bjrmmsHvDypH0qYqYGPlk56DPSnWFuz3BN2MCkB0niGK10bw/ExCyMxwhVskH8K5O98SPfWixqPm + AxkjBNEkkz2iQSzgqn3U54rPm4RkY4YEfhQBd0gPBMGnwc8fSpvElpFBIGU5Y4Ix0qjcanIkKBG5 + 7VGzPdIHvF3P9aAHpGtymc4Ira0fU5YYUG7KA5P0rAEgjOFjfHtVqzndD8ilFkGKAPTri4h1fRrW + DVAojmjwjdwPY/XNcJK6aTfubdjhDgc9a19PnbUYLW2upsRJ8o61S8WeH1sryKJ2AeRSUb1oApTX + TXpaQMWJGcdal8PSf6UTcj5WOKz5YW0zgTKZG44Bq4THLpSqj7LhWJdsdfSgDo9e16OGFba0ji3p + wZCBzXOoYZp2N2u0Mecd6Zp12cIbkfIBzTbwRG53W4wp5oAbeWVmgY2ZYeuTVC4SWFAzjdGO5qws + HmK28jaTVi1vhaR+XfRGeJhtVR69jz6dfwpgZEcrPcAp92pl2IzMxLuRwamfSJZCXtnRhnLgcFR6 + VWc7J9mNpbtikAW9w0MheQj5ea3NG1Y2sPmWhCvjuf5Vk7UadY48RseW960rDS11C3b7EMzL3oAt + 6hpn9pZu4GzGq7djH5g2PzpPDsMV/Y3Fveg/uVZl+vNJYRy2KhXfcB972q5aRw310/2eZLbcuCWH + X8qaA4yTeT845B4qaEqjZlVtzflV+80qY31z/Z8T3ENqMs8ZAAGcd6zoZMncEwH6H0pAdDpusLZQ + 7Rjc3ApkFoZJHmY4iAPXpms8R7oh/Gc5HtXQaALbUtGMN6ApPHrzQA/TvEdsdOWD92rRk8gcmud8 + QXkl1cZzlfapr3QP7NujGjfKTlSKzr2Jmdgx/wBX096AIkn8ucBQQjdat/bWMLZKOOnOOKzdjL0P + BoiXe2Cu7vQBpxC0KAyK2488Hiql3LskbaDtbpjrV+3tlubYC2TExGBVe+tJNOAF4PmHNAFO0meG + R1bI9jU0iK23zcbsdagWYO+xOH7mrkMWYcNgkUAQwKGA4JC5pzyFmPlEADt61asYIgSJWA3dOKv6 + zosFpdxPaBGVlG445BwKAMwuWADAbqs6eI/3hl++Pu1cj8NFyrRncAdxb0psElpY37NMhljD4YKe + poAsWmm/aIjKknlsvUnoalhtHLcbiueucA1Uu9UMs8wt4SsOfkUnkCrOmXcotj9rkV0HSLnmgDoD + 4JSXSzPNNFJhdwCkZX9a5+K9gD+XPgDdjNTpez6ZZywwPskcZbk/KK5qZ2llPmvvYnrQATr8zE5D + N1zxRbou7951anhZNYuUVFw7dvSp59IltXdZ1IZKAGvpLNGfLAfufaqDCSKUEkgdMkVd07VWs7oG + XLL0x60+7ePUjyCpByMUAV3bBGxsk1ZikV4gAMkHOKpzW5SUmN849qjjnlil3KODxj0oA6KykW7t + yJW8pk4BFdxrGhwax4TS5JWWaEBEY9QDn/CvNrPUfJmBcZDHLV0s2vsfDMwt2ZYy4z7cGgDHv9NK + yjfD+8bgYFUNRtTps4S6HlkjIBPU/wCcVeN86xKZmJlyMc5p/ifU5L/RYVmto9wJUyZ5oAy01Dfb + qZV2xnoKbfX6NEv2ZcHHWmPLFJYQx2ZLTL1U1EIJA+2bAJ6Y5oAIboyDb0PU1c8xLkBJLna4Hy44 + 5x06VAbZbdcyZ3elNBXeCRjnOaAG2808N5syYmJ7fx+5q7tW5QCZQso/iqsULT7rXLr6k4xVi0dX + +9kmgBlxpbI7SxqZAoGWz0p+i3txZ3AezJAHXjrWlZ26mFyzEnPC+vStzTLO3vZ1M8Yjwp6Hr0oA + 5/xFqyrIggQKrLlsdc96xpQZ5wySbu2DVnVYQ9/MJCSitxVOQFW4G1aAOm+H3iGPSbie1upBDBqC + CKRugwOfwrI8VWsenazNHZtvs0fEb/3h6j171Elg02N65x6Gt200i18VwwwXcjQ3Fou2NQMiTvye + 3WgDn4riKEhkfKf3h6+9aFlGLeyS8eT5DIMoDnv3FXZ9I0iwhJFxJLMpwY2ACg1TvvISzMs77S5w + EUcUAW9dH9qW6y6ZKBgcgdawoNOu7iWMmNiWOMDtT4Jxb5e1bKuMEHsfWpNM1ZrG4WWFmct0BHSg + CprWivp0u193mMeR6VHa2jmQbVH0zV3WNRkv5mkn5YnjFRJGBMjRMScdKANvR7OO1u4pS+SGGV68 + d61/GnhSHUYReQyqsZXiPI64rK0S5hRNzfePXvWr5w1KIwwucAccUAefW1q8kqiT+WK0RpdzFFuE + bFT0bHBqxrFj/Z87LjDZ/Km2ctw7Kgk3KO3SgDPQPuHmqNynv2rRs7hrhjDIcDqD6VPeafDfWbbC + UnUjav8AeHfn8qsaL4bl2pLcYWJT85PYdzQBq6dfjRtKX7QnmC4JQH07f1rIl0SztbsSrcoQnJQH + qaseJ7mBVT7PIXtDwrYwQ3esOO4RrxvLZmjI+90P5UAXrm881T9lHOeAOareXPH+8BKOB19Kb9rF + pcq0ILDPc8mp7m+S6k3fdKj7vWgB8Gtj7Oq3AZ3fCs7DmorqxQTbl+oAqJJlu4gJMKwIxT3kNq+H + G5/7o7D1zTA7Pwpd6NBrk5vQwMv3Pl+7UnjAwwXX7tFe3l5UjBbHvXP3GnCOxhuo2IL1G+qPcFYX + cknoT/n2pbgVZtGFxZvNbH5VOBk+vt+FZ8lrPakrcqyHGcEYzWidWS3lCxAlVPUdDWxf6pa6nLH/ + AGlH99QoI4wTwKbA45pHEirjk1asbxYZCsoDYH1rV17wyumSKVbeGG4Y6gVk/wBn7UdgCpPc0gLw + aEwtLKMDtWhoNykVwHdd8JGCjDIrDkSW1g2zOhVhkVLo+puSVlKlccYoA6Dxf4PbSLRb21wto7DG + W7ntj61mpKdXtxaOQvlfMCSBuJrqLfWIfEvhg2muKzQoN4CnBJHT9cVyU5hEjNbB0CHABPNAGTPa + fZriQONjqcZ6flUtqqB1SRmMr/dJzWlDaLrEUh1Qbnx+628ZNZE1s9nfctxEccjpQBO9tLcy7Zjw + vfNQ31q9oee3A75qe2Yyzby5OKiutRMsjKQDg4FG4EVvEyfM5xnsD1q5bbzKHBAB9KrCJN4YMd3p + V+wt8szRZUCnYDXsWSGPz7jGI+SMVVuvErXKEWuRk9QMYqXVyLXTUyRmRcmsSC4EAO8D2pAXxbma + IMR8w7+tVdRtkUAT9ew71as7wsF2nFGsKodDOMzHo/YU0rgULe7j098qW545Gaki1FIbwzeYyzfw + EdvyqkyGSfaw+bvRcQLayqyEnAyaQHR6gi6/pXnBER0IGFHzN15rnmlXyTGRuQHByeQau2GrS20G + 9OhO3H1//VWhf6RprXbXmnrMtuYsOjNk78DkfiDQBi2rpHIVQjb1otHPnBZAMAdRVUQiW6Bgyis2 + Buq29q2nXJjn/eDsycUAOLCG8yg9zkcVCzeVIZY+cenekN0LqYRSHAHA9aLMCOTy5BlTyPegCxa6 + ltkL2+ORzxjFWbTXpLSV3Y84+XFVJvLilKjgVFMpAyBxQBq6prEF7bQSzA+ZJ97jpVRGjDbUJAB+ + U+tUywlJUdE6VteHLK3kuoDqQZ0zyAcYFAG3feVo+io90u2d13R/LyR35rm77VZNSmzC5SEj5hnH + 14/Otu+hv/FN3gTWywW4KRqQM4/OsUeFZp5miaVAc9R0oAaXWa0EUWCIjuA9PeqEMbCYM3G77oAr + bi8Gz2YDmeLc3ygev61X1CxnnuTE8TvPb9fKXigDMuIJFlBdtzHnAPSrEF0IwDCm5hw2VNRzxTWt + 0BeKVMnTIxj8KZ/ahtgY49uT7UAX7VH1K63oERVOTxiuu0ex0nS7L7chJkm+R1kwwyPQZrh4JJDw + zbVbk4/OrNpefLsnyyg5UUAf/9k= +END:VCARD diff --git a/htdocs/includes/sabre/sabre/vobject/tests/VObject/issue64.vcf b/htdocs/includes/sabre/sabre/vobject/tests/VObject/issue64.vcf new file mode 100644 index 00000000000..611052907a2 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/VObject/issue64.vcf @@ -0,0 +1,351 @@ +BEGIN:VCARD +VERSION:2.1 +PHOTO;ENCODING=BASE64;JPEG: + /9j/4AAQSkZJRgABAQAAAQABAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQA + AAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAABQKADAAQAAAABAAABQAAAAAD/2wBD + AAIBAQIBAQICAQICAgICAwUDAwMDAwYEBAMFBwYHBwcGBgYHCAsJBwgKCAYGCQ0JCgsLDAwMBwkN + Dg0MDgsMDAv/2wBDAQICAgMCAwUDAwULCAYICwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsL + CwsLCwsLCwsLCwsLCwsLCwsLCwv/wAARCAFAAUADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAA + AAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKB + kaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZn + aGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT + 1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcI + CQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAV + YnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6 + goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk + 5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD8J7JbO8tYo1tIFCDLOVG5qfdaVZRwmSOFWzyA + F4H1rLt5WViMhdp6HgmtKK8O3B+4Rhx6fSgBI9FtjaNN5aErwRjilSys7lFAt41xyTtqc2yJCVlY + 7eqgGqv2jyLcebjZnGPWncdzT0+w0u5eQXtrGiBcIyoPmNMXwpb/AGMTSRRbH6YAyPwqK21GKdfL + BAVfu+1SQX4jnjKFsp03dPypCKN9oEaKSkC7R0bGKpnSlSPdHErZOORXV3Ouy337sCLB6kpx+FY0 + t+VfyrgcbuCB1oAfoMemrcImq2sZX+I7ATXS618PdK1DRlvvDEaMq5LoV2nisx4LVrUfu5BOePau + m8EQS6PY3HmFXjljKhTzjOf1oA4mz8OxvMrLbW5RD8wbByKg1LRrRriRYY408w/KAMba1pRaWt/H + a6a7CVm2u7N8lUPEujzaRekzSK6tgqVNAGNBZJauY5Yon92GTRJp0ROY0Un0A4q3c2odkaYOMjii + KL7NIDGcj1NDAZBplmmWv1xnoFHStfS/DFpewqYoYm3DutZ8lv8AapdyOqk8EVteEbSe3KBSrDrQ + BT8S+HbawiiWGCAPjsuMnPesqHS4JSFlSMP7DitbXbvfrkkM2eGw3p+FMfTh5X+hr8w7t3oAhOhW + u8MkMZUY3fL0Heo9UsrN5FFrbxKmMBgoG41fWFra0Acjpzg9aoXjtgRoo29vagCoun27kbY059qn + bwykskYjRArdTT7GEl2UqMr2q/JtVU27iR15NADdK8DC/wBPle2iicxNg5ALH6Umm6FZ/a3ttQt4 + g2Cqnb0PbJ+tamn3j6ZCW0nILfeBORWVfO4dhLw7fMW7560AZuqeHf7MuTFcRpv6qVGVx70q2Eci + QwyW0SsPvOqjJrUtb6S9tHQKGeMZYuM8VUs7gRxbrncy9mWgB1x4QtTHvsQWkHJVhhax3tkhugHh + UkfeAXIFdPZ3v2uxkQ9G4jI6/j+tYun3r2Fy6yxeb2Py5IoAqXenJ5xaGNNvXH/1qcLSGeBdkSg9 + CcdaswC3be0pfexOMnpn2qaS1KQkQASKoydvLCgDNi09RKTNCuO2BxVjSobc6gqXMERQHkleDUsc + u9VADbG6qOWAp11bLbptkjlCkZRsde9AFi5sbO3kKfZYTnkHaOlVbuO2F5thtYcADjaKXUpHj8ku + Co2VDFL5wLeg696YFwQ2z7Qtlb8HJO0c1Zsr7T7a9kL6XazZ4CmMFRWfHdkEgjGRjPpU9raP5LSP + j5h2pAWdQ0+z1KdG+y21qvcRqBn8qXSvC+iTu63ssqyE/IAuR+NQwSrGm1g+c8E9qiSQW9wPNYYP + OR2oAW68GNa28k3lwGNHwvzDJGfSqM9nHBgm3j59QMVdmma4zIjsUBHy5OKp6o8s2BJjZjjAoAro + /nysbgYY9zWmLPCR+WQQwyaz4k2F/Pbft/GtKxvUeFN+B2x+NAEptsWpZSdo9etZe8su2X7pPFdU + LeOazKqVwevNYt7pw5EA5HIxQBQA8tAIeGz1NWIJvJlhW5OQBzjrUMR/eN9pwoXjB4qQ3ERJeYcy + 9P8AZoA0jf8AmybVxsHAFS6jp63ixmwjIwOfrWfaou12GcDpmt/w5qJhXc6hh2GM0AZkHiRpblVl + G0RjGMdxXQ+H/E0Rm+bjdw1crqEHm3EksY4Y9PTmq0cskc42qUOfpmgDovHOhLBOZ9O+aEnIUdRW + QZft1sgum/1Ywua3fDfiFDL5WoEPEwxzzirPizwTFPZC60kYUjcAp4NAHPSq91EoRS3061DHD9nb + 94Mkfw020v57GbcCRt4IIqzNcedIH2jc3JyOaAIYrRZmJxtNdB4fkGn2hluBgBR+NZ2n2X9ozAQD + 5qvaxGbKIRXkuFU4C96AMDxBKZdQkuEUkStuUegpNM1eWScAkqpHTHNPlwbjMzExZ4Pal1PS/s6+ + dY/6vuwPSgC9G8c0A+1xEknrnpUVxaeXNm2dVUfjVazvEZAEkMrccZzV1YYyBIhJP8SZ6fhQBSmV + 4JfMVT+96UJdSQdcMO4A6fjVmTUoJiqTOMJ/q+elRyQs0TtaxF0PVhzmgCzpd55r7YI2HHPTmrV0 + sDTF7gnJXGO4OKyNKgn80NbFhjoBzWjqdg6SISPmIBOaAKVnI1leyhsMJOD7CqOqRtZqotjiFulW + rhsSMshKH1ogsZbmF475TKifdf0oApabevHIAhCYOdxp0t59luS0I+995uxqpdRyWsrqmXGeCR/K + rVlZfaogqv8AvD/CaAIY42kV3K5zzn1p9jNLp6u/A80YPNWWsJNPAVpC4JAZT2HfFWJoVmVVjhVk + HTPrQBPoi2wsoo4APtBHL+tP1mS5uVEFxgJGNqH15plp5WmyBriMRsowM8UybXTNdbrpd6A/KKAD + xbJAGs44FIPlnd9c/wD16ynt/LiDW2SR2qa5vP7RnMs6BNuQMd6jhkAUb2K8+tADYp0fhj8w6itC + yQ3CFYeAOoqi8Uew+UMuf4u9T2NwIW+UgMetO4FmS6RJ1ik6HqxHAqC+gimUiA8DvjrU0kcE8ieY + itu+8c0+bShaWxksSZoM4b0SkBTgha0cq33Cuc1SvrrLFV6jpWqbuGe1HnnDdAKy7i3WSY7OT2NN + AMulWSV8ZDNzxV7SlbaFjClx69Kpww7W3ct7jpUtnNJHd5UjZnt1NIDdt7h7NQ7qGfpt7VR1XVEh + dhEpP94/4VpafexTy7ZlbBGDVHxFbQh1j04HaOTkdKAM5ZVlYso3E+tVp4w8gx0Bqd7QxNu+6D6V + DIoVySxAx2NAFyNmli2pjYBz61paW3lWrFS3BwP8/hWJbTBFJy2D6HgfWtiTWPsqxraBHyOeBg0A + RSoLSTdIepzz0606exTWyQGMXljORTNT1B7+ECZR5fHzDqapfbHjbFkTsIwSTQA43ptyyS44Paun + 8N64Z7Bre4YlZBtU5+7XLTQbjwN4Pb+IfWn2lw9uyrIw2Z5HpQBv3GirHc7LxWVZOVI71FNp7WDg + QYlIIGD6VvaPdi+tljb5yeAzcn8DT9YtbPSpVhDM87jJ3Htjnn6UAUIrJreD7Si7MDoKhv8AUxqt + pGt5GqIOr9zRfLM8ZFgZGtex2nGe4zWKN8rsDhYx2JpJ3Atx+HxcRSzWcpcL/CRwaj0zW1sQy3cS + nsFPSoYJpbIl7dm8tT8wzV7+0hqEO1Y4lQ9cqMn9KoCp9kW7kaaxU+Yx+5j5etWrb/RGxfr5bkdu + lW7KFILpfspDbVyc1fjNnrLtHqOYWP8AFjGfxpAc/e6Ql/GzW4AfqBWfpupS6Xer5vPlHmMjg10V + 5pp0u4JhYNGvAYHrUn2WLWrVo41AvSMRZAC/8CPr1oAvafdWOuNG+lqDekY+zg8MPXPX/wDXWZrF + tcWNw0erKElB4Rf4R6c1BpqyaBdbrnEcwyAc4x06H0rQS9a9jUTgOXPzMwycexoAw7u1jYb3zkU3 + Srtgdk54PFamv2C2pDQbWjcfKCeSa56aJld23YA6ZOKFqBrXGjjULuOKxKuZOTn+H/OKwr/ztOvs + uCrg7RgVLYapPbXAEW4EkHJNdBNBH4gtgyhFmXuw60AVpbT7VpiPJ94jLetQWsDRSIYz8mec1c0+ + 1nexdrw7GjJXk/epsFtDPG0bOdw+b5SaAKWsXA+14Y71FQi5S4RvlAC8A0y5hHmHarhvQ9BVGSQx + sUXPHX3oAmDCJ8rzgHg96gQ+ZGWbg9vahNRG7EnalkkF6hEXyD270MCWF3aEhdue1OsmNnMAih/r + VaBgAUY8561PaubdnMxJXseuKANhIY5Assp2v12itZtAgubEi2nb5xuKYHWubstQaO6SVzujTqpP + X8K2rXWLRF8xZJPMfjAzgUAcxcNiaRSpUocc96sW+yNgZCMVF4lvJdRvTOYkj52jbgZ98D6VWmlY + 2qCUnJOKaVwCzviibANwYc8Utkdl7tbKhjxmpUspvm8tgn16ipigSEG4G4pxu9TSA27GeFbRlGGm + P3cdhUN8GEP2hV3JjafrWfpU/wBmuAcZLA4/Sr1trkarJHcRmSEZO3uTQBmrcbZCLoDZ2x1qOHSi + yebJIAPQipp4kmbzI1EQJ6GtCxsoHP8Ap91GB2yDQBlSWO+M/ZsBHHzZ71XkfMIWNgGU9vSt3U9N + t9m21uonz0Iz/hVCfRkjg82FhtHDGgCuZ8EMjDZjBzSZ8pAwU7XbGT0pWtEjjAZgV4PFOml2QKqk + OoOcU1qBNYRSrdkrhw3BIrah8KwXoV/m3PyVzyDWNp999kccgZq/ea7PFAGgZlJ6EUgN23thpdi4 + V1Eucr7ev9K53V/ER1a/MkuWdBtG04zioLrXJ5wDK2XAxmqVqmZ2YPtHJ/GgDsvC3i0ppr2d2ish + yFAHIz706bRLNdOPnErKw4y3NcvZ3pjA8o4kB61o3OpSX9nbx3QIkU/MwoAj/sGaPzFjlWSJjk46 + ioYYwqssjIHHAHpWm4ESN9nYDIFZV+I7uVI1wrY5b1oAtafcvb3W4MM9Nx6U/VZpNRys54ToU4zW + KXaDKrJuC8cVdtpi1gzs43HNAD9N195bdYtRIUR4wD1NX2KuA9uThuSQelcsZwzq9xyzfezV/SdX + e3m8pXJhkPKkUAdYZk8RywjVVJES7U2cE/WtA+HDHohuY3Uxg7RF/GeaPBlxaawMW6rHKnAU9SOO + lX/FFv8A2bpzTQk+cpAAz93nrQBx+r4c5CODEOA3Y+wrKu5V1C1GFKznkk9K6Wzv49fs8Xf7y7DY + MhGNgrmtX0s2t66WknnKvUp0/WgCnbrJFdot0NwJxkDFdDYp86oMjjIArJivxbR7LuMyEjKitS21 + MW8auuW44H93/PFAG15aXdr5Uv7uULkA/wCFc+Yvstw0at8+eoq/p+rm6vRJMNwIx9KranYySXSy + WEZZHOCw7UARXFyj5STAk7ntWVf2gALLyfUVoataLbfLO2SO/Ws2c+VwhLK3QDpQBmz2xAyCG56d + 6uWPlnCkFcjoTzUBkMc/3cZpwn8oZkDFs8HsKALN1apDIHOeaiLkRkMOtSXE6yxAsRUcdxldswIJ + HANMCuJW8xQgOP51oacWPPGAeRUUOIZQzDhecd6mbIcbPusM0gLmq6bHPohlhDeZuH4c1zzF1+Rs + HByDXTae0s0IhjjZg3GPWqOs+HpLCTbNGyb+cHrQBZitjPEzW/LL97vinw2v2m2aORec9AKXQbsw + ygBBiX72TWxfaS8kiGFQAwz8vWkncDlbqNraT5cjb/n+lMGckx8kjOa1tU2TxkPkMpxyKyrhJ4Wa + KIDbTAkgvIp7URzgBwe/BpZYrd4vmZWNZ81x5cgBXDdzVlIvtUOGIBHpQA2aEROpR8DsB2q3bvG9 + iySzEsTkLnrVMqViCZzt7nrT7GBVuQRnODQA6Q+Sx80A4HApEJB3BAR9K19EmhkvCJ0ZsKe3tUc8 + Mc1yy7cpn6YoAzoUiclnYYY8AHpUl8zRxqpPy9qtC2tULgSMAvQ460lzIl9b7YiDt4GaAKMMQlJ5 + z9Kj8gIW5yKnS3Crlzhh6d6k0mbyZT565Q5z60ANtrRpPmhzWhbwy7DJcDhhwMdKlt7aK+gb+z33 + yKdxVuMCqaz5cqGYfWgB6yu8rBB8o6Gs/UpjGQXBGPTvVmSfyImyepqrqjbIw3WgCDz1ib9yOTg4 + NbVlNBJYvlVBHt1rBaPzQWU4IHSn2FwRJslJxQA6e3M0O4oAzdB6VXR2iKGQENGOK0ms1eAkFjF/ + BjrVGaAo371smgC7pety2kwl06Vo5AOWXmuwm+Itv4g8Ota30aWlySAJQfmkP/1zXIeG4Y5SVBB3 + evamXGly2tydwG0nKkHpQBZ86fRbpBLI252y4PGRWhO8Ml1IbJhHn+BTnNU9O1oRwvDqqhB2lHJP + 4U6awb+z4JdKbzdh5ZurDHtQBat5LaRHiaOP7QejEZKD/Oauy+FI7W3Bsroyhxkq3QH8q5a7ujM8 + nWOQnBqTR9burCT98xdR60AbbaHc6ZG3ymJsZC/3hVnw/fNIXt7hygHzZp2oeIBqCxzqfmCgEe3+ + RVdrmLVAEtf3bxfOW/ve36UAV7+7DXMu5Q4/Os2e3eRWkiAGOijtWrPodxfQmeNVAPOPWsppJIpi + JxsKcY9aAMwRyTSbpflx68VOYvOXb97OKtXAiZdzkqT0AGc037BIIRLHjsR60AVprZrZwGj4qTY0 + xyRj3PUVMJDduFfqvFRzxJCzrCzEr60ALEu+YI53c4qeGB7lGCnBU4FUopTBLvfk1at9R2sAMjNA + GtaXsnhy2FzPHvC46jgnNQ33imTXrkz3oVFAwo9Kfrtq03hAzEfJ5gyc81hWM5hhKrhgT0NPcByS + P5g2uVI98Vp6X4uuNGlyzCQIQR0bI7/1rNQxqW+05J7Y4qK5ZYUP2ZCW9TSA7SR9M8V30X9nMFZw + WfcNi5qPWPDtjo0pE7O03U/Mf055rmtFmN9E0DEox+atPWbiW7lSO8Ja4jQbcDC4A9PXFADYtM0+ + 6nc3u7aOm3IP6Vnak9tYt/xL/M445zTIbieOdmWNsE46cip42EkyC4hYx469KAFsrT7XEJgFPOT6 + 1s+H9PD3XlzxnL/MDtqn9pghgb7GjL/eJORWqfEnmrA9oFRoxjJ5BoAp6NqDW2pzRXtuyIAw3FMf + rVS4iF08pydmeCDxWvqeuC+Ro9qglcMw71mwReXD5aAlFJPPU0AZ0cEsbkSZKH15FD2xJJiJVj6c + VfnzLGEXAA71PFpDPaebE6/KOh60AYVws8TBgrFe57CmHUG25RVJA7AVozzSLbNvX5T1AHNY/m/Z + nPlqwDetAEtvqzJNu3FZBwQBjI96vPqkd3mRtokH31UYx+VZqWruxaFl+frkZxT1tvs1ujJgEH5m + PR/pQAXl2S371XAHI+Wkaf7VD8hGR2arKySylRccQ98DmiS0jifdsdgeODQBQd9x3IBx1xTYlBm3 + En86sXUAwPswKg9QeaBErIEj6nrQC0NHRtUjt0K3AHzDABGcVW1fTzJL51jyOpz0NVooispebBI4 + wK2YFEthk8qR07igDAgJil+TKtnnHFaP2h5yI3ZsgdSfaqd2P3im3BGM9aktsjmRgCOaAJZrMwR7 + 3A5PT0pdMvZtOning+byzuVDyh/A8VHczSzDPy7RwOKgiuHEewjKeoFAzp7TUNM8XXEw8RhYNQmP + 7ny18uNeOM7cCtMfDiS8uY0tDEYghyynjPbn864htP8ANhLIehzWzovxDvtFsDB9+PI4I/rQI0r3 + wNc6DO0N2VaQqW2q24YxmqFhYRgE/vkkDfMGBBP4GrSeJ7tZd6SxvIfmK4yQP84p0XiyC71gS65G + 00zAKGX5Qv4UAbFpd28WnIsBLsDzmub1+AXt1LJEoQqfu4xu+lbWsWgs4/NsCXjPIbqK5+5kklmE + rDD54BFAGb5cjybCrAnnB6ipEvXil2sM4GMVpFY7m4UNmNyOWJ4qteaM0BISVZe+RQBFHC2/zISg + B69KlIVhIHA3HuR70lqotlBulY5P4Vcls44k3u6N5oyoHb60wM6O1SRir5LemOKv2vhuW4iLg7VA + 6k4FTR2ax4aaVIwR3HWqGua5PcQm1WRBH6jqaQFzWbE2nhzynuIi+8HaHyKweJSEQEN6jpVcKyOw + cMVznOeKmtZvOPDKuOKAJbi0JYFf4eue9IW8sncfvdqnlvVFyFyu09abI0bysMZx0oArC4eCTcgb + juK2dNvE1N1M0ohljGQzc5A7cfSs6aweWAk7kTuapQysIT9mOSvG49aAOkvzLMxk06QNuG1l7j3r + PlnnJAuGJij+nNQ6XqT7wEYqyn5v9utLULaW7j321uiEjLqMkKKAIotbghb/AI8hKGPIBHNXLG6t + 7uzk3RLbKG/iP+Fc+8f2d1eFztzyD2q5p2oCFWRoxOX52nPFAGgLyC2lyZFKdB70r69buxRJBHjr + nvWVdeXLE7xE8fwnoPpVKZUnQPkBhwRmgDq7a9tLyARWiiWYngL1qG4gurJ28+NowO2a5a3v3smD + aa5WUd1HNbC6zI0KSX13JO7D5lbHFAE4V7pi0b5x1GazdUtXSM7v4iPw5rQ0/XrcXX75FgUdxzuq + /qFrp+sWRe3uDkc4BFAHLRDY42ycd6uPOXiiV+RGPlWnXOg3IQvEmIB/Ft6/jUUEZmMcgydvzECg + C1G2+Ly3YAvyM9qY88kaFcmmp807uwPJ4FS3do+Fzn5ulAFVrjbgS8Z4yah2C03SMffNWZdPknVA + iluQOnHWmX9pILvyY13HHK46UAVre7LSyOCTmtjSiy7VijLeZ0IqO08OzPIUiTI74Ga6bRP7O01F + h1KYJOv3V4BoA4zU1lExMrkbOAvpVcSifhjgrzmtjxPp7pO7SggOcqfUViy25hG5fSgC8rrLAojb + d7d6SexlEgwpRfTNV7e5LFBbKAwPNWHeX7TguxI7GmBPBExhaNVIJ6egqOVknO1fkx1J61aj1gLC + UEKlk4LVWvozC67kCFxkD1pAQ24e3uDLC3z9CR3H/wCqrczJdOGiOxvYc5/CocMYhtUBj3xU8Qjk + XbKPIZOjqclvzoAu2HiO60xPKvd7wY/1fGBWnJo8WuW6y6XIPMYZEAzuH9KxISonAuzuRzgk9qtR + 79KmMuhTt5cRyxznFADLzS2tMw6pAY5OoDEZ/Sm20TQQ74YwVQckGtMatB4kUpqreVIRw5+8aqXF + jc6bAsbD9yThWz94UAOmmjvrRCMJjOQRVS0sD9pLyABM5Of6Vdtrdn+RUGcZqO6uRBG0MuFI79KA + MfV7r7ZqDI7kohAVT6U2eJNimJQOuTnpSXFussrMvBz1pJov3YUsR9O9ABblRncQ3bAqY2EUwIiA + Vqr20ojfYqZx3q9bSKAGcYJPIoAoq7OCEQBffrRDGEcleM8nNPjuGkhHmbB74ApvmxltsuTnuDQA + +SFEjDwu5buD0qpLL5vMg2kEdOlXECMAyZGOMMePyprQRI5N0rt3BXO326UAV4b0Wt0pC5HrXS2W + qq9zE7jcO+OhFc81kbg7iMqeAFHSpLa8eymaNOUIwD6UAavjPQYYybq1bBmXcF9O39Kw4iXdDKcE + DAxW3q7NdWELISdiYIz71kz6ZNZNHI0cjqQfujIFAEtzAtu/7vODzmqlyzNyAo9vWp7uWSWJd+AM + jjGGqOWCSWRVVW2+uKAKskpWU5TP0p8c+ExsPPNTmCVD+5U/QrzRJHJGymeOQc45HFAFczh497KR + jirWlEsAudvII9znitEeBp7yAPZvEVPJUsP5ZqCO3j0yYDUNwliI6dOPpQBt/wDCR3Wj6eHFujvI + do3DIX9KoHXoL6J11CJYZAONlaWueIYtY8Nwx6ZHu2MdxVeTXKG0eaXKRuCeuBQB0mn+HRe2Yeze + MqRkFmwfyra0rwsIrRmvZICcgDLVw7xXFuFd2uEQfeAJAxUkkjSxh4J7gjPAErf40Abvjq1i0y4S + KByCdrfL+FUI7SR4Wc+WzMOCW5qhf3Mt9cCV2ZiihRk5qpdTSBgRI+R2DnFAFw2k6AqJZMjuD1qn + cxzyyAkPuiP3ieT/AJzV+01R7a2RpMZPVmGQ1WVuTqLDCptcfMBwRQBEkst/YMCSTH8vJqtJaoYQ + JPv1o+ZDZKAo+UnBpmrCBpRNp4/0crgZ9f8A9dAzCdGgkOynxSus2xjkj+L1qW5/fxYj+8D+NRWz + R4fzCd2O9Ai0lzI6mPaMOcZqW4uI7rbtJ3IMc1XScKqncQT0olPlKWfBz6UATKjSDcmdoFWtPCyR + kzckHiqUV0623lKVIPzHHWp7Ic/vSRz0zQBcCqdyT4J7YqC3uZdKv1a2UupO7B6H2NMglMUsmcnd + 0Lc4q3BmaMBiDjr60AWJRBfyb9P2RueWJ6KfQVLHqMdtcEysxJXayN0x0yKyWihWQBdwTOSdxHNb + zWEF5ErXhX7QQAMNge2f0oAnhs4rq2kksHwirkg9SfauXnJnmL3AbL9jXSRWh0N28x1cEfMqtnA/ + Cs+70+O9/fWRIb+76fhSTuBimbyyyKDgnipLk7AML1pZbCWO7Hnjn26U6ZykRL+veqAryuvm/Jwf + Sk3mo2AyHyCT6Ux5pLU5Gwg88gGkBPNAILUO3KmooyjL8ueegzTvPMsRjG4qBwKrW1sxJZzsIPGa + AJbmfp5q7MZx71NZawEi8qZSyHg4NRGLzCPtB3eme1R3Nutocodyd8UAaVtqEUDlI8/N3PaqV2Ht + X2x4lIOSwHFSWkEFyo+cD1BpbmNbNdkh20AMh1UiJ1c9RzWj/wAJa1vYiK1RmRvvetY5gDENxgnp + UlhN5TiI4O4845oAmu51lXzFDGQ8jnpTra4uJkBAOQavXvhG8tIhPawvJAfmY9gKE1COwgIiAZiO + 3rQBV866T52Qsw6YrXguZNTs0WSJ8IPnHr9KwZNamNumZSpPU4pbPxBeRy/uJjtXqfWgDodMtnXK + QjYeo3VnalpiXjMzXMKS9O9VV1ydCXkmLY/SorWwTVJTmQEt81AHTeCY49Mik+0SJKmOg71W1bxH + HLdgaXaSRNnjdzWapGlBBG2ec4GKtQ6yZD5hjLMvbIzQBfutWC2ajV4ywwN2OM/Sql/JY2kKGzU/ + McnBBqlf3Lam5e8lKMv3Yz2FU4VjgzsGQ3WgDa0ya0u7kxzgqCCcn1q43hizkEjRkOoXcAOua5Ka + 6Mc3ygEVb0nW57ac/ZC4Xuo5zQBBeZjcwuMxRn5fUUmnySx6kv2cgg98deK1LjT31pTLpymSVuWi + Xqv17U2GzFgFBUCVOo7igCTT7cnTp/ty5ZnyCvGOKz2uwimOY7geQB0FWY7tzu8xiqk8A96qOvmy + MSowOc0AVpkkgk3uAiP39KkjtonYtnO4cKOP1q1Z3K+X5V2N6OeM8gfWiewaxiKhDsAyJB2oAk0u + 1juAwniYshwoB61FLZfaJDv/AHWexpulXRNwpjkP7s8nu1Wd4uC7zfezxQBTjxZTHzlMigbdy8Up + YXEv7nPvk1aNqbhDhgARnFZMCvbzuWZgc/nQBo2l6qs63AJA6VIsiG4DI4jXP8XeqcbrK5JH3xkH + 0pWhWVR52CF6UAa8kUd7H8rD5f1p5txHAfNPasWRCjgh8D0BrV0a+DgCdfM3DaB9RigCml/JFPyB + 159xV+C/wfNHAbtUN9orxO3k5dhycfw1XmT7JarIjb1k6U2BcuNSVGDSAPu6be1QTXcO0CVSwbPA + 7VRtpftEmxW2Mx6HvUv2V1J2jkdaQBFJB5jBVYemetRyW6SqTKCfTFNllCHBX5vWkLBPvk4NADTG + 0ePKB5qdLN5NjycqvNQIpZAFVj71LsaJQBuGaAH3aCVwycKODUMsZgJjxv8AXIzUs0DpHhmBycjm + gOd37wdRjNAFETeTcARAbSeTViApfrhjufHXNJNCsUu18Z61Xit3Q5JxQBdW0MYKyn5hSf2BPIjS + 24I29T6f5xUMMrs5HOF71ooVmtMyu3ynAAzQBqeCfG7aaPsmuYkiYFG3HseKq67YQW2rSNpLCS0l + GQ5GSh74xWZc2SyxK4OZl5x7d/0rV0K+j+xPFOu4Pwpx0oAo3OnFreM7AR9Kp/2eYpxtyCx6VoXd + g2nSlQzMh6UxJdjqSpKgfN6mgCOLSZGkKyYw/wCn+c1YltRodoWA+Y8Z+taPhWz866DQqxLdmq34 + x0ZbS23yY3NgkUAcZcSyrjcc7zw3YU62meOeTazdOhrZ07TYLkYvSFVfmqveQWkDj7CW9zg0AZs9 + 8wbO3L8ZpvmGRsyZQDsO9WLu0EwZojwMc1DJCrsA5we1AFmGVZLc7Y1bA6nvU1gIyNzgxtnoKr7I + NgHO8dx0pJ3AYG3UnHegDRS+NpL5lsxh3dQverj38OtL/pKCKSPhWU/f+tYEt98xMnC9qgludrrJ + GzFl7DvQBq6pYNGdzHGO3aqS33kEBhlSME0+01z7OcXGXRupJ5H0q5fafFqNuJLLnofmGDRsBmJe + DzMEZGevpW7o8sN/bzLqTBML8oB71k/2YYh83FQRqbdtr7sDv60AX7jSo4ZsiVo067hj9anuNHey + jVizMj8gkdaqQyi+UxjO7O0A96tXDz6rEFucp5HygUANGEQKjDJGaqzWbzgyn5QOPY1p2xZtOaGN + VMo5BPoKqxa1NHHtmij+Q4xkUAUraZFiYScMOgNMf76CIZHf2q5KRq8arEjK4OTsGaki0oKwAEhP + uDmgCohEsqq/O6rrMNMj3AEdgfQmn3tqUgEcaYz1JFMtLdn0wpFGxYHhjQBa026M0XM2WQ/NnHzU + 6Yw6tCPt6rbpH0CdvzrPtrZ45ceU4cHk9qtzW6XLOjqwY9+1AEa+HWun8zR28xU5LAZx+VLaGSV9 + jrkr145amvEY4hGkjKMg5XoPY/571vaHFDr95HHqDMkoU4C9G+uKAOevoo5iSBjBxVYwLdRkL1Xt + XSeK/CdzpkjRMqyJ95SjbsD3rmJbUwoeuGOCfSgC9eWc9rcbbdA0KHPmhcq39Ka8e9DkBS5zk1X0 + /wAR3dvEtuTm3AwVzW/D4w0xIEivbOaSTAVWBAH40AYMu6CZDkFcHcTz6UrtkYlwVHIwOtb91olr + qtuRZSL5h5EX8VY97pc1jKAqZ2jB/wA/nQBRJhubjE4YOOnNMC+S+DzmrMkIA819wPTbjmqwfzcM + 4w3vQA9mbYwgIz/ENvSm2t+6jZsYKeTkVYjn/eqwGAOp9aeW+2sdkgVf5UAQLKY5MHGferNv+6IM + XT07CmyaeZIS1vtmkUdQKbZ+akOZoyqMe45oAvRzjUJPLLgSds8/zqyPDzwETagy4U8YwARWMbcw + NuDDePenPrbXEfkTn5hwrdqAO709LPSbbzlZdvqD0Ncnr/iufX793uWQrGdmFGBjpmstdQeFRHKx + 2Nn5f73+f61E7iLCxDnrjvQBaubtNypAxyRzg0q263DMsJIzzyc1mwyDeSD82e9XIGUIrSyBNw+X + 2+tAD3tSpcFvufrVZbdL2XbnDdjnGKnhs2nkYtcIEJ6461HMiJIApBVe5HWgB8mmtpzDzSrrkZYU + 65mRGYoBgirEkCStiJlC7c5IqjLNsYhtu0d6AKkshbAZcAdc81Gdwb5SD6cVZjYy5WXBVu/pWppn + h63urfdLdxR47MDk0AYjnhehxntVq11OVANuTj8q2/8AhBZ7mwkm00CYKQBtHXrWe+kTWS7J4zE+ + OQ1ACQX/ANrkC3DD0wODV280KQwM0jxheueKdZWcCrvkjYYHUHvRe6jFLapHtLKeDjg0AVrDQ5xd + xuhIUEMHx8pH1roZtH+2W+dPIbHDMOcms+81YNoqWltlFKhQD1HNP0e5udHsHFkcyMRkDoaALUPh + aa1n8yUgqRgjPOO/eq+reDkvHzoQYIB85JzzW5HBLqWmCSWQJM3UEdB3/Sk0S3uNPmIkBlgJyXAw + o/Ci4EHh3QYfDsfm3mHklGGLdFqS91HSYpvMw0jjkhTx/KqXjLUg8hihYiMn746H6Vg+QYxuV9vH + 1oA3xrem38TNe28rqp+VUyD+gpbTU7O6ylvEYoEBPzjDAjp2HeuUk1aeyfNqMH+8BTrvVhqEAMuP + O7n1oA3X1Q3U0klp5S7OGHFZt7rj4DwxlTJ6riqMTiDZsHTn6/WpbfU5EP8AxMVMqdFIOMfWgCZb + lpEO/GDgn9K6bwZpktjcC7lUsAMYPvj/AArBi0lrpc2sqbZsHbjkV20SvDp8UUZBcDp60AY+ueIZ + dIu3Frh0lbD+YNxAPXBPSqLrpuunyNPBSSM7mZyQpJ/KtWQ2uqvNDcjypQjAFjnJx0rhNYhntbvy + 7jcucgIe9AEUMOy5ImYgg4xViVVa4UFSoToc9a6DxZoEdqv2rTsHzDlx/dFcujFpG27vlPGe9AEi + anPpV359o7b143jqo/yP0rWs/FSavF9l1JltlB3tOerd+axl3XGfMXC9896iu7UbtyYIxg0AdTc2 + Vrqe3+zZxIF4Uj+I1S1Hwpexu0kts8aL7Vg2t9JZ8REjJ+UD+Guh0TxjeaW3/EwAuFAxh260AY8y + ujfLkBOCOuabHcqgCxYAbrz0rsbSysfHdzks1rO33Y0AwTWd4h+D2r6M5mmt0ER5D85P1oAxLfWZ + LSYrbnAb5eKnudVnyELFkHOcCqUmjzRzBWyD9K6W38JtLo6TtkLzmgDHtryGZiZUDZqDU1Vl3wp8 + g+9jsf8AOKmGnw2cpE8jFR1I7VdGjRXMQa0kdoSPmHrQBn6bYnWz5NydjgZVgORWeztBK8ZBJQld + x6nFdZ4ZtoNI1QPI7O+OB7VX8faO9rdC7ESrC4BJHqaAOcgUTtuORiraW0M9yiXLAIeoPc+1RWar + u6Haxq7e6ekEZkBGzGVz1ptgVprUw3ku3iJDgDPUYFEzAwZRN2CDgUw3JEkezD7+xolvytwn2pVV + RkADv060gLVlMk4aLIDHp7+1Vbu1+yzgThiHOOelElyIZl8v5CDkVtxWkGtaYs0bMblCcr/KgDCe + 3LzsN20L2HepUQJnHI9KsX+gT29pHKCd79qWw0u4aPcwU4796AL+meIr2G1aDSbiWHOMhR1qxZXz + xXBl1n/iYBBlg/FR6VZW1nciS9mdJADgYGO1Q3pIOOu5hz60AO1vxLDqluP7Pt47eJSQ2KzvtiSg + eWuPpU89gsfzH5cc+1ZaSpbXRZT8tAGjjz237gNuPwrc0O48uUPOM4GBXORXC3HmJD1bB/QVZivZ + fLwp+71oA6fVfEiwXC+UBGjfKTj14qZbi7gtJWjkY2zx5C9s4rnbCRdZiaOUkFQTke3P9KbYa1c6 + XcBARLEWxhzwBU2AotqzH5Ls5YdFPOKmiu1KgxfvCOqHrXTL4EXxLbl9MO6bGRkYzXPal4TuNLu2 + ju/3csfUD9KoDO19yChhO3OcqO1VoZEUbHVckZL9x3q09s8a5uDkZxUDWX2i4OzgHvQBLCwkwyEF + c4z6VNDZm7utkROCfwqCzAhuGRhhV/WtR5okjjkQ7ST2oAlSRtMdUjHzR1p2OuOI2Ly4kHQViS3K + iYBMsW5zSNF9klEjPnPSgC1dzm4uVKSMZd4JP41oeJPD8+r6ZHLbwmW5H3yCMqvr/Os6xu/tDfvU + CqSOfWuj0yf7OxLO2CAG9x6UAZs6vcIqSiVw3GQMisR7RVvpFkGFU46e1dN4c1hYmCXm0quDIO9c + 54quVl16drdDHGzZX6UAV5bTzWIi4Ws6/DQEoQSpI5q9BfywxkS7WU9OOlMa3F8hG7bj5sn86AKc + ErggKVA96lFwLcYHX3NQPAHnYD5e26pAnluA/JoAu6JevFqsEqs4YN0HQV39p8aL+CJVnWKWOP5c + OAf6VwCzrbxAIMMefpT48zEFD9RQB6hZ+PNE8YqsfiJFt5GOC0abcH6ioPF+i2/hiGK50xmuLOQ4 + AjO9s/T8a8wlzLIdxKkHIwcc1s6R43vdJi2xurxsdriQbto9RnpQBal1C1urtzcIVjfqu3FRMNM8 + zbpplViehyAKnuU0/X4N+ixtFdR/67e2fN+g4xzWPcWzWFyDL8gP3Qw+9+NAGhqulSWzpJHt/wBn + Bzj2NejeHLG28f8Ahox6/HsmA2DHBGO9eTrrksUTKSOD0Par+n/EnVdMRVsZYgpHIK9u9KwEvjn4 + eTeF9UY2Jie3HI+bJFc6b6eMkt909j2rsrTxpYa7bGHWYpXlc8Ord/yrOu/B8gEjQul3Ao6RjLL9 + cGhaAcu0skr7mK8HtTjEAcMMk881Zm0l7JXxg7uQBywqqzysygDBPr1qgHSWqzANL6UunXjWBOxW + KsaZcggbu4HSlindrf5ANxNIDqblPteiWrESNC2fujJ7Vd0bRY7KLfZswWYZYSdT2/pWJ4Q8ST21 + 1b2krIYj8pBFdd4k024ht0nsdpjA4AHNAHO6npkSs2SwPase6ieJcSYdenB+atGbWykgF9G2cHvi + qGqMxiWW0GFyCSRnFAFeSN4yGiLE9we1QXYEhzMo+bnAqaC9YzbpSGY8CoL/ACwDQ80AV1mxdJwQ + q9h1qd71WHU/QdqgDO0gJAyevFE4WI8dW60AafhzUHt5v3ZAzxVzXNFku/38Odg9KwbK4ELA4z+N + ddourgQKJsMv92gCr4Y8Qy6VGUmkdLcDjn5/8a6vS5tM8SWTG3kkaZeP3xIyfxrmPEuk/ZXF9akG + CY/LHj7tZy38tvcxSwnYw7DpQB0viLwrIigwhcHqAeKxDpbmcgJtKjOfStXRPHgjlEeuAzZ6bf4e + lajX+navE4gZIyQcFmxQBxd5ZPG+9iuDxmqitHGR5oO09M+tdDqmjNsDl90YPBHSsJ4N7uH7dOOt + MByxj+EkE/d5qwYGkUNu+VetUgxVz6gVNAryx7Y84J5PpSAeZWjG8A/Lg1sabqn2hF8wnniqPkK6 + qk/z/TilaEWo/cgqKANPSbRba8zM6MXGDzVPxHYPPOzOOVPy471R03XmSRXlQEHv6VstqaakgJKh + h0X1oA5jBjYrP8uTkA9TQ0qoxLHqPyrQ1+z6TMu104x65/8A1ViSsVc5GdwoAseWbkDyQWC01QVv + S+5WGcbe9OguTFZqIjhxnPHWnWTCO6LyKjPnpQBDfs4n3sMc8Y7VPBKWT922498U7X0RCjRnJmAL + KP4aq2rtA/ycBu5HXFAGkYg0GT8rY5J5qIw5jyMORxU28zwAou5jxj1pnktAzCUlT1xQBHFP/Z8w + dpNsg6ccj8a6jQPFNjqdqbfxJbvPM/yxTE/LF9c1zsNsJ1U3EYIP8VPe1iicCORsnnHTBoAtat4Z + mS92Wn79WBK7aw0ia3uXW4jdChxkjvW/Z+KLjTZFd4hKwyAc44qy+nwazpxEOPNdvMdx1UdTQBzb + AbSNyqGPf+lWvDPiW58IXDtZzOIpRiVVON4qS/0ePcG04/aYV4Z8YwaoPGJrgq2AqnAPY0AdVdww + eJLX7XoxSKfbnyRwzn61zGooyMzsreYpwQTyn+P/ANap9NvX0S4DQtzu7dhW/rel2viWzWfRiPtC + L88a/wAfuaAOQEvyDepIOOamtbFJZWKzrH7Gpk02QRBLgYYHkDtSTaf5LBgM7u1AEVxbS2aiSNfm + xw3St7RfiTLFZi2vUe4VRt44xWJDczTzoLoFgvO096bMomlkaJfI5ztFAG7Jqdlrcm2WNYHA+82C + KidbiCAoVLWzfKoHOawo1dyGO4bQcc9frWppOvSwQLDcDzQSOvbmgCjcWBQsqDYwOTmo44BdAZfG + OeuK1NYdZLjzCdu8dAKzpLYQt+6OKAK88ciXREQ3AY5/Ckmt3dlMoznPSrMU2zJxgD2zSSRmX5kY + gdiO9AFWO3KSDgqMjrXQ6fYuUAjG3HO7rWRawNeSDLYKnHPeunVG0bR4ruTnc20g96AHxn7ZbNA7 + qzgcVzup2s2mzOl0CAT8jYzvrb1TxpZ3tgr6fBFFL/EUqpp+pJqpxeqJAPulucfSgDDfcjgxAqSP + mB60xXXlZFBPXpV2+tms5W2oTnpk1nht0uZCAfTFAG9oOvCJBb6jueJj8qj+Grer6XFCqvHMvHTA + zmuajlMUmWHznoKvQ6tLDEPtKeZnsT0oAkaBVLGX7x54qOG6NvkEEA/rV2dYLi08y3fMhH3e4rMR + mkDLOMkHg9KALcN7vXI4Iq9ZyG5jw7An1rFuWMWMAopxTzqMkIxZAuOpINAD7ZAcg9F6VqaXdRFg + pX5h92sPzRbfKQdvr61c0+4MjDyxsYHkkUAdA2lvdQ+ZcDIPGOuawNY0wWNywjwVbocdK2E1ubTF + +T5gw5yM1Lc2kOqaX5kXMxG4nPT8KAOSUSKu5VGM03aZmRo22k9Tird26Fgp+6hwcVAZfNmCnBVu + mKAJp7N71FDcuOI8d6pJlLlt+d44PoK0dTZLKCI2HmCZQCd33c+1R6iqXKpJBu34+bPQGmBNpzND + bgH7zHjPapLiXMhEvzMRwarQXG+ILcfMP7w7VZjdHj+QgMOmaQCRF7AsVBZO2am2G5t2kIAJ9O1V + 2vzM21l+UU9Cjj5M8eh4NAAIXjUeRl8/pUa6k1hGFtWyG6n+lWYX25Y8dsUs9t5tkVkK7Tz7+tAE + 9l4hAj8q/RUf+Db0P1qZ/DUWrTO0paK9cfLGg+Qn61zc0SeYc53DgVr+HNfk0u623LgwSDaxHLY9 + QaYFa80a60G58vU1VmbqF5AFWdC1k6PqaTW6qyEbSD+FdRJd2s8IikZJbO46MTmRB7nr2/WsrxD4 + QjtohLo+9kHXPb0pAd6uh6Lrekm6hkkQSRgNtQfK/p+dc1f/AAsuGUnSWSVScgynbisHQfGFxpki + RKw8tRyD0z/nNWPFHji/1lFihkCxKMAocUAaNt8NNSt3bzYrYsnT5xTLvwZYQTIuqzlLh/vqigqP + xrk/7QuIwRHcXG4jnMpP9ary3kzhvtUkrSH7p3E0AdXqPgvT1vI47K4kfcCcYAx0/wAar2ngu2uW + ZIJX3pnjHFc3DqUikfPIGHU5PFb2ka3PDe7dPZGGzGW7/wCc0AX7LRLSzcxb3eXrhhxVG78JeVcA + bvvcVfEgudqaoyrOrbiV9Pwpmo311pMnmWmySH3w1AGRrXh6TRfLMq8yfcHGPxqxZ6fpmnmNddml + jlk5+RQRx/8ArqO51ptT3vMwWU9iOF/CsOZHnkIkYu3YnmgDo7qPTtPszcWTu5LcAr1ycVl6p4hk + 1BRbsCEXkCqEGqz20wEWGEZGAeRxVy+vRqV2JpUVJiACQMAUAZ0+mvaNuuz88hwAOmaktbt7C4Ub + c8jvW5rGkp/YUEsRM0nLSf7PFYogSWEF/lJ6CgDWcjXyuMhwOAO9Y09hLbSyKy9+pqzpM9xo90Jr + co2OMMM5ropr2PxBYGK7VVXBbIXG4jnrQByUI8xSADs6HPWpPLIjGxssvr3pxQmcqx+VGwFHenJI + gOF5oAW0jZB5nQnnH6Usnzjrg0rW2/8AeISD1x2pWR5VySNo60AQBX2EzHIXpSQJ5kjOOFpLgrtI + iLFvWi2Y3CFYuoNAEt4myTBBQ46Gq6OyHKjGTzSyyyXUm+/cnHc0+PY42RtuDcDigDS03UzdQlHG + WHFSw3/2CX99lo+hA64NUorOeyG9FJA68VJFaLqNu0hkIlXkgelAF3VtEjvNMF1pKOctyPTFc/bw + tGVeMfMRzW54f119M8yJ2IjlGzk9B/k1p6f4fsmi2xXsUmeP88U7gYV5Et3aQlWCsox+NR2eUnWG + 7bdvrZ1TRY7FXjuQsatzHJ7VkyeXbxnz38xl6NmkBFfiXR3MDKQjHI9xUMV0ijMnNdBZWbeJbUcC + SZU+U454rFu/DF7byNJcW0qxqeeOtAE0EcbI+4nax49qnKNY7CCG46Vjw3DRHO1gtaNrqPnBRKu1 + R0Y80AXYDHPAzlPmzzTWG2Evn8KafMMWIsFfamKxcAyjAHbNAFSeRJpOBg0xrXykVjyp6VLqFv5b + AqwTI6dal02ZZ5VjuMNGentQBJZxXFtFuUZDcitDSPFrwOYrkFkfj6Vl30l7p87RpKRDn92eoIqG + 31gRxk3qMzqRnmgC/wCJtIa2uzLYfMjgEj2rNs70woyIMjPLHtW7Y3y38gkUnGBke1R6p4dS/mNx + obeZgfvIVH3Pf3oAz7W3EmGzgrSSRqszF13+4/hqOOLdGSrk5HO0d6WCUxYaUMYhw4HegCM6TLcy + Ztkd0wckd6jtZZbPiI+aqnlem2tTStXNvcbYZyiSA4QcdMf41Y8Taf8A2dZieGMR7sAkc7s8H+dA + GVJqTT3AKtjIxtrStNVy/kyLuUj1rAlhG4NtKqOc/wB+l+2SpP8AcKMn3s07gdJdeHPtLRS2zpCr + csD171laro72bGSFWZRwzHpQdUe8hTDEMg5xU0N7Pcx7GVpIf4lzSAwlk2yAoevUDpWpa2hvYeTg + 0mo2UM8w8lPs4HUDvRpsFz9oYW6NKB07U0BbjvptGhkgJDRMu01VLRyyIYQSgA3HstVdVMiSlZyx + bPKiksbyS1hdWUmKQ5K0gJpt8UgAw69iKn0/UyJdrdOmKIPIvW/cyLEqj7p4zUEUIEr+blHXJBx1 + oAk1O28q6VoSFVhk1GbZQ25TzUlvcfakIucKAcAnqaWK1cyFkQlB70AJvJdNq5I4+tBcbCnCjv71 + LIVcAowVhxj0qO2t9zkXHKt0bsKAIpbPIHlKWUjk06wgaNiqIBzViF/kKKwBHA9aguI5oX3REk9j + TQErWypGPOGc/pTLTy47gMFyob5fetB7EmcG3G6N8hSTjNWRpgsws/y7ouWB70gKd5dGSRcfKnIP + HFXrHSYL61e4kfyVVcYA61lC7OrxurAKxbIHtUtxfC2sTDA/A49KAEazRmkEw+TqG9as+H7YSTeX + bvu7ccYrIt7qRdobPLc59K6jw9pf2KUXcJBVjuI/z9aALF88MsJh1AiRoPl54Iqt5GmXUG3ABx1x + 0/WneMbGfTryO8VB5d2N6qfTJHP5VBoNtFqUb/b28uU/d2d6AJLPV4dGtP8AQyokHGKgu/Fwu9wl + PXgj0pmpaSmnOxmYEdu5rOht2knZ4FX3oAimiju3AtlAznrVWSAW7OC2HQ/d7VdNjLaMjurbSeMC + s+4WS41BjyEB5zQBcgnk2ARnJbqKZcydmZt3fFVxB+9DRkjHfNWLh/KKGTp/6FQBGLg3C5PzFeBT + LeT5yEzlB0p1zb7wGtzt9RTNhWVQOHPWgDc0iUajbPbTgM5GE9aydTtPKk8sKcDrk9adZX5+0FLc + FZM/K1dPpmgReJLR2nOyZDhQT1z60AYWgXYtrvy5cFXBXA9+OtGpLceH9YIsZ3BwGI4+YHsaNR09 + 9C1ERTFTMjBgE6YyO9S+IoDqHlag5++RGPfGKALelpb+IbtA+Ldk+ZkXofxqHxFpn2Vpv7OXdGOW + 56Vk3GpCBQB8pB429a0bHXN8kX2gKY1ILju1AGakfmFfJXLN0/z+VdZYQG503yda5xyPp/8AqqXw + 2LKJJvsqbjIdwDL936Viarq8u9nhA8sNg88/TFAGrdeFbeWBHscSL/AM9DWRqnhObyS7KUYdfetH + wkx1Gdnm3rECAB6Vu674psYbIRxeZuHBJHWgDzZw2nybQMluDVnT9T2PsJK56Ve1OS1vJ/OhOfXj + pWVdWctu/mJhgTxQBeYrOS0xAxTojJHKHspCQ3GPSqaXCTuqpnf+lTQIJ5XRXwy0AaN7YxzWzT3I + /fSHp6VnS2LI8Yt13kj5ucAU17me4hYbvkHXJ5qvJfDMYDNlevqeaAJTAVJGBuHPFSWuoMN32iNW + UgjOelVo5vNUvg8HGKVollOIG4HNAGhb6dHewhrVy8gPK4qaFTZZRssT1GKzLWd7C5zDlS1a9rq5 + vU2uFAIznuaAK93po2GSIEjqefu1C8QZApc+uBxWnbQpeyCG1OB1cnjmi5sUuTlxgpTQFBAYCWEQ + bjrmmsHvDypH0qYqYGPlk56DPSnWFuz3BN2MCkB0niGK10bw/ExCyMxwhVskH8K5O98SPfWixqPm + AxkjBNEkkz2iQSzgqn3U54rPm4RkY4YEfhQBd0gPBMGnwc8fSpvElpFBIGU5Y4Ix0qjcanIkKBG5 + 7VGzPdIHvF3P9aAHpGtymc4Ira0fU5YYUG7KA5P0rAEgjOFjfHtVqzndD8ilFkGKAPTri4h1fRrW + DVAojmjwjdwPY/XNcJK6aTfubdjhDgc9a19PnbUYLW2upsRJ8o61S8WeH1sryKJ2AeRSUb1oApTX + TXpaQMWJGcdal8PSf6UTcj5WOKz5YW0zgTKZG44Bq4THLpSqj7LhWJdsdfSgDo9e16OGFba0ji3p + wZCBzXOoYZp2N2u0Mecd6Zp12cIbkfIBzTbwRG53W4wp5oAbeWVmgY2ZYeuTVC4SWFAzjdGO5qws + HmK28jaTVi1vhaR+XfRGeJhtVR69jz6dfwpgZEcrPcAp92pl2IzMxLuRwamfSJZCXtnRhnLgcFR6 + VWc7J9mNpbtikAW9w0MheQj5ea3NG1Y2sPmWhCvjuf5Vk7UadY48RseW960rDS11C3b7EMzL3oAt + 6hpn9pZu4GzGq7djH5g2PzpPDsMV/Y3Fveg/uVZl+vNJYRy2KhXfcB972q5aRw310/2eZLbcuCWH + X8qaA4yTeT845B4qaEqjZlVtzflV+80qY31z/Z8T3ENqMs8ZAAGcd6zoZMncEwH6H0pAdDpusLZQ + 7Rjc3ApkFoZJHmY4iAPXpms8R7oh/Gc5HtXQaALbUtGMN6ApPHrzQA/TvEdsdOWD92rRk8gcmud8 + QXkl1cZzlfapr3QP7NujGjfKTlSKzr2Jmdgx/wBX096AIkn8ucBQQjdat/bWMLZKOOnOOKzdjL0P + BoiXe2Cu7vQBpxC0KAyK2488Hiql3LskbaDtbpjrV+3tlubYC2TExGBVe+tJNOAF4PmHNAFO0meG + R1bI9jU0iK23zcbsdagWYO+xOH7mrkMWYcNgkUAQwKGA4JC5pzyFmPlEADt61asYIgSJWA3dOKv6 + zosFpdxPaBGVlG445BwKAMwuWADAbqs6eI/3hl++Pu1cj8NFyrRncAdxb0psElpY37NMhljD4YKe + poAsWmm/aIjKknlsvUnoalhtHLcbiueucA1Uu9UMs8wt4SsOfkUnkCrOmXcotj9rkV0HSLnmgDoD + 4JSXSzPNNFJhdwCkZX9a5+K9gD+XPgDdjNTpez6ZZywwPskcZbk/KK5qZ2llPmvvYnrQATr8zE5D + N1zxRbou7951anhZNYuUVFw7dvSp59IltXdZ1IZKAGvpLNGfLAfufaqDCSKUEkgdMkVd07VWs7oG + XLL0x60+7ePUjyCpByMUAV3bBGxsk1ZikV4gAMkHOKpzW5SUmN849qjjnlil3KODxj0oA6KykW7t + yJW8pk4BFdxrGhwax4TS5JWWaEBEY9QDn/CvNrPUfJmBcZDHLV0s2vsfDMwt2ZYy4z7cGgDHv9NK + yjfD+8bgYFUNRtTps4S6HlkjIBPU/wCcVeN86xKZmJlyMc5p/ifU5L/RYVmto9wJUyZ5oAy01Dfb + qZV2xnoKbfX6NEv2ZcHHWmPLFJYQx2ZLTL1U1EIJA+2bAJ6Y5oAIboyDb0PU1c8xLkBJLna4Hy44 + 5x06VAbZbdcyZ3elNBXeCRjnOaAG2808N5syYmJ7fx+5q7tW5QCZQso/iqsULT7rXLr6k4xVi0dX + +9kmgBlxpbI7SxqZAoGWz0p+i3txZ3AezJAHXjrWlZ26mFyzEnPC+vStzTLO3vZ1M8Yjwp6Hr0oA + 5/xFqyrIggQKrLlsdc96xpQZ5wySbu2DVnVYQ9/MJCSitxVOQFW4G1aAOm+H3iGPSbie1upBDBqC + CKRugwOfwrI8VWsenazNHZtvs0fEb/3h6j171Elg02N65x6Gt200i18VwwwXcjQ3Fou2NQMiTvye + 3WgDn4riKEhkfKf3h6+9aFlGLeyS8eT5DIMoDnv3FXZ9I0iwhJFxJLMpwY2ACg1TvvISzMs77S5w + EUcUAW9dH9qW6y6ZKBgcgdawoNOu7iWMmNiWOMDtT4Jxb5e1bKuMEHsfWpNM1ZrG4WWFmct0BHSg + CprWivp0u193mMeR6VHa2jmQbVH0zV3WNRkv5mkn5YnjFRJGBMjRMScdKANvR7OO1u4pS+SGGV68 + d61/GnhSHUYReQyqsZXiPI64rK0S5hRNzfePXvWr5w1KIwwucAccUAefW1q8kqiT+WK0RpdzFFuE + bFT0bHBqxrFj/Z87LjDZ/Km2ctw7Kgk3KO3SgDPQPuHmqNynv2rRs7hrhjDIcDqD6VPeafDfWbbC + UnUjav8AeHfn8qsaL4bl2pLcYWJT85PYdzQBq6dfjRtKX7QnmC4JQH07f1rIl0SztbsSrcoQnJQH + qaseJ7mBVT7PIXtDwrYwQ3esOO4RrxvLZmjI+90P5UAXrm881T9lHOeAOareXPH+8BKOB19Kb9rF + pcq0ILDPc8mp7m+S6k3fdKj7vWgB8Gtj7Oq3AZ3fCs7DmorqxQTbl+oAqJJlu4gJMKwIxT3kNq+H + G5/7o7D1zTA7Pwpd6NBrk5vQwMv3Pl+7UnjAwwXX7tFe3l5UjBbHvXP3GnCOxhuo2IL1G+qPcFYX + cknoT/n2pbgVZtGFxZvNbH5VOBk+vt+FZ8lrPakrcqyHGcEYzWidWS3lCxAlVPUdDWxf6pa6nLH/ + AGlH99QoI4wTwKbA45pHEirjk1asbxYZCsoDYH1rV17wyumSKVbeGG4Y6gVk/wBn7UdgCpPc0gLw + aEwtLKMDtWhoNykVwHdd8JGCjDIrDkSW1g2zOhVhkVLo+puSVlKlccYoA6Dxf4PbSLRb21wto7DG + W7ntj61mpKdXtxaOQvlfMCSBuJrqLfWIfEvhg2muKzQoN4CnBJHT9cVyU5hEjNbB0CHABPNAGTPa + fZriQONjqcZ6flUtqqB1SRmMr/dJzWlDaLrEUh1Qbnx+628ZNZE1s9nfctxEccjpQBO9tLcy7Zjw + vfNQ31q9oee3A75qe2Yyzby5OKiutRMsjKQDg4FG4EVvEyfM5xnsD1q5bbzKHBAB9KrCJN4YMd3p + V+wt8szRZUCnYDXsWSGPz7jGI+SMVVuvErXKEWuRk9QMYqXVyLXTUyRmRcmsSC4EAO8D2pAXxbma + IMR8w7+tVdRtkUAT9ew71as7wsF2nFGsKodDOMzHo/YU0rgULe7j098qW545Gaki1FIbwzeYyzfw + EdvyqkyGSfaw+bvRcQLayqyEnAyaQHR6gi6/pXnBER0IGFHzN15rnmlXyTGRuQHByeQau2GrS20G + 9OhO3H1//VWhf6RprXbXmnrMtuYsOjNk78DkfiDQBi2rpHIVQjb1otHPnBZAMAdRVUQiW6Bgyis2 + Buq29q2nXJjn/eDsycUAOLCG8yg9zkcVCzeVIZY+cenekN0LqYRSHAHA9aLMCOTy5BlTyPegCxa6 + ltkL2+ORzxjFWbTXpLSV3Y84+XFVJvLilKjgVFMpAyBxQBq6prEF7bQSzA+ZJ97jpVRGjDbUJAB+ + U+tUywlJUdE6VteHLK3kuoDqQZ0zyAcYFAG3feVo+io90u2d13R/LyR35rm77VZNSmzC5SEj5hnH + 14/Otu+hv/FN3gTWywW4KRqQM4/OsUeFZp5miaVAc9R0oAaXWa0EUWCIjuA9PeqEMbCYM3G77oAr + bi8Gz2YDmeLc3ygev61X1CxnnuTE8TvPb9fKXigDMuIJFlBdtzHnAPSrEF0IwDCm5hw2VNRzxTWt + 0BeKVMnTIxj8KZ/ahtgY49uT7UAX7VH1K63oERVOTxiuu0ex0nS7L7chJkm+R1kwwyPQZrh4JJDw + zbVbk4/OrNpefLsnyyg5UUAf/9k= + +END:VCARD diff --git a/htdocs/includes/sabre/sabre/vobject/tests/bootstrap.php b/htdocs/includes/sabre/sabre/vobject/tests/bootstrap.php new file mode 100644 index 00000000000..14281e2182e --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/bootstrap.php @@ -0,0 +1,25 @@ +addPsr4('Sabre\\VObject\\', __DIR__ . '/VObject'); + +if (!defined('SABRE_TEMPDIR')) { + define('SABRE_TEMPDIR', __DIR__ . '/temp/'); +} + +if (!file_exists(SABRE_TEMPDIR)) { + mkdir(SABRE_TEMPDIR); +} diff --git a/htdocs/includes/sabre/sabre/vobject/tests/phpunit.xml b/htdocs/includes/sabre/sabre/vobject/tests/phpunit.xml new file mode 100644 index 00000000000..46dad6a3d22 --- /dev/null +++ b/htdocs/includes/sabre/sabre/vobject/tests/phpunit.xml @@ -0,0 +1,23 @@ + + + VObject/ + + + + + ../lib/ + + ../lib/Sabre/VObject/includes.php + + + + diff --git a/htdocs/includes/sabre/sabre/xml/.gitignore b/htdocs/includes/sabre/sabre/xml/.gitignore new file mode 100644 index 00000000000..accb586c771 --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/.gitignore @@ -0,0 +1,9 @@ +vendor +composer.lock +tests/cov +.*.swp + +# Composer binaries +bin/phpunit +bin/php-cs-fixer +bin/sabre-cs-fixer diff --git a/htdocs/includes/sabre/sabre/xml/.travis.yml b/htdocs/includes/sabre/sabre/xml/.travis.yml new file mode 100644 index 00000000000..96396564e76 --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/.travis.yml @@ -0,0 +1,26 @@ +language: php +php: + - 5.5 + - 5.6 + - 7.0 + - 7.1 + +matrix: + fast_finish: true + +sudo: false + +cache: + directories: + - $HOME/.composer/cache + +before_install: + - phpenv config-rm xdebug.ini; true + +install: + - composer install + +script: + - ./bin/phpunit --configuration tests/phpunit.xml.dist + - ./bin/sabre-cs-fixer fix . --dry-run --diff + diff --git a/htdocs/includes/sabre/sabre/xml/CHANGELOG.md b/htdocs/includes/sabre/sabre/xml/CHANGELOG.md new file mode 100644 index 00000000000..39a39bffe9f --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/CHANGELOG.md @@ -0,0 +1,228 @@ +ChangeLog +========= + +1.5.0 (2016-10-09) +------------------ + +* Now requires PHP 5.5. +* Using `finally` to always roll back the context stack when serializing. +* #94: Fixed an infinite loop condition when reading some invalid XML + documents. + + +1.4.2 (2016-05-19) +------------------ + +* The `contextStack` in the Reader object is now correctly rolled back in + error conditions (@staabm). +* repeatingElements deserializer now still parses if a bare element name + without clark notation was given. +* `$elementMap` in the Reader now also supports bare element names. +* `Service::expect()` can now also work with bare element names. + + +1.4.1 (2016-03-12) +----------------- + +* Parsing clark-notation is now cached. This can speed up parsing large + documents with lots of repeating elements a fair bit. (@icewind1991). + + +1.4.0 (2016-02-14) +------------------ + +* Any array thrown into the serializer with numeric keys is now simply + traversed and each individual item is serialized. This fixes an issue + related to serializing value objects with array children. +* When serializing value objects, properties that have a null value or an + empty array are now skipped. We believe this to be the saner default, but + does constitute a BC break for those depending on this. +* Serializing array properties in value objects was broken. + + +1.3.0 (2015-12-29) +------------------ + +* The `Service` class adds a new `mapValueObject` method which provides basic + capabilities to map between ValueObjects and XML. +* #61: You can now specify serializers for specific classes, allowing you + separate the object you want to serialize from the serializer. This uses the + `$classMap` property which is defined on both the `Service` and `Writer`. +* It's now possible to pass an array of possible root elements to + `Sabre\Xml\Service::expect()`. +* Moved some parsing logic to `Reader::getDeserializerForElementName()`, + so people with more advanced use-cases can implement their own logic there. +* #63: When serializing elements using arrays, the `value` key in the array is + now optional. +* #62: Added a `keyValue` deserializer function. This can be used instead of + the `Element\KeyValue` class and is a lot more flexible. (@staabm) +* Also added an `enum` deserializer function to replace + `Element\Elements`. +* Using an empty string for a namespace prefix now has the same effect as + `null`. + + +1.2.0 (2015-08-30) +------------------ + +* #53: Added `parseGetElements`, a function like `parseInnerTree`, except + that it always returns an array of elements, or an empty array. + + +1.1.0 (2015-06-29) +------------------ + +* #44, #45: Catching broken and invalid XML better and throwing + `Sabre\Xml\LibXMLException` whenever we encounter errors. (@stefanmajoor, + @DaanBiesterbos) + + +1.0.0 (2015-05-25) +------------------ + +* No functional changes since 0.4.3. Marking it as 1.0.0 as a promise for + API stability. +* Using php-cs-fixer for automated CS enforcement. + + +0.4.3 (2015-04-01) +----------------- + +* Minor tweaks for the public release. + + +0.4.2 (2015-03-20) +------------------ + +* Removed `constants.php` again. They messed with PHPUnit and don't really + provide a great benefit. +* #41: Correctly handle self-closing xml elements. + + +0.4.1 (2015-03-19) +------------------ + +* #40: An element with an empty namespace (xmlns="") is not allowed to have a + prefix. This is now fixed. + + +0.4.0 (2015-03-18) +------------------ + +* Added `Sabre\Xml\Service`. This is intended as a simple way to centrally + configure xml applications and easily parse/write things from there. #35, #38. +* Renamed 'baseUri' to 'contextUri' everywhere. +* #36: Added a few convenience constants to `lib/constants.php`. +* `Sabre\Xml\Util::parseClarkNotation` is now in the `Sabre\Xml\Service` class. + + +0.3.1 (2015-02-08) +------------------ + +* Added `XmlDeserializable` to match `XmlSerializable`. + + +0.3.0 (2015-02-06) +------------------ + +* Added `$elementMap` argument to parseInnerTree, for quickly overriding + parsing rules within an element. + + +0.2.2 (2015-02-05) +------------------ + +* Now depends on sabre/uri 1.0. + + +0.2.1 (2014-12-17) +------------------ + +* LibXMLException now inherits from ParseException, so it's easy for users to + catch any exception thrown by the parser. + + +0.2.0 (2014-12-05) +------------------ + +* Major BC Break: method names for the Element interface have been renamed + from `serializeXml` and `deserializeXml` to `xmlSerialize` and + `xmlDeserialize`. This is so that it matches PHP's `JsonSerializable` + interface. +* #25: Added `XmlSerializable` to allow people to write serializers without + having to implement a deserializer in the same class. +* #26: Renamed the `Sabre\XML` namespace to `Sabre\Xml`. Due to composer magic + and the fact that PHP namespace are case-insensitive, this should not affect + anyone, unless you are doing exact string matches on class names. +* #23: It's not possible to automatically extract or serialize Xml fragments + from documents using `Sabre\Xml\Element\XmlFragment`. + + +0.1.0 (2014-11-24) +------------------ + +* #16: Added ability to override `elementMap`, `namespaceMap` and `baseUri` for + a fragment of a document during reading an writing using `pushContext` and + `popContext`. +* Removed: `Writer::$context` and `Reader::$context`. +* #15: Added `Reader::$baseUri` to match `Writer::$baseUri`. +* #20: Allow callbacks to be used instead of `Element` classes in the `Reader`. +* #25: Added `readText` to quickly grab all text from a node and advance the + reader to the next node. +* #15: Added `Sabre\XML\Element\Uri`. + + +0.0.6 (2014-09-26) +------------------ + +* Added: `CData` element. +* #13: Better support for xml with no namespaces. (@kalmas) +* Switched to PSR-4 directory structure. + + +0.0.5 (2013-03-27) +------------------ + +* Added: baseUri property to the Writer class. +* Added: The writeElement method can now write complex elements. +* Added: Throwing exception when invalid objects are written. + + +0.0.4 (2013-03-14) +------------------ + +* Fixed: The KeyValue parser was skipping over elements when there was no + whitespace between them. +* Fixed: Clearing libxml errors after parsing. +* Added: Support for CDATA. +* Added: Context properties. + + +0.0.3 (2013-02-22) +------------------ + +* Changed: Reader::parse returns an array with 1 level less depth. +* Added: A LibXMLException is now thrown if the XMLReader comes across an error. +* Fixed: Both the Elements and KeyValue parsers had severe issues with + nesting. +* Fixed: The reader now detects when the end of the document is hit before it + should (because we're still parsing an element). + + +0.0.2 (2013-02-17) +------------------ + +* Added: Elements parser. +* Added: KeyValue parser. +* Change: Reader::parseSubTree is now named parseInnerTree, and returns either + a string (in case of a text-node), or an array (in case there were child + elements). +* Added: Reader::parseCurrentElement is now public. + + +0.0.1 (2013-02-07) +------------------ + +* First alpha release + +Project started: 2012-11-13. First experiments in June 2009. diff --git a/htdocs/includes/sabre/sabre/xml/LICENSE b/htdocs/includes/sabre/sabre/xml/LICENSE new file mode 100644 index 00000000000..c9faf409b95 --- /dev/null +++ b/htdocs/includes/sabre/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/htdocs/includes/sabre/sabre/xml/README.md b/htdocs/includes/sabre/sabre/xml/README.md new file mode 100644 index 00000000000..e6fc4db5f3d --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/README.md @@ -0,0 +1,25 @@ +sabre/xml +========= + +[![Build Status](https://secure.travis-ci.org/fruux/sabre-xml.svg?branch=master)](http://travis-ci.org/fruux/sabre-xml) + +The sabre/xml library is a specialized XML reader and writer. + +Documentation +------------- + +* [Introduction](http://sabre.io/xml/). +* [Installation](http://sabre.io/xml/install/). +* [Reading XML](http://sabre.io/xml/reading/). +* [Writing XML](http://sabre.io/xml/writing/). + + +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/htdocs/includes/sabre/sabre/xml/bin/.empty b/htdocs/includes/sabre/sabre/xml/bin/.empty new file mode 100644 index 00000000000..e69de29bb2d diff --git a/htdocs/includes/sabre/sabre/xml/composer.json b/htdocs/includes/sabre/sabre/xml/composer.json new file mode 100644 index 00000000000..386f8213f5e --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/composer.json @@ -0,0 +1,53 @@ +{ + "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" : ">=5.5.5", + "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": { + "sabre/cs": "~1.0.0", + "phpunit/phpunit" : "*" + }, + "config" : { + "bin-dir" : "bin/" + } +} diff --git a/htdocs/includes/sabre/sabre/xml/lib/ContextStackTrait.php b/htdocs/includes/sabre/sabre/xml/lib/ContextStackTrait.php new file mode 100644 index 00000000000..ee3a3baca5c --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/lib/ContextStackTrait.php @@ -0,0 +1,123 @@ +contextStack[] = [ + $this->elementMap, + $this->contextUri, + $this->namespaceMap, + $this->classMap + ]; + + } + + /** + * Restore the previous "context". + * + * @return null + */ + function popContext() { + + list( + $this->elementMap, + $this->contextUri, + $this->namespaceMap, + $this->classMap + ) = array_pop($this->contextStack); + + } + +} diff --git a/htdocs/includes/sabre/sabre/xml/lib/Deserializer/functions.php b/htdocs/includes/sabre/sabre/xml/lib/Deserializer/functions.php new file mode 100644 index 00000000000..2e5d877e912 --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/lib/Deserializer/functions.php @@ -0,0 +1,258 @@ +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. + * + * + * @param Reader $reader + * @param string $namespace + * @return array + */ +function keyValue(Reader $reader, $namespace = null) { + + // If there's no children, we don't do anything. + if ($reader->isEmptyElement) { + $reader->next(); + return []; + } + + $values = []; + + $reader->read(); + do { + + if ($reader->nodeType === Reader::ELEMENT) { + if ($namespace !== null && $reader->namespaceURI === $namespace) { + $values[$reader->localName] = $reader->parseCurrentElement()['value']; + } else { + $clark = $reader->getClark(); + $values[$clark] = $reader->parseCurrentElement()['value']; + } + } else { + $reader->read(); + } + } while ($reader->nodeType !== Reader::END_ELEMENT); + + $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", + * ]; + * + * @param Reader $reader + * @param string $namespace + * @return string[] + */ +function enum(Reader $reader, $namespace = null) { + + // If there's no children, we don't do anything. + if ($reader->isEmptyElement) { + $reader->next(); + return []; + } + $reader->read(); + $currentDepth = $reader->depth; + + $values = []; + do { + + if ($reader->nodeType !== Reader::ELEMENT) { + continue; + } + if (!is_null($namespace) && $namespace === $reader->namespaceURI) { + $values[] = $reader->localName; + } else { + $values[] = $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. + * + * @param Reader $reader + * @param string $className + * @param string $namespace + * @return object + */ +function valueObject(Reader $reader, $className, $namespace) { + + $valueObject = new $className(); + if ($reader->isEmptyElement) { + $reader->next(); + return $valueObject; + } + + $defaultProperties = get_class_vars($className); + + $reader->read(); + do { + + if ($reader->nodeType === Reader::ELEMENT && $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(); + } + } else { + $reader->read(); + } + } while ($reader->nodeType !== Reader::END_ELEMENT); + + $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. + * + * @param Reader $reader + * @param string $childElementName Element name in clark-notation + * @return array + */ +function repeatingElements(Reader $reader, $childElementName) { + + if ($childElementName[0] !== '{') { + $childElementName = '{}' . $childElementName; + } + $result = []; + + foreach ($reader->parseGetElements() as $element) { + + if ($element['name'] === $childElementName) { + $result[] = $element['value']; + } + + } + + return $result; + +} diff --git a/htdocs/includes/sabre/sabre/xml/lib/Element.php b/htdocs/includes/sabre/sabre/xml/lib/Element.php new file mode 100644 index 00000000000..dd89c58882c --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/lib/Element.php @@ -0,0 +1,20 @@ +value = $value; + + } + + /** + * The xmlSerialize metod 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. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Xml\Writer $writer) { + + $writer->write($this->value); + + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statictly, 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. + * + * @param Xml\Reader $reader + * @return mixed + */ + static function xmlDeserialize(Xml\Reader $reader) { + + $subTree = $reader->parseInnerTree(); + return $subTree; + + } + +} diff --git a/htdocs/includes/sabre/sabre/xml/lib/Element/Cdata.php b/htdocs/includes/sabre/sabre/xml/lib/Element/Cdata.php new file mode 100644 index 00000000000..5f42c4c6e8e --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/lib/Element/Cdata.php @@ -0,0 +1,64 @@ +value = $value; + } + + /** + * The xmlSerialize metod 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 XmlSerializble 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. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Xml\Writer $writer) { + + $writer->writeCData($this->value); + + } + +} diff --git a/htdocs/includes/sabre/sabre/xml/lib/Element/Elements.php b/htdocs/includes/sabre/sabre/xml/lib/Element/Elements.php new file mode 100644 index 00000000000..9eefd1bf884 --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/lib/Element/Elements.php @@ -0,0 +1,108 @@ + + * + * + * + * + * 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 + * + * @param array $value + */ + function __construct(array $value = []) { + + $this->value = $value; + + } + + /** + * The xmlSerialize metod 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 XmlSerializble 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. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Xml\Writer $writer) { + + Serializer\enum($writer, $this->value); + + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statictly, 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. + * + * @param Xml\Reader $reader + * @return mixed + */ + static function xmlDeserialize(Xml\Reader $reader) { + + return Deserializer\enum($reader); + + } + +} diff --git a/htdocs/includes/sabre/sabre/xml/lib/Element/KeyValue.php b/htdocs/includes/sabre/sabre/xml/lib/Element/KeyValue.php new file mode 100644 index 00000000000..7ce53bf4c6d --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/lib/Element/KeyValue.php @@ -0,0 +1,108 @@ +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 + * + * @param array $value + */ + function __construct(array $value = []) { + + $this->value = $value; + + } + + /** + * The xmlSerialize metod 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 XmlSerializble 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. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Xml\Writer $writer) { + + $writer->write($this->value); + + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called staticly, 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. + * + * @param Xml\Reader $reader + * @return mixed + */ + static function xmlDeserialize(Xml\Reader $reader) { + + return Deserializer\keyValue($reader); + + } + +} diff --git a/htdocs/includes/sabre/sabre/xml/lib/Element/Uri.php b/htdocs/includes/sabre/sabre/xml/lib/Element/Uri.php new file mode 100644 index 00000000000..8f45c0027eb --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/lib/Element/Uri.php @@ -0,0 +1,104 @@ +/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 + */ + function __construct($value) + { + $this->value = $value; + } + + /** + * The xmlSerialize metod 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 XmlSerializble 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. + * + * @param Writer $writer + * @return void + */ + 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. + * + * @param Xml\Reader $reader + * @return mixed + */ + static function xmlDeserialize(Xml\Reader $reader) { + + return new self( + \Sabre\Uri\resolve( + $reader->contextUri, + $reader->readText() + ) + ); + + } + +} diff --git a/htdocs/includes/sabre/sabre/xml/lib/Element/XmlFragment.php b/htdocs/includes/sabre/sabre/xml/lib/Element/XmlFragment.php new file mode 100644 index 00000000000..642241ca485 --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/lib/Element/XmlFragment.php @@ -0,0 +1,147 @@ +xml = $xml; + + } + + function getXml() { + + return $this->xml; + + } + + /** + * The xmlSerialize metod 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 XmlSerializble 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. + * + * @param Writer $writer + * @return void + */ + 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( + $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($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 statictly, 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. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + + $result = new self($reader->readInnerXml()); + $reader->next(); + return $result; + + } + +} diff --git a/htdocs/includes/sabre/sabre/xml/lib/LibXMLException.php b/htdocs/includes/sabre/sabre/xml/lib/LibXMLException.php new file mode 100644 index 00000000000..f0190eb51ea --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/lib/LibXMLException.php @@ -0,0 +1,53 @@ +errors = $errors; + parent::__construct($errors[0]->message . ' on line ' . $errors[0]->line . ', column ' . $errors[0]->column, $code, $previousException); + + } + + /** + * Returns the LibXML errors + * + * @return void + */ + function getErrors() { + + return $this->errors; + + } + +} diff --git a/htdocs/includes/sabre/sabre/xml/lib/ParseException.php b/htdocs/includes/sabre/sabre/xml/lib/ParseException.php new file mode 100644 index 00000000000..3a6883b2ff8 --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/lib/ParseException.php @@ -0,0 +1,17 @@ +localName) { + return null; + } + + return '{' . $this->namespaceURI . '}' . $this->localName; + + } + + /** + * Reads the entire document. + * + * This function returns an array with the following three elements: + * * name - The root element name. + * * value - The value for the root element. + * * attributes - An array of attributes. + * + * This function will also disable the standard libxml error handler (which + * usually just results in PHP errors), and throw exceptions instead. + * + * @return array + */ + function parse() { + + $previousEntityState = libxml_disable_entity_loader(true); + $previousSetting = libxml_use_internal_errors(true); + + try { + + // Really sorry about the silence operator, seems like I have no + // choice. See: + // + // https://bugs.php.net/bug.php?id=64230 + while ($this->nodeType !== self::ELEMENT && @$this->read()) { + // noop + } + $result = $this->parseCurrentElement(); + + $errors = libxml_get_errors(); + libxml_clear_errors(); + if ($errors) { + throw new LibXMLException($errors); + } + + } finally { + libxml_use_internal_errors($previousSetting); + libxml_disable_entity_loader($previousEntityState); + } + + return $result; + } + + + + /** + * parseGetElements parses everything in the current sub-tree, + * and returns a an array of elements. + * + * Each element has a 'name', 'value' and 'attributes' key. + * + * If the the element didn't contain sub-elements, an empty array is always + * returned. If there was any text inside the element, it will be + * discarded. + * + * If the $elementMap argument is specified, the existing elementMap will + * be overridden while parsing the tree, and restored after this process. + * + * @param array $elementMap + * @return array + */ + function parseGetElements(array $elementMap = null) { + + $result = $this->parseInnerTree($elementMap); + if (!is_array($result)) { + return []; + } + return $result; + + } + + /** + * Parses all elements below the current element. + * + * This method will return a string if this was a text-node, or an array if + * there were sub-elements. + * + * If there's both text and sub-elements, the text will be discarded. + * + * If the $elementMap argument is specified, the existing elementMap will + * be overridden while parsing the tree, and restored after this process. + * + * @param array $elementMap + * @return array|string + */ + function parseInnerTree(array $elementMap = null) { + + $text = null; + $elements = []; + + if ($this->nodeType === self::ELEMENT && $this->isEmptyElement) { + // Easy! + $this->next(); + return null; + } + + if (!is_null($elementMap)) { + $this->pushContext(); + $this->elementMap = $elementMap; + } + + try { + + // Really sorry about the silence operator, seems like I have no + // choice. See: + // + // https://bugs.php.net/bug.php?id=64230 + 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)'); + } + + while (true) { + + 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(); + break 2; + 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. + * + * @return string + */ + function readText() { + + $result = ''; + $previousDepth = $this->depth; + + while ($this->read() && $this->depth != $previousDepth) { + if (in_array($this->nodeType, [XMLReader::TEXT, XMLReader::CDATA, XMLReader::WHITESPACE])) { + $result .= $this->value; + } + } + return $result; + + } + + /** + * Parses the current XML element. + * + * This method returns arn array with 3 properties: + * * name - A clark-notation XML element name. + * * value - The parsed value. + * * attributes - A key-value list of attributes. + * + * @return array + */ + function parseCurrentElement() { + + $name = $this->getClark(); + + $attributes = []; + + if ($this->hasAttributes) { + $attributes = $this->parseAttributes(); + } + + $value = call_user_func( + $this->getDeserializerForElementName($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 retured in clark-notation. + * + * @return array + */ + function parseAttributes() { + + $attributes = []; + + while ($this->moveToNextAttribute()) { + if ($this->namespaceURI) { + + // Ignoring 'xmlns', it doesn't make any sense. + if ($this->namespaceURI === 'http://www.w3.org/2000/xmlns/') { + 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 it's clark-notation name. + * + * @param string $name + * @return callable + */ + function getDeserializerForElementName($name) { + + + 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 ($type === 'string') { + $type .= ' (' . $deserializer . ')'; + } elseif ($type === 'object') { + $type .= ' (' . get_class($deserializer) . ')'; + } + throw new \LogicException('Could not use this type as a deserializer: ' . $type . ' for element: ' . $name); + + } + +} diff --git a/htdocs/includes/sabre/sabre/xml/lib/Serializer/functions.php b/htdocs/includes/sabre/sabre/xml/lib/Serializer/functions.php new file mode 100644 index 00000000000..21448017d94 --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/lib/Serializer/functions.php @@ -0,0 +1,249 @@ + + * + * + * content + * + * + * @param Writer $writer + * @param string[] $values + * @return void + */ +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 Writer $writer + * @param object $valueObject + * @param string $namespace + */ +function valueObject(Writer $writer, $valueObject, $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 ($val !== null) { + $writer->writeElement('{' . $namespace . '}' . $key, $val); + } + } +} + + +/** + * This serializer helps you serialize xml structures that look like + * this: + * + * + * ... + * ... + * ... + * + * + * In that previous example, this serializer just serializes the item element, + * and this could be called like this: + * + * repeatingElements($writer, $items, '{}item'); + * + * @param Writer $writer + * @param array $items A list of items sabre/xml can serialize. + * @param string $childElementName Element name in clark-notation + * @return void + */ +function repeatingElements(Writer $writer, array $items, $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 Writer $writer + * @param string|int|float|bool|array|object + * @return void + */ +function standardSerializer(Writer $writer, $value) { + + if (is_scalar($value)) { + + // String, integer, float, boolean + $writer->text($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_null($value)) { + + // nothing! + + } 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)); + + } else { + + throw new InvalidArgumentException('The writer cannot serialize values of type: ' . gettype($value)); + + } + +} diff --git a/htdocs/includes/sabre/sabre/xml/lib/Service.php b/htdocs/includes/sabre/sabre/xml/lib/Service.php new file mode 100644 index 00000000000..09ee341cf8f --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/lib/Service.php @@ -0,0 +1,297 @@ +elementMap = $this->elementMap; + return $r; + + } + + /** + * Returns a fresh xml writer + * + * @return Writer + */ + function getWriter() { + + $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 + * @param string|null $contextUri + * @param string|null $rootElementName + * @throws ParseException + * @return array|object|string + */ + function parse($input, $contextUri = null, &$rootElementName = null) { + + if (is_resource($input)) { + // Unfortunately the XMLReader doesn't support streams. When it + // does, we can optimize this. + $input = stream_get_contents($input); + } + $r = $this->getReader(); + $r->contextUri = $contextUri; + $r->xml($input); + + $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 + * @param string|null $contextUri + * @return void + */ + function expect($rootElementName, $input, $contextUri = null) { + + if (is_resource($input)) { + // Unfortunately the XMLReader doesn't support streams. When it + // does, we can optimize this. + $input = stream_get_contents($input); + } + $r = $this->getReader(); + $r->contextUri = $contextUri; + $r->xml($input); + + $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 ', (array)$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 $rootElementName + * @param string|array|XmlSerializable $value + * @param string|null $contextUri + */ + function write($rootElementName, $value, $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 setup the Reader and Writer + * classes to turn a specific XML element to a PHP class. + * + * For example, given a class such as : + * + * class Author { + * public $firstName; + * public $lastName; + * } + * + * and an XML element such as: + * + * + * ... + * ... + * + * + * These can easily be mapped by calling: + * + * $service->mapValueObject('{http://example.org}author', 'Author'); + * + * @param string $elementName + * @param object $className + * @return void + */ + function mapValueObject($elementName, $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 + * @param string $contextUri + * @return void + */ + function writeValueObject($object, $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. + * + * @param string $str + * @throws InvalidArgumentException + * @return array + */ + static function parseClarkNotation($str) { + 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/htdocs/includes/sabre/sabre/xml/lib/Version.php b/htdocs/includes/sabre/sabre/xml/lib/Version.php new file mode 100644 index 00000000000..7edb40d67ac --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/lib/Version.php @@ -0,0 +1,19 @@ + "..", + * "{namespace}name2" => "..", + * ] + * + * One element will be created for each key in this array. The values of + * this array support any format this method supports (this method is + * called recursively). + * + * Array format 2: + * + * [ + * [ + * "name" => "{namespace}name1" + * "value" => "..", + * "attributes" => [ + * "attr" => "attribute value", + * ] + * ], + * [ + * "name" => "{namespace}name1" + * "value" => "..", + * "attributes" => [ + * "attr" => "attribute value", + * ] + * ] + * ] + * + * @param mixed $value + * @return void + */ + 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: + * + * + * + * @param string $name + * @return bool + */ + function startElement($name) { + + 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 === "" || $namespace === null) { + $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 + * + * @param string $name + * @param string $content + * @return bool + */ + function writeElement($name, $content = null) { + + $this->startElement($name); + if (!is_null($content)) { + $this->write($content); + } + $this->endElement(); + + } + + /** + * Writes a list of attributes. + * + * Attributes are specified as a key->value array. + * + * The key is an attribute name. If the key is a 'localName', the current + * xml namespace is assumed. If it's a 'clark notation key', this namespace + * will be used instead. + * + * @param array $attributes + * @return void + */ + 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. + * + * @param string $name + * @param string $value + * @return bool + */ + function writeAttribute($name, $value) { + + if ($name[0] === '{') { + + list( + $namespace, + $localName + ) = Service::parseClarkNotation($name); + + if (array_key_exists($namespace, $this->namespaceMap)) { + // It's an attribute with a namespace we know + $this->writeAttribute( + $this->namespaceMap[$namespace] . ':' . $localName, + $value + ); + } else { + + // 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); + } + $this->writeAttributeNS( + $this->adhocNamespaces[$namespace], + $localName, + $namespace, + $value + ); + + } + + } else { + return parent::writeAttribute($name, $value); + } + + } + +} diff --git a/htdocs/includes/sabre/sabre/xml/lib/XmlDeserializable.php b/htdocs/includes/sabre/sabre/xml/lib/XmlDeserializable.php new file mode 100644 index 00000000000..fa857e82c79 --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/lib/XmlDeserializable.php @@ -0,0 +1,38 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader); + +} diff --git a/htdocs/includes/sabre/sabre/xml/lib/XmlSerializable.php b/htdocs/includes/sabre/sabre/xml/lib/XmlSerializable.php new file mode 100644 index 00000000000..3e2c528b945 --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/lib/XmlSerializable.php @@ -0,0 +1,36 @@ +stack = $this->getMockForTrait('Sabre\\Xml\\ContextStackTrait'); + + } + + function testPushAndPull() { + + $this->stack->contextUri = '/foo/bar'; + $this->stack->elementMap['{DAV:}foo'] = 'Bar'; + $this->stack->namespaceMap['DAV:'] = 'd'; + + $this->stack->pushContext(); + + $this->assertEquals('/foo/bar', $this->stack->contextUri); + $this->assertEquals('Bar', $this->stack->elementMap['{DAV:}foo']); + $this->assertEquals('d', $this->stack->namespaceMap['DAV:']); + + $this->stack->contextUri = '/gir/zim'; + $this->stack->elementMap['{DAV:}foo'] = 'newBar'; + $this->stack->namespaceMap['DAV:'] = 'dd'; + + $this->stack->popContext(); + + $this->assertEquals('/foo/bar', $this->stack->contextUri); + $this->assertEquals('Bar', $this->stack->elementMap['{DAV:}foo']); + $this->assertEquals('d', $this->stack->namespaceMap['DAV:']); + + } + +} diff --git a/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Deserializer/EnumTest.php b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Deserializer/EnumTest.php new file mode 100644 index 00000000000..2eea9bb5aaf --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Deserializer/EnumTest.php @@ -0,0 +1,62 @@ +elementMap['{urn:test}root'] = 'Sabre\Xml\Deserializer\enum'; + + $xml = << + + + + +XML; + + $result = $service->parse($xml); + + $expected = [ + '{urn:test}foo1', + '{urn:test}foo2', + ]; + + + $this->assertEquals($expected, $result); + + + } + + function testDeserializeDefaultNamespace() { + + $service = new Service(); + $service->elementMap['{urn:test}root'] = function($reader) { + return enum($reader, 'urn:test'); + }; + + $xml = << + + + + +XML; + + $result = $service->parse($xml); + + $expected = [ + 'foo1', + 'foo2', + ]; + + + $this->assertEquals($expected, $result); + + } + +} diff --git a/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Deserializer/KeyValueTest.php b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Deserializer/KeyValueTest.php new file mode 100644 index 00000000000..a94ff4e01a1 --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Deserializer/KeyValueTest.php @@ -0,0 +1,112 @@ + + + + + hi + + foo + foo & bar + + + +BLA; + + $reader = new Reader(); + $reader->elementMap = [ + '{http://sabredav.org/ns}struct' => function(Reader $reader) { + return keyValue($reader, 'http://sabredav.org/ns'); + } + ]; + $reader->xml($input); + $output = $reader->parse(); + + $this->assertEquals([ + 'name' => '{http://sabredav.org/ns}root', + 'value' => [ + [ + 'name' => '{http://sabredav.org/ns}struct', + 'value' => [ + 'elem1' => null, + 'elem2' => 'hi', + '{http://sabredav.org/another-ns}elem3' => [ + [ + 'name' => '{http://sabredav.org/another-ns}elem4', + 'value' => 'foo', + 'attributes' => [], + ], + [ + 'name' => '{http://sabredav.org/another-ns}elem5', + 'value' => 'foo & bar', + 'attributes' => [], + ], + ] + ], + 'attributes' => [], + ] + ], + 'attributes' => [], + ], $output); + } + + /** + * @expectedException \Sabre\Xml\LibXMLException + */ + function testKeyValueLoop() { + + /** + * This bug is a weird one, because it triggers an infinite loop, but + * only if the XML document is a certain size (in bytes). Removing one + * or two characters from the xml body here cause the infinite loop to + * *not* get triggered, so to properly test this bug (Issue #94), don't + * change the XML body. + */ + $invalid_xml = ' + + + NONE + ENVELOPE + 1 + DC + + NONE + ENVELOPE + 1 + DC/FleetType> + + '; + $reader = new Reader(); + + $reader->xml($invalid_xml); + $reader->elementMap = [ + + '{}Package' => function($reader) { + $recipient = []; + // Borrowing a parser from the KeyValue class. + $keyValue = keyValue($reader); + + if (isset($keyValue['{}WeightOz'])){ + $recipient['referenceId'] = $keyValue['{}WeightOz']; + } + + return $recipient; + }, + ]; + + $reader->parse(); + + + } + +} diff --git a/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Deserializer/RepeatingElementsTest.php b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Deserializer/RepeatingElementsTest.php new file mode 100644 index 00000000000..025d997feb2 --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Deserializer/RepeatingElementsTest.php @@ -0,0 +1,35 @@ +elementMap['{urn:test}collection'] = function($reader) { + return repeatingElements($reader, '{urn:test}item'); + }; + + $xml = << + + foo + bar + +XML; + + $result = $service->parse($xml); + + $expected = [ + 'foo', + 'bar', + ]; + + $this->assertEquals($expected, $result); + + } + +} diff --git a/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Deserializer/ValueObjectTest.php b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Deserializer/ValueObjectTest.php new file mode 100644 index 00000000000..2d6ce98ce4f --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Deserializer/ValueObjectTest.php @@ -0,0 +1,169 @@ + + + Harry + Turtle + +XML; + + $reader = new Reader(); + $reader->xml($input); + $reader->elementMap = [ + '{urn:foo}foo' => function(Reader $reader) { + return valueObject($reader, 'Sabre\\Xml\\Deserializer\\TestVo', 'urn:foo'); + } + ]; + + $output = $reader->parse(); + + $vo = new TestVo(); + $vo->firstName = 'Harry'; + $vo->lastName = 'Turtle'; + + $expected = [ + 'name' => '{urn:foo}foo', + 'value' => $vo, + 'attributes' => [] + ]; + + $this->assertEquals( + $expected, + $output + ); + + } + + function testDeserializeValueObjectIgnoredElement() { + + $input = << + + Harry + Turtle + harry@example.org + +XML; + + $reader = new Reader(); + $reader->xml($input); + $reader->elementMap = [ + '{urn:foo}foo' => function(Reader $reader) { + return valueObject($reader, 'Sabre\\Xml\\Deserializer\\TestVo', 'urn:foo'); + } + ]; + + $output = $reader->parse(); + + $vo = new TestVo(); + $vo->firstName = 'Harry'; + $vo->lastName = 'Turtle'; + + $expected = [ + 'name' => '{urn:foo}foo', + 'value' => $vo, + 'attributes' => [] + ]; + + $this->assertEquals( + $expected, + $output + ); + + } + + function testDeserializeValueObjectAutoArray() { + + $input = << + + Harry + Turtle + http://example.org/ + http://example.net/ + +XML; + + $reader = new Reader(); + $reader->xml($input); + $reader->elementMap = [ + '{urn:foo}foo' => function(Reader $reader) { + return valueObject($reader, 'Sabre\\Xml\\Deserializer\\TestVo', 'urn:foo'); + } + ]; + + $output = $reader->parse(); + + $vo = new TestVo(); + $vo->firstName = 'Harry'; + $vo->lastName = 'Turtle'; + $vo->link = [ + 'http://example.org/', + 'http://example.net/', + ]; + + + $expected = [ + 'name' => '{urn:foo}foo', + 'value' => $vo, + 'attributes' => [] + ]; + + $this->assertEquals( + $expected, + $output + ); + + } + function testDeserializeValueObjectEmpty() { + + $input = << + +XML; + + $reader = new Reader(); + $reader->xml($input); + $reader->elementMap = [ + '{urn:foo}foo' => function(Reader $reader) { + return valueObject($reader, 'Sabre\\Xml\\Deserializer\\TestVo', 'urn:foo'); + } + ]; + + $output = $reader->parse(); + + $vo = new TestVo(); + + $expected = [ + 'name' => '{urn:foo}foo', + 'value' => $vo, + 'attributes' => [] + ]; + + $this->assertEquals( + $expected, + $output + ); + + } + +} + +class TestVo { + + public $firstName; + public $lastName; + + public $link = []; + +} diff --git a/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Element/CDataTest.php b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Element/CDataTest.php new file mode 100644 index 00000000000..2d12d7b2196 --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Element/CDataTest.php @@ -0,0 +1,58 @@ + + + + +BLA; + + $reader = new Reader(); + $reader->elementMap = [ + '{http://sabredav.org/ns}blabla' => 'Sabre\\Xml\\Element\\Cdata', + ]; + $reader->xml($input); + + $output = $reader->parse(); + + } + + function testSerialize() { + + $writer = new Writer(); + $writer->namespaceMap = [ + 'http://sabredav.org/ns' => null + ]; + $writer->openMemory(); + $writer->startDocument('1.0'); + $writer->setIndent(true); + $writer->write([ + '{http://sabredav.org/ns}root' => new Cdata(''), + ]); + + $output = $writer->outputMemory(); + + $expected = << +]]> + +XML; + + $this->assertEquals($expected, $output); + + + } + +} diff --git a/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Element/Eater.php b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Element/Eater.php new file mode 100644 index 00000000000..aaba2a01f2b --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Element/Eater.php @@ -0,0 +1,78 @@ +startElement('{http://sabredav.org/ns}elem1'); + $writer->write('hiiii!'); + $writer->endElement(); + + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statictly, 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. + * + * @param Xml\Reader $reader + * @return mixed + */ + static function xmlDeserialize(Xml\Reader $reader) { + + $reader->next(); + + $count = 1; + while ($count) { + + $reader->read(); + if ($reader->nodeType === $reader::END_ELEMENT) { + $count--; + } + + } + $reader->read(); + + } + +} diff --git a/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Element/ElementsTest.php b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Element/ElementsTest.php new file mode 100644 index 00000000000..f17f2094b2e --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Element/ElementsTest.php @@ -0,0 +1,129 @@ + + + + + + + + content + + + + + + + + + +BLA; + + $reader = new Reader(); + $reader->elementMap = [ + '{http://sabredav.org/ns}listThingy' => 'Sabre\\Xml\\Element\\Elements', + ]; + $reader->xml($input); + + $output = $reader->parse(); + + $this->assertEquals([ + 'name' => '{http://sabredav.org/ns}root', + 'value' => [ + [ + 'name' => '{http://sabredav.org/ns}listThingy', + 'value' => [ + '{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', + '{http://sabredav.org/ns}elem6', + ], + 'attributes' => [], + ], + [ + 'name' => '{http://sabredav.org/ns}listThingy', + 'value' => [], + 'attributes' => [], + ], + [ + 'name' => '{http://sabredav.org/ns}otherThing', + 'value' => [ + [ + 'name' => '{http://sabredav.org/ns}elem1', + 'value' => null, + 'attributes' => [], + ], + [ + 'name' => '{http://sabredav.org/ns}elem2', + 'value' => null, + 'attributes' => [], + ], + [ + 'name' => '{http://sabredav.org/ns}elem3', + 'value' => null, + 'attributes' => [], + ], + ], + 'attributes' => [], + ], + ], + 'attributes' => [], + ], $output); + + } + + function testSerialize() { + + $value = [ + '{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', + '{http://sabredav.org/ns}elem6', + ]; + + $writer = new Writer(); + $writer->namespaceMap = [ + 'http://sabredav.org/ns' => null + ]; + $writer->openMemory(); + $writer->startDocument('1.0'); + $writer->setIndent(true); + $writer->write([ + '{http://sabredav.org/ns}root' => new Elements($value), + ]); + + $output = $writer->outputMemory(); + + $expected = << + + + + + + + + + +XML; + + $this->assertEquals($expected, $output); + + + } + +} diff --git a/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Element/KeyValueTest.php b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Element/KeyValueTest.php new file mode 100644 index 00000000000..51c87b5203d --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Element/KeyValueTest.php @@ -0,0 +1,210 @@ + + + + + hi + + foo + foo & bar + + Hithere + + + + + + +BLA; + + $reader = new Reader(); + $reader->elementMap = [ + '{http://sabredav.org/ns}struct' => 'Sabre\\Xml\\Element\\KeyValue', + ]; + $reader->xml($input); + + $output = $reader->parse(); + + $this->assertEquals([ + 'name' => '{http://sabredav.org/ns}root', + 'value' => [ + [ + 'name' => '{http://sabredav.org/ns}struct', + 'value' => [ + '{http://sabredav.org/ns}elem1' => null, + '{http://sabredav.org/ns}elem2' => 'hi', + '{http://sabredav.org/ns}elem3' => [ + [ + 'name' => '{http://sabredav.org/ns}elem4', + 'value' => 'foo', + 'attributes' => [], + ], + [ + 'name' => '{http://sabredav.org/ns}elem5', + 'value' => 'foo & bar', + 'attributes' => [], + ], + ], + '{http://sabredav.org/ns}elem6' => 'Hithere', + ], + 'attributes' => [], + ], + [ + 'name' => '{http://sabredav.org/ns}struct', + 'value' => [], + 'attributes' => [], + ], + [ + 'name' => '{http://sabredav.org/ns}otherThing', + 'value' => [ + [ + 'name' => '{http://sabredav.org/ns}elem1', + 'value' => null, + 'attributes' => [], + ], + ], + 'attributes' => [], + ], + ], + 'attributes' => [], + ], $output); + + } + + /** + * This test was added to find out why an element gets eaten by the + * SabreDAV MKCOL parser. + */ + function testElementEater() { + + $input = << + + + + + bla + + + +BLA; + + $reader = new Reader(); + $reader->elementMap = [ + '{DAV:}set' => 'Sabre\\Xml\\Element\\KeyValue', + '{DAV:}prop' => 'Sabre\\Xml\\Element\\KeyValue', + '{DAV:}resourcetype' => 'Sabre\\Xml\\Element\\Elements', + ]; + $reader->xml($input); + + $expected = [ + 'name' => '{DAV:}mkcol', + 'value' => [ + [ + 'name' => '{DAV:}set', + 'value' => [ + '{DAV:}prop' => [ + '{DAV:}resourcetype' => [ + '{DAV:}collection', + ], + '{DAV:}displayname' => 'bla', + ], + ], + 'attributes' => [], + ], + ], + 'attributes' => [], + ]; + + $this->assertEquals($expected, $reader->parse()); + + } + + + function testSerialize() { + + $value = [ + '{http://sabredav.org/ns}elem1' => null, + '{http://sabredav.org/ns}elem2' => 'textValue', + '{http://sabredav.org/ns}elem3' => [ + '{http://sabredav.org/ns}elem4' => 'text2', + '{http://sabredav.org/ns}elem5' => null, + ], + '{http://sabredav.org/ns}elem6' => 'text3', + ]; + + $writer = new Writer(); + $writer->namespaceMap = [ + 'http://sabredav.org/ns' => null + ]; + $writer->openMemory(); + $writer->startDocument('1.0'); + $writer->setIndent(true); + $writer->write([ + '{http://sabredav.org/ns}root' => new KeyValue($value), + ]); + + $output = $writer->outputMemory(); + + $expected = << + + + textValue + + text2 + + + text3 + + +XML; + + $this->assertEquals($expected, $output); + + } + + /** + * I discovered that when there's no whitespace between elements, elements + * can get skipped. + */ + function testElementSkipProblem() { + + $input = << + +val3val4val5 +BLA; + + $reader = new Reader(); + $reader->elementMap = [ + '{http://sabredav.org/ns}root' => 'Sabre\\Xml\\Element\\KeyValue', + ]; + $reader->xml($input); + + $output = $reader->parse(); + + $this->assertEquals([ + 'name' => '{http://sabredav.org/ns}root', + 'value' => [ + '{http://sabredav.org/ns}elem3' => 'val3', + '{http://sabredav.org/ns}elem4' => 'val4', + '{http://sabredav.org/ns}elem5' => 'val5', + ], + 'attributes' => [], + ], $output); + + } + +} diff --git a/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Element/Mock.php b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Element/Mock.php new file mode 100644 index 00000000000..f96684cb55a --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Element/Mock.php @@ -0,0 +1,60 @@ +startElement('{http://sabredav.org/ns}elem1'); + $writer->write('hiiii!'); + $writer->endElement(); + + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statictly, 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. + * + * @param Xml\Reader $reader + * @return mixed + */ + static function xmlDeserialize(Xml\Reader $reader) { + + $reader->next(); + return 'foobar'; + + } + +} diff --git a/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Element/UriTest.php b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Element/UriTest.php new file mode 100644 index 00000000000..53f89ed7aae --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Element/UriTest.php @@ -0,0 +1,76 @@ + + + /foo/bar + +BLA; + + $reader = new Reader(); + $reader->contextUri = 'http://example.org/'; + $reader->elementMap = [ + '{http://sabredav.org/ns}uri' => 'Sabre\\Xml\\Element\\Uri', + ]; + $reader->xml($input); + + $output = $reader->parse(); + + $this->assertEquals( + [ + 'name' => '{http://sabredav.org/ns}root', + 'value' => [ + [ + 'name' => '{http://sabredav.org/ns}uri', + 'value' => new Uri('http://example.org/foo/bar'), + 'attributes' => [], + ] + ], + 'attributes' => [], + ], + $output + ); + + } + + function testSerialize() { + + $writer = new Writer(); + $writer->namespaceMap = [ + 'http://sabredav.org/ns' => null + ]; + $writer->openMemory(); + $writer->startDocument('1.0'); + $writer->setIndent(true); + $writer->contextUri = 'http://example.org/'; + $writer->write([ + '{http://sabredav.org/ns}root' => [ + '{http://sabredav.org/ns}uri' => new Uri('/foo/bar'), + ] + ]); + + $output = $writer->outputMemory(); + + $expected = << + + http://example.org/foo/bar + + +XML; + + $this->assertEquals($expected, $output); + + + } + +} diff --git a/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Element/XmlFragmentTest.php b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Element/XmlFragmentTest.php new file mode 100644 index 00000000000..461cc155ca6 --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Element/XmlFragmentTest.php @@ -0,0 +1,143 @@ + + + $input + +BLA; + + $reader = new Reader(); + $reader->elementMap = [ + '{http://sabredav.org/ns}fragment' => 'Sabre\\Xml\\Element\\XmlFragment', + ]; + $reader->xml($input); + + $output = $reader->parse(); + + $this->assertEquals([ + 'name' => '{http://sabredav.org/ns}root', + 'value' => [ + [ + 'name' => '{http://sabredav.org/ns}fragment', + 'value' => new XmlFragment($expected), + 'attributes' => [], + ], + ], + 'attributes' => [], + ], $output); + + } + + /** + * Data provider for serialize and deserialize tests. + * + * Returns three items per test: + * + * 1. Input data for the reader. + * 2. Expected output for XmlFragment deserializer + * 3. Expected output after serializing that value again. + * + * If 3 is not set, use 1 for 3. + * + * @return void + */ + function xmlProvider() { + + return [ + [ + 'hello', + 'hello', + ], + [ + 'hello', + 'hello' + ], + [ + 'hello', + 'hello' + ], + [ + 'hello', + 'hello' + ], + [ + 'hello', + 'hello', + 'hello', + ], + [ + 'hello', + 'hello', + 'hello', + ], + [ + 'hello', + 'hello', + 'hello', + ], + [ + 'hello', + 'hello', + 'hello', + ], + [ + '', + '', + '', + ], + [ + '', + '', + '', + ], + ]; + + } + + /** + * @dataProvider xmlProvider + */ + function testSerialize($expectedFallback, $input, $expected = null) { + + if (is_null($expected)) { + $expected = $expectedFallback; + } + + $writer = new Writer(); + $writer->namespaceMap = [ + 'http://sabredav.org/ns' => null + ]; + $writer->openMemory(); + $writer->startDocument('1.0'); + //$writer->setIndent(true); + $writer->write([ + '{http://sabredav.org/ns}root' => [ + '{http://sabredav.org/ns}fragment' => new XmlFragment($input), + ], + ]); + + $output = $writer->outputMemory(); + + $expected = << +$expected +XML; + + $this->assertEquals($expected, $output); + + } + +} diff --git a/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/InfiteLoopTest.php b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/InfiteLoopTest.php new file mode 100644 index 00000000000..ec8a136d092 --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/InfiteLoopTest.php @@ -0,0 +1,50 @@ + + + + +'; + + $reader = new Reader(); + $reader->elementMap = [ + '{DAV:}set' => 'Sabre\\Xml\\Element\\KeyValue', + ]; + $reader->xml($body); + + $output = $reader->parse(); + + $this->assertEquals([ + 'name' => '{DAV:}propertyupdate', + 'value' => [ + [ + 'name' => '{DAV:}set', + 'value' => [ + '{DAV:}prop' => null, + ], + 'attributes' => [], + ], + [ + 'name' => '{DAV:}set', + 'value' => [ + '{DAV:}prop' => null, + ], + 'attributes' => [], + ], + ], + 'attributes' => [], + ], $output); + + } + +} diff --git a/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/ReaderTest.php b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/ReaderTest.php new file mode 100644 index 00000000000..8da81d1202a --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/ReaderTest.php @@ -0,0 +1,585 @@ + + +BLA; + $reader = new Reader(); + $reader->xml($input); + + $reader->next(); + + $this->assertEquals('{http://sabredav.org/ns}root', $reader->getClark()); + + } + + function testGetClarkNoNS() { + + $input = << + +BLA; + $reader = new Reader(); + $reader->xml($input); + + $reader->next(); + + $this->assertEquals('{}root', $reader->getClark()); + + } + + function testGetClarkNotOnAnElement() { + + $input = << + +BLA; + $reader = new Reader(); + $reader->xml($input); + + $this->assertNull($reader->getClark()); + } + + function testSimple() { + + $input = << + + + + Hi! + + +BLA; + + $reader = new Reader(); + $reader->xml($input); + + $output = $reader->parse(); + + $expected = [ + 'name' => '{http://sabredav.org/ns}root', + 'value' => [ + [ + 'name' => '{http://sabredav.org/ns}elem1', + 'value' => null, + 'attributes' => [ + 'attr' => 'val', + ], + ], + [ + 'name' => '{http://sabredav.org/ns}elem2', + 'value' => [ + [ + 'name' => '{http://sabredav.org/ns}elem3', + 'value' => 'Hi!', + 'attributes' => [], + ], + ], + 'attributes' => [], + ], + + ], + 'attributes' => [], + + ]; + + $this->assertEquals($expected, $output); + + } + + function testCDATA() { + + $input = << + + + +BLA; + + $reader = new Reader(); + $reader->xml($input); + + $output = $reader->parse(); + + $expected = [ + 'name' => '{http://sabredav.org/ns}root', + 'value' => [ + [ + 'name' => '{http://sabredav.org/ns}foo', + 'value' => 'bar', + 'attributes' => [], + ], + + ], + 'attributes' => [], + + ]; + + $this->assertEquals($expected, $output); + + } + + function testSimpleNamespacedAttribute() { + + $input = << + + + +BLA; + + $reader = new Reader(); + $reader->xml($input); + + $output = $reader->parse(); + + $expected = [ + 'name' => '{http://sabredav.org/ns}root', + 'value' => [ + [ + 'name' => '{http://sabredav.org/ns}elem1', + 'value' => null, + 'attributes' => [ + '{urn:foo}attr' => 'val', + ], + ], + ], + 'attributes' => [], + ]; + + $this->assertEquals($expected, $output); + + } + + function testMappedElement() { + + $input = << + + + +BLA; + + $reader = new Reader(); + $reader->elementMap = [ + '{http://sabredav.org/ns}elem1' => 'Sabre\\Xml\\Element\\Mock' + ]; + $reader->xml($input); + + $output = $reader->parse(); + + $expected = [ + 'name' => '{http://sabredav.org/ns}root', + 'value' => [ + [ + 'name' => '{http://sabredav.org/ns}elem1', + 'value' => 'foobar', + 'attributes' => [], + ], + ], + 'attributes' => [], + + ]; + + $this->assertEquals($expected, $output); + + } + + /** + * @expectedException \LogicException + */ + function testMappedElementBadClass() { + + $input = << + + + +BLA; + + $reader = new Reader(); + $reader->elementMap = [ + '{http://sabredav.org/ns}elem1' => new \StdClass() + ]; + $reader->xml($input); + + $reader->parse(); + } + + /** + * @depends testMappedElement + */ + function testMappedElementCallBack() { + + $input = << + + + +BLA; + + $reader = new Reader(); + $reader->elementMap = [ + '{http://sabredav.org/ns}elem1' => function(Reader $reader) { + $reader->next(); + return 'foobar'; + } + ]; + $reader->xml($input); + + $output = $reader->parse(); + + $expected = [ + 'name' => '{http://sabredav.org/ns}root', + 'value' => [ + [ + 'name' => '{http://sabredav.org/ns}elem1', + 'value' => 'foobar', + 'attributes' => [], + ], + ], + 'attributes' => [], + + ]; + + $this->assertEquals($expected, $output); + + } + + /** + * @depends testMappedElementCallBack + */ + function testMappedElementCallBackNoNamespace() { + + $input = << + + + +BLA; + + $reader = new Reader(); + $reader->elementMap = [ + 'elem1' => function(Reader $reader) { + $reader->next(); + return 'foobar'; + } + ]; + $reader->xml($input); + + $output = $reader->parse(); + + $expected = [ + 'name' => '{}root', + 'value' => [ + [ + 'name' => '{}elem1', + 'value' => 'foobar', + 'attributes' => [], + ], + ], + 'attributes' => [], + + ]; + + $this->assertEquals($expected, $output); + + } + + /** + * @depends testMappedElementCallBack + */ + function testReadText() { + + $input = << + + + hello + world + + +BLA; + + $reader = new Reader(); + $reader->elementMap = [ + '{http://sabredav.org/ns}elem1' => function(Reader $reader) { + return $reader->readText(); + } + ]; + $reader->xml($input); + + $output = $reader->parse(); + + $expected = [ + 'name' => '{http://sabredav.org/ns}root', + 'value' => [ + [ + 'name' => '{http://sabredav.org/ns}elem1', + 'value' => 'hello world', + 'attributes' => [], + ], + ], + 'attributes' => [], + + ]; + + $this->assertEquals($expected, $output); + + } + + function testParseProblem() { + + $input = << + +BLA; + + $reader = new Reader(); + $reader->elementMap = [ + '{http://sabredav.org/ns}elem1' => 'Sabre\\Xml\\Element\\Mock' + ]; + $reader->xml($input); + + try { + $output = $reader->parse(); + $this->fail('We expected a ParseException to be thrown'); + } catch (LibXMLException $e) { + + $this->assertInternalType('array', $e->getErrors()); + + } + + } + + /** + * @expectedException \Sabre\Xml\ParseException + */ + function testBrokenParserClass() { + + $input = << + + + +BLA; + + $reader = new Reader(); + $reader->elementMap = [ + '{http://sabredav.org/ns}elem1' => 'Sabre\\Xml\\Element\\Eater' + ]; + $reader->xml($input); + $reader->parse(); + + + } + + /** + * Test was added for Issue #10. + * + * @expectedException Sabre\Xml\LibXMLException + */ + function testBrokenXml() { + + $input = << + + + +BLA; + + $reader = new Reader(); + $reader->xml($input); + $reader->parse(); + + } + + /** + * Test was added for Issue #45. + * + * @expectedException Sabre\Xml\LibXMLException + */ + function testBrokenXml2() { + + $input = << + + + + + + ""Administrative w"> + + + + xml($input); + $reader->parse(); + + } + + + /** + * @depends testMappedElement + */ + function testParseInnerTree() { + + $input = << + + + + + +BLA; + + $reader = new Reader(); + $reader->elementMap = [ + '{http://sabredav.org/ns}elem1' => function(Reader $reader) { + + $innerTree = $reader->parseInnerTree(['{http://sabredav.org/ns}elem1' => function(Reader $reader) { + $reader->next(); + return "foobar"; + }]); + + return $innerTree; + } + ]; + $reader->xml($input); + + $output = $reader->parse(); + + $expected = [ + 'name' => '{http://sabredav.org/ns}root', + 'value' => [ + [ + 'name' => '{http://sabredav.org/ns}elem1', + 'value' => [ + [ + 'name' => '{http://sabredav.org/ns}elem1', + 'value' => 'foobar', + 'attributes' => [], + ] + ], + 'attributes' => [], + ], + ], + 'attributes' => [], + + ]; + + $this->assertEquals($expected, $output); + + } + + /** + * @depends testParseInnerTree + */ + function testParseGetElements() { + + $input = << + + + + + +BLA; + + $reader = new Reader(); + $reader->elementMap = [ + '{http://sabredav.org/ns}elem1' => function(Reader $reader) { + + $innerTree = $reader->parseGetElements(['{http://sabredav.org/ns}elem1' => function(Reader $reader) { + $reader->next(); + return "foobar"; + }]); + + return $innerTree; + } + ]; + $reader->xml($input); + + $output = $reader->parse(); + + $expected = [ + 'name' => '{http://sabredav.org/ns}root', + 'value' => [ + [ + 'name' => '{http://sabredav.org/ns}elem1', + 'value' => [ + [ + 'name' => '{http://sabredav.org/ns}elem1', + 'value' => 'foobar', + 'attributes' => [], + ] + ], + 'attributes' => [], + ], + ], + 'attributes' => [], + + ]; + + $this->assertEquals($expected, $output); + + } + + /** + * @depends testParseInnerTree + */ + function testParseGetElementsNoElements() { + + $input = << + + + hi + + +BLA; + + $reader = new Reader(); + $reader->elementMap = [ + '{http://sabredav.org/ns}elem1' => function(Reader $reader) { + + $innerTree = $reader->parseGetElements(['{http://sabredav.org/ns}elem1' => function(Reader $reader) { + $reader->next(); + return "foobar"; + }]); + + return $innerTree; + } + ]; + $reader->xml($input); + + $output = $reader->parse(); + + $expected = [ + 'name' => '{http://sabredav.org/ns}root', + 'value' => [ + [ + 'name' => '{http://sabredav.org/ns}elem1', + 'value' => [], + 'attributes' => [], + ], + ], + 'attributes' => [], + + ]; + + $this->assertEquals($expected, $output); + + } + + +} diff --git a/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Serializer/EnumTest.php b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Serializer/EnumTest.php new file mode 100644 index 00000000000..2d26e665a2a --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Serializer/EnumTest.php @@ -0,0 +1,36 @@ +namespaceMap['urn:test'] = null; + + $xml = $service->write('{urn:test}root', function($writer) { + enum($writer, [ + '{urn:test}foo1', + '{urn:test}foo2', + ]); + }); + + $expected = << + + + + +XML; + + + $this->assertXmlStringEqualsXmlString($expected, $xml); + + + } + + +} diff --git a/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Serializer/RepeatingElementsTest.php b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Serializer/RepeatingElementsTest.php new file mode 100644 index 00000000000..dbca65a572a --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/Serializer/RepeatingElementsTest.php @@ -0,0 +1,35 @@ +namespaceMap['urn:test'] = null; + $xml = $service->write('{urn:test}collection', function($writer) { + repeatingElements($writer, [ + 'foo', + 'bar', + ], '{urn:test}item'); + }); + + $expected = << + + foo + bar + +XML; + + + $this->assertXmlStringEqualsXmlString($expected, $xml); + + + } + + +} diff --git a/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/ServiceTest.php b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/ServiceTest.php new file mode 100644 index 00000000000..e6fcf149926 --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/ServiceTest.php @@ -0,0 +1,328 @@ + 'Test!', + ]; + + $util = new Service(); + $util->elementMap = $elems; + + $reader = $util->getReader(); + $this->assertInstanceOf('Sabre\\Xml\\Reader', $reader); + $this->assertEquals($elems, $reader->elementMap); + + } + + function testGetWriter() { + + $ns = [ + 'http://sabre.io/ns' => 's', + ]; + + $util = new Service(); + $util->namespaceMap = $ns; + + $writer = $util->getWriter(); + $this->assertInstanceOf('Sabre\\Xml\\Writer', $writer); + $this->assertEquals($ns, $writer->namespaceMap); + + } + + /** + * @depends testGetReader + */ + function testParse() { + + $xml = << + value + +XML; + $util = new Service(); + $result = $util->parse($xml, null, $rootElement); + $this->assertEquals('{http://sabre.io/ns}root', $rootElement); + + $expected = [ + [ + 'name' => '{http://sabre.io/ns}child', + 'value' => 'value', + 'attributes' => [], + ] + ]; + + $this->assertEquals( + $expected, + $result + ); + + } + + /** + * @depends testGetReader + */ + function testParseStream() { + + $xml = << + value + +XML; + $stream = fopen('php://memory', 'r+'); + fwrite($stream, $xml); + rewind($stream); + + $util = new Service(); + $result = $util->parse($stream, null, $rootElement); + $this->assertEquals('{http://sabre.io/ns}root', $rootElement); + + $expected = [ + [ + 'name' => '{http://sabre.io/ns}child', + 'value' => 'value', + 'attributes' => [], + ] + ]; + + $this->assertEquals( + $expected, + $result + ); + + } + + /** + * @depends testGetReader + */ + function testExpect() { + + $xml = << + value + +XML; + $util = new Service(); + $result = $util->expect('{http://sabre.io/ns}root', $xml); + + $expected = [ + [ + 'name' => '{http://sabre.io/ns}child', + 'value' => 'value', + 'attributes' => [], + ] + ]; + + $this->assertEquals( + $expected, + $result + ); + } + + /** + * @depends testGetReader + */ + function testExpectStream() { + + $xml = << + value + +XML; + + $stream = fopen('php://memory', 'r+'); + fwrite($stream, $xml); + rewind($stream); + + $util = new Service(); + $result = $util->expect('{http://sabre.io/ns}root', $stream); + + $expected = [ + [ + 'name' => '{http://sabre.io/ns}child', + 'value' => 'value', + 'attributes' => [], + ] + ]; + + $this->assertEquals( + $expected, + $result + ); + } + + /** + * @depends testGetReader + * @expectedException \Sabre\Xml\ParseException + */ + function testExpectWrong() { + + $xml = << + value + +XML; + $util = new Service(); + $util->expect('{http://sabre.io/ns}error', $xml); + + } + + /** + * @depends testGetWriter + */ + function testWrite() { + + $util = new Service(); + $util->namespaceMap = [ + 'http://sabre.io/ns' => 's', + ]; + $result = $util->write('{http://sabre.io/ns}root', [ + '{http://sabre.io/ns}child' => 'value', + ]); + + $expected = << + + value + + +XML; + $this->assertEquals( + $expected, + $result + ); + + } + + function testMapValueObject() { + + $input = << + + 1234 + 99.99 + black friday deal + + 5 + + + + +XML; + + $ns = 'http://sabredav.org/ns'; + $orderService = new \Sabre\Xml\Service(); + $orderService->mapValueObject('{' . $ns . '}order', 'Sabre\Xml\Order'); + $orderService->mapValueObject('{' . $ns . '}status', 'Sabre\Xml\OrderStatus'); + $orderService->namespaceMap[$ns] = null; + + $order = $orderService->parse($input); + $expected = new Order(); + $expected->id = 1234; + $expected->amount = 99.99; + $expected->description = 'black friday deal'; + $expected->status = new OrderStatus(); + $expected->status->id = 5; + $expected->status->label = 'processed'; + + $this->assertEquals($expected, $order); + + $writtenXml = $orderService->writeValueObject($order); + $this->assertEquals($input, $writtenXml); + } + + function testMapValueObjectArrayProperty() { + + $input = << + + 1234 + 99.99 + black friday deal + + 5 + + + http://example.org/ + http://example.com/ + + +XML; + + $ns = 'http://sabredav.org/ns'; + $orderService = new \Sabre\Xml\Service(); + $orderService->mapValueObject('{' . $ns . '}order', 'Sabre\Xml\Order'); + $orderService->mapValueObject('{' . $ns . '}status', 'Sabre\Xml\OrderStatus'); + $orderService->namespaceMap[$ns] = null; + + $order = $orderService->parse($input); + $expected = new Order(); + $expected->id = 1234; + $expected->amount = 99.99; + $expected->description = 'black friday deal'; + $expected->status = new OrderStatus(); + $expected->status->id = 5; + $expected->status->label = 'processed'; + $expected->link = ['http://example.org/', 'http://example.com/']; + + $this->assertEquals($expected, $order); + + $writtenXml = $orderService->writeValueObject($order); + $this->assertEquals($input, $writtenXml); + } + + /** + * @expectedException \InvalidArgumentException + */ + function testWriteVoNotFound() { + + $service = new Service(); + $service->writeValueObject(new \StdClass()); + + } + + function testParseClarkNotation() { + + $this->assertEquals([ + 'http://sabredav.org/ns', + 'elem', + ], Service::parseClarkNotation('{http://sabredav.org/ns}elem')); + + } + + /** + * @expectedException \InvalidArgumentException + */ + function testParseClarkNotationFail() { + + Service::parseClarkNotation('http://sabredav.org/ns}elem'); + + } + +} + +/** + * asset for testMapValueObject() + * @internal + */ +class Order { + public $id; + public $amount; + public $description; + public $status; + public $empty; + public $link = []; +} + +/** + * asset for testMapValueObject() + * @internal + */ +class OrderStatus { + public $id; + public $label; +} diff --git a/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/WriterTest.php b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/WriterTest.php new file mode 100644 index 00000000000..574d8023700 --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/tests/Sabre/Xml/WriterTest.php @@ -0,0 +1,439 @@ +writer = new Writer(); + $this->writer->namespaceMap = [ + 'http://sabredav.org/ns' => 's', + ]; + $this->writer->openMemory(); + $this->writer->setIndent(true); + $this->writer->startDocument(); + + } + + function compare($input, $output) { + + $this->writer->write($input); + $this->assertEquals($output, $this->writer->outputMemory()); + + } + + + function testSimple() { + + $this->compare([ + '{http://sabredav.org/ns}root' => 'text', + ], << +text + +HI + ); + + } + + /** + * @depends testSimple + */ + function testSimpleQuotes() { + + $this->compare([ + '{http://sabredav.org/ns}root' => '"text"', + ], << +"text" + +HI + ); + + } + + function testSimpleAttributes() { + + $this->compare([ + '{http://sabredav.org/ns}root' => [ + 'value' => 'text', + 'attributes' => [ + 'attr1' => 'attribute value', + ], + ], + ], << +text + +HI + ); + + } + function testMixedSyntax() { + $this->compare([ + '{http://sabredav.org/ns}root' => [ + '{http://sabredav.org/ns}single' => 'value', + '{http://sabredav.org/ns}multiple' => [ + [ + 'name' => '{http://sabredav.org/ns}foo', + 'value' => 'bar', + ], + [ + 'name' => '{http://sabredav.org/ns}foo', + 'value' => 'foobar', + ], + ], + [ + 'name' => '{http://sabredav.org/ns}attributes', + 'value' => null, + 'attributes' => [ + 'foo' => 'bar', + ], + ], + [ + 'name' => '{http://sabredav.org/ns}verbose', + 'value' => 'syntax', + 'attributes' => [ + 'foo' => 'bar', + ], + ], + ], + ], << + + value + + bar + foobar + + + syntax + + +HI + ); + } + + function testNull() { + + $this->compare([ + '{http://sabredav.org/ns}root' => null, + ], << + + +HI + ); + + } + + function testArrayFormat2() { + + $this->compare([ + '{http://sabredav.org/ns}root' => [ + [ + 'name' => '{http://sabredav.org/ns}elem1', + 'value' => 'text', + 'attributes' => [ + 'attr1' => 'attribute value', + ], + ], + ], + ], << + + text + + +HI + ); + + } + + function testArrayOfValues() { + + $this->compare([ + '{http://sabredav.org/ns}root' => [ + [ + 'name' => '{http://sabredav.org/ns}elem1', + 'value' => [ + 'foo', + 'bar', + 'baz', + ], + ], + ], + ], << + + foobarbaz + + +HI + ); + + } + + /** + * @depends testArrayFormat2 + */ + function testArrayFormat2NoValue() { + + $this->compare([ + '{http://sabredav.org/ns}root' => [ + [ + 'name' => '{http://sabredav.org/ns}elem1', + 'attributes' => [ + 'attr1' => 'attribute value', + ], + ], + ], + ], << + + + + +HI + ); + + } + + function testCustomNamespace() { + + $this->compare([ + '{http://sabredav.org/ns}root' => [ + '{urn:foo}elem1' => 'bar', + ], + ], << + + bar + + +HI + ); + + } + + function testEmptyNamespace() { + + // Empty namespaces are allowed, so we should support this. + $this->compare([ + '{http://sabredav.org/ns}root' => [ + '{}elem1' => 'bar', + ], + ], << + + bar + + +HI + ); + + } + + function testAttributes() { + + $this->compare([ + '{http://sabredav.org/ns}root' => [ + [ + 'name' => '{http://sabredav.org/ns}elem1', + 'value' => 'text', + 'attributes' => [ + 'attr1' => 'val1', + '{http://sabredav.org/ns}attr2' => 'val2', + '{urn:foo}attr3' => 'val3', + ], + ], + ], + ], << + + text + + +HI + ); + + } + + function testBaseElement() { + + $this->compare([ + '{http://sabredav.org/ns}root' => new Element\Base('hello') + ], << +hello + +HI + ); + + } + + function testElementObj() { + + $this->compare([ + '{http://sabredav.org/ns}root' => new Element\Mock() + ], << + + hiiii! + + +HI + ); + + } + + function testEmptyNamespacePrefix() { + + $this->writer->namespaceMap['http://sabredav.org/ns'] = null; + $this->compare([ + '{http://sabredav.org/ns}root' => new Element\Mock() + ], << + + hiiii! + + +HI + ); + + } + + function testEmptyNamespacePrefixEmptyString() { + + $this->writer->namespaceMap['http://sabredav.org/ns'] = ''; + $this->compare([ + '{http://sabredav.org/ns}root' => new Element\Mock() + ], << + + hiiii! + + +HI + ); + + } + + function testWriteElement() { + + $this->writer->writeElement("{http://sabredav.org/ns}foo", 'content'); + + $output = << +content + +HI; + + $this->assertEquals($output, $this->writer->outputMemory()); + + + } + + function testWriteElementComplex() { + + $this->writer->writeElement("{http://sabredav.org/ns}foo", new Element\KeyValue(['{http://sabredav.org/ns}bar' => 'test'])); + + $output = << + + test + + +HI; + + $this->assertEquals($output, $this->writer->outputMemory()); + + } + + /** + * @expectedException \InvalidArgumentException + */ + function testWriteBadObject() { + + $this->writer->write(new \StdClass()); + + } + + function testStartElementSimple() { + + $this->writer->startElement("foo"); + $this->writer->endElement(); + + $output = << + + +HI; + + $this->assertEquals($output, $this->writer->outputMemory()); + + } + + function testCallback() { + + $this->compare([ + '{http://sabredav.org/ns}root' => function(Writer $writer) { + $writer->text('deferred writer'); + }, + ], << +deferred writer + +HI + ); + + } + + /** + * @expectedException \InvalidArgumentException + */ + function testResource() { + + $this->compare([ + '{http://sabredav.org/ns}root' => fopen('php://memory', 'r'), + ], << +deferred writer + +HI + ); + + } + + function testClassMap() { + + $obj = (object)[ + 'key1' => 'value1', + 'key2' => 'value2', + ]; + + $this->writer->classMap['stdClass'] = function(Writer $writer, $value) { + + foreach (get_object_vars($value) as $key => $val) { + $writer->writeElement('{http://sabredav.org/ns}' . $key, $val); + } + + }; + + $this->compare([ + '{http://sabredav.org/ns}root' => $obj + ], << + + value1 + value2 + + +HI + ); + + } +} diff --git a/htdocs/includes/sabre/sabre/xml/tests/phpcs/ruleset.xml b/htdocs/includes/sabre/sabre/xml/tests/phpcs/ruleset.xml new file mode 100644 index 00000000000..07acb89eea0 --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/tests/phpcs/ruleset.xml @@ -0,0 +1,56 @@ + + + sabre.io codesniffer ruleset + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/htdocs/includes/sabre/sabre/xml/tests/phpunit.xml.dist b/htdocs/includes/sabre/sabre/xml/tests/phpunit.xml.dist new file mode 100644 index 00000000000..fe8b2359aa6 --- /dev/null +++ b/htdocs/includes/sabre/sabre/xml/tests/phpunit.xml.dist @@ -0,0 +1,17 @@ + + + Sabre/ + + + + + ../lib/ + + + diff --git a/htdocs/modulebuilder/template/core/modules/modMyModule.class.php b/htdocs/modulebuilder/template/core/modules/modMyModule.class.php index eea2a85e656..42d40e4dcc5 100644 --- a/htdocs/modulebuilder/template/core/modules/modMyModule.class.php +++ b/htdocs/modulebuilder/template/core/modules/modMyModule.class.php @@ -100,7 +100,7 @@ class modMyModule extends DolibarrModules // Data directories to create when module is enabled. // Example: this->dirs = array("/mymodule/temp","/mymodule/subdir"); - $this->dirs = array(); + $this->dirs = array("/mymodule/temp"); // Config pages. Put here list of php page, stored into mymodule/admin directory, to use to setup module. $this->config_page_url = array("setup.php@mymodule");